From 2368d3b69d1fd59105279fd21b3ebcba8ec45af3 Mon Sep 17 00:00:00 2001 From: pfZhu <530675553@qq.com> Date: Fri, 20 May 2022 13:25:36 +0800 Subject: [PATCH] init ernie-sat --- ernie-sat/.DS_Store | Bin 0 -> 8196 bytes ernie-sat/README_zh.md | 87 ++ ernie-sat/model_paddle.py | 1057 ++++++++++++++++ ernie-sat/paddlespeech/__init__.py | 16 + ernie-sat/paddlespeech/cli/README.md | 44 + ernie-sat/paddlespeech/cli/README_cn.md | 45 + ernie-sat/paddlespeech/cli/__init__.py | 26 + ernie-sat/paddlespeech/cli/asr/__init__.py | 14 + ernie-sat/paddlespeech/cli/asr/infer.py | 544 +++++++++ ernie-sat/paddlespeech/cli/base_commands.py | 49 + ernie-sat/paddlespeech/cli/cls/__init__.py | 14 + ernie-sat/paddlespeech/cli/cls/infer.py | 295 +++++ ernie-sat/paddlespeech/cli/download.py | 329 +++++ ernie-sat/paddlespeech/cli/entry.py | 41 + ernie-sat/paddlespeech/cli/executor.py | 229 ++++ ernie-sat/paddlespeech/cli/log.py | 59 + ernie-sat/paddlespeech/cli/st/__init__.py | 14 + ernie-sat/paddlespeech/cli/st/infer.py | 380 ++++++ ernie-sat/paddlespeech/cli/stats/__init__.py | 14 + ernie-sat/paddlespeech/cli/stats/infer.py | 193 +++ ernie-sat/paddlespeech/cli/text/__init__.py | 14 + ernie-sat/paddlespeech/cli/text/infer.py | 322 +++++ ernie-sat/paddlespeech/cli/tts/__init__.py | 14 + ernie-sat/paddlespeech/cli/tts/infer.py | 838 +++++++++++++ ernie-sat/paddlespeech/cli/utils.py | 340 ++++++ ernie-sat/paddlespeech/cli/vector/__init__.py | 14 + ernie-sat/paddlespeech/cli/vector/infer.py | 519 ++++++++ ernie-sat/paddlespeech/cls/__init__.py | 13 + ernie-sat/paddlespeech/cls/exps/__init__.py | 13 + .../paddlespeech/cls/exps/panns/__init__.py | 13 + .../cls/exps/panns/deploy/__init__.py | 13 + .../cls/exps/panns/deploy/predict.py | 145 +++ .../cls/exps/panns/export_model.py | 45 + .../paddlespeech/cls/exps/panns/predict.py | 72 ++ .../paddlespeech/cls/exps/panns/train.py | 172 +++ ernie-sat/paddlespeech/cls/models/__init__.py | 14 + .../paddlespeech/cls/models/panns/__init__.py | 15 + .../cls/models/panns/classifier.py | 36 + .../paddlespeech/cls/models/panns/panns.py | 309 +++++ ernie-sat/paddlespeech/s2t/__init__.py | 507 ++++++++ ernie-sat/paddlespeech/s2t/decoders/README.md | 14 + .../paddlespeech/s2t/decoders/__init__.py | 13 + .../s2t/decoders/beam_search/__init__.py | 17 + .../decoders/beam_search/batch_beam_search.py | 18 + .../s2t/decoders/beam_search/beam_search.py | 531 ++++++++ .../s2t/decoders/ctcdecoder/__init__.py | 18 + .../ctcdecoder/decoders_deprecated.py | 248 ++++ .../decoders/ctcdecoder/scorer_deprecated.py | 78 ++ .../s2t/decoders/ctcdecoder/swig_wrapper.py | 159 +++ .../ctcdecoder/tests/test_decoders.py | 100 ++ ernie-sat/paddlespeech/s2t/decoders/recog.py | 196 +++ .../paddlespeech/s2t/decoders/recog_bin.py | 376 ++++++ .../s2t/decoders/scorers/__init__.py | 13 + .../paddlespeech/s2t/decoders/scorers/ctc.py | 164 +++ .../s2t/decoders/scorers/ctc_prefix_score.py | 370 ++++++ .../s2t/decoders/scorers/length_bonus.py | 73 ++ .../s2t/decoders/scorers/ngram.py | 116 ++ .../s2t/decoders/scorers/scorer_interface.py | 202 +++ ernie-sat/paddlespeech/s2t/decoders/utils.py | 130 ++ ernie-sat/paddlespeech/s2t/exps/__init__.py | 62 + .../s2t/exps/deepspeech2/__init__.py | 13 + .../s2t/exps/deepspeech2/bin/__init__.py | 13 + .../exps/deepspeech2/bin/deploy/__init__.py | 13 + .../s2t/exps/deepspeech2/bin/deploy/client.py | 95 ++ .../s2t/exps/deepspeech2/bin/deploy/record.py | 55 + .../exps/deepspeech2/bin/deploy/runtime.py | 198 +++ .../s2t/exps/deepspeech2/bin/deploy/send.py | 50 + .../s2t/exps/deepspeech2/bin/deploy/server.py | 133 ++ .../s2t/exps/deepspeech2/bin/export.py | 56 + .../s2t/exps/deepspeech2/bin/test.py | 60 + .../s2t/exps/deepspeech2/bin/test_export.py | 65 + .../s2t/exps/deepspeech2/bin/test_wav.py | 205 ++++ .../s2t/exps/deepspeech2/bin/train.py | 56 + .../s2t/exps/deepspeech2/model.py | 649 ++++++++++ .../s2t/exps/lm/transformer/__init__.py | 13 + .../s2t/exps/lm/transformer/bin/__init__.py | 13 + .../lm/transformer/bin/cacu_perplexity.py | 82 ++ .../exps/lm/transformer/lm_cacu_perplexity.py | 132 ++ .../paddlespeech/s2t/exps/u2/__init__.py | 13 + .../paddlespeech/s2t/exps/u2/bin/__init__.py | 13 + .../paddlespeech/s2t/exps/u2/bin/alignment.py | 57 + .../paddlespeech/s2t/exps/u2/bin/export.py | 53 + .../paddlespeech/s2t/exps/u2/bin/test.py | 64 + .../paddlespeech/s2t/exps/u2/bin/test_wav.py | 141 +++ .../paddlespeech/s2t/exps/u2/bin/train.py | 61 + ernie-sat/paddlespeech/s2t/exps/u2/model.py | 546 +++++++++ ernie-sat/paddlespeech/s2t/exps/u2/trainer.py | 219 ++++ .../s2t/exps/u2_kaldi/__init__.py | 13 + .../s2t/exps/u2_kaldi/bin/__init__.py | 13 + .../s2t/exps/u2_kaldi/bin/recog.py | 19 + .../s2t/exps/u2_kaldi/bin/test.py | 87 ++ .../s2t/exps/u2_kaldi/bin/train.py | 69 ++ .../paddlespeech/s2t/exps/u2_kaldi/model.py | 509 ++++++++ .../paddlespeech/s2t/exps/u2_st/__init__.py | 13 + .../s2t/exps/u2_st/bin/__init__.py | 13 + .../paddlespeech/s2t/exps/u2_st/bin/export.py | 53 + .../paddlespeech/s2t/exps/u2_st/bin/test.py | 64 + .../paddlespeech/s2t/exps/u2_st/bin/train.py | 59 + .../paddlespeech/s2t/exps/u2_st/model.py | 552 +++++++++ .../paddlespeech/s2t/frontend/__init__.py | 13 + ernie-sat/paddlespeech/s2t/frontend/audio.py | 730 +++++++++++ .../s2t/frontend/augmentor/__init__.py | 13 + .../s2t/frontend/augmentor/augmentation.py | 230 ++++ .../s2t/frontend/augmentor/base.py | 59 + .../frontend/augmentor/impulse_response.py | 52 + .../s2t/frontend/augmentor/noise_perturb.py | 66 + .../online_bayesian_normalization.py | 63 + .../s2t/frontend/augmentor/resample.py | 48 + .../s2t/frontend/augmentor/shift_perturb.py | 49 + .../s2t/frontend/augmentor/spec_augment.py | 256 ++++ .../s2t/frontend/augmentor/speed_perturb.py | 106 ++ .../s2t/frontend/augmentor/volume_perturb.py | 55 + .../s2t/frontend/featurizer/__init__.py | 16 + .../frontend/featurizer/audio_featurizer.py | 363 ++++++ .../frontend/featurizer/speech_featurizer.py | 106 ++ .../frontend/featurizer/text_featurizer.py | 235 ++++ .../paddlespeech/s2t/frontend/normalizer.py | 200 +++ ernie-sat/paddlespeech/s2t/frontend/speech.py | 243 ++++ .../paddlespeech/s2t/frontend/utility.py | 393 ++++++ ernie-sat/paddlespeech/s2t/io/__init__.py | 13 + ernie-sat/paddlespeech/s2t/io/batchfy.py | 470 +++++++ ernie-sat/paddlespeech/s2t/io/collator.py | 347 ++++++ ernie-sat/paddlespeech/s2t/io/converter.py | 107 ++ ernie-sat/paddlespeech/s2t/io/dataloader.py | 201 +++ ernie-sat/paddlespeech/s2t/io/dataset.py | 231 ++++ ernie-sat/paddlespeech/s2t/io/reader.py | 414 +++++++ ernie-sat/paddlespeech/s2t/io/sampler.py | 251 ++++ ernie-sat/paddlespeech/s2t/io/utility.py | 109 ++ ernie-sat/paddlespeech/s2t/models/__init__.py | 13 + .../paddlespeech/s2t/models/asr_interface.py | 162 +++ .../paddlespeech/s2t/models/ds2/__init__.py | 29 + ernie-sat/paddlespeech/s2t/models/ds2/conv.py | 171 +++ .../s2t/models/ds2/deepspeech2.py | 267 ++++ ernie-sat/paddlespeech/s2t/models/ds2/rnn.py | 315 +++++ .../s2t/models/ds2_online/__init__.py | 29 + .../s2t/models/ds2_online/conv.py | 33 + .../s2t/models/ds2_online/deepspeech2.py | 397 ++++++ .../paddlespeech/s2t/models/lm/__init__.py | 13 + .../paddlespeech/s2t/models/lm/dataset.py | 74 ++ .../paddlespeech/s2t/models/lm/transformer.py | 266 ++++ .../paddlespeech/s2t/models/lm_interface.py | 83 ++ .../paddlespeech/s2t/models/st_interface.py | 76 ++ .../paddlespeech/s2t/models/u2/__init__.py | 19 + ernie-sat/paddlespeech/s2t/models/u2/u2.py | 926 ++++++++++++++ .../paddlespeech/s2t/models/u2/updater.py | 150 +++ .../paddlespeech/s2t/models/u2_st/__init__.py | 15 + .../paddlespeech/s2t/models/u2_st/u2_st.py | 676 ++++++++++ .../paddlespeech/s2t/modules/__init__.py | 13 + .../paddlespeech/s2t/modules/activation.py | 164 +++ ernie-sat/paddlespeech/s2t/modules/align.py | 139 +++ .../paddlespeech/s2t/modules/attention.py | 237 ++++ ernie-sat/paddlespeech/s2t/modules/cmvn.py | 53 + .../s2t/modules/conformer_convolution.py | 166 +++ ernie-sat/paddlespeech/s2t/modules/crf.py | 370 ++++++ ernie-sat/paddlespeech/s2t/modules/ctc.py | 470 +++++++ ernie-sat/paddlespeech/s2t/modules/decoder.py | 252 ++++ .../paddlespeech/s2t/modules/decoder_layer.py | 155 +++ .../paddlespeech/s2t/modules/embedding.py | 165 +++ ernie-sat/paddlespeech/s2t/modules/encoder.py | 495 ++++++++ .../paddlespeech/s2t/modules/encoder_layer.py | 288 +++++ .../paddlespeech/s2t/modules/initializer.py | 172 +++ ernie-sat/paddlespeech/s2t/modules/loss.py | 185 +++ ernie-sat/paddlespeech/s2t/modules/mask.py | 277 +++++ .../s2t/modules/positionwise_feed_forward.py | 60 + .../paddlespeech/s2t/modules/subsampling.py | 250 ++++ .../paddlespeech/s2t/training/__init__.py | 13 + ernie-sat/paddlespeech/s2t/training/cli.py | 127 ++ .../s2t/training/extensions/__init__.py | 41 + .../s2t/training/extensions/evaluator.py | 102 ++ .../s2t/training/extensions/extension.py | 53 + .../s2t/training/extensions/plot.py | 419 +++++++ .../s2t/training/extensions/snapshot.py | 134 ++ .../s2t/training/extensions/visualizer.py | 39 + .../paddlespeech/s2t/training/gradclip.py | 85 ++ .../paddlespeech/s2t/training/optimizer.py | 122 ++ .../paddlespeech/s2t/training/reporter.py | 145 +++ .../paddlespeech/s2t/training/scheduler.py | 130 ++ ernie-sat/paddlespeech/s2t/training/timer.py | 50 + .../paddlespeech/s2t/training/trainer.py | 492 ++++++++ .../s2t/training/triggers/__init__.py | 13 + .../triggers/compare_value_trigger.py | 62 + .../s2t/training/triggers/interval_trigger.py | 39 + .../s2t/training/triggers/limit_trigger.py | 32 + .../s2t/training/triggers/time_trigger.py | 42 + .../s2t/training/triggers/utils.py | 28 + .../s2t/training/updaters/__init__.py | 13 + .../s2t/training/updaters/standard_updater.py | 196 +++ .../s2t/training/updaters/trainer.py | 185 +++ .../s2t/training/updaters/updater.py | 85 ++ .../paddlespeech/s2t/transform/__init__.py | 13 + .../paddlespeech/s2t/transform/add_deltas.py | 54 + .../s2t/transform/channel_selector.py | 57 + ernie-sat/paddlespeech/s2t/transform/cmvn.py | 201 +++ .../paddlespeech/s2t/transform/functional.py | 86 ++ .../paddlespeech/s2t/transform/perturb.py | 470 +++++++ .../s2t/transform/spec_augment.py | 214 ++++ .../paddlespeech/s2t/transform/spectrogram.py | 475 +++++++ .../s2t/transform/transform_interface.py | 35 + .../s2t/transform/transformation.py | 158 +++ ernie-sat/paddlespeech/s2t/transform/wpe.py | 58 + ernie-sat/paddlespeech/s2t/utils/__init__.py | 13 + ernie-sat/paddlespeech/s2t/utils/asr_utils.py | 52 + .../paddlespeech/s2t/utils/bleu_score.py | 118 ++ .../paddlespeech/s2t/utils/check_kwargs.py | 35 + .../paddlespeech/s2t/utils/checkpoint.py | 298 +++++ .../paddlespeech/s2t/utils/cli_readers.py | 242 ++++ ernie-sat/paddlespeech/s2t/utils/cli_utils.py | 71 ++ .../paddlespeech/s2t/utils/cli_writers.py | 294 +++++ ernie-sat/paddlespeech/s2t/utils/ctc_utils.py | 211 ++++ .../paddlespeech/s2t/utils/dynamic_import.py | 69 ++ .../s2t/utils/dynamic_pip_install.py | 22 + .../paddlespeech/s2t/utils/error_rate.py | 364 ++++++ .../paddlespeech/s2t/utils/layer_tools.py | 88 ++ ernie-sat/paddlespeech/s2t/utils/log.py | 162 +++ ernie-sat/paddlespeech/s2t/utils/mp_tools.py | 30 + ernie-sat/paddlespeech/s2t/utils/profiler.py | 119 ++ .../paddlespeech/s2t/utils/socket_server.py | 113 ++ .../paddlespeech/s2t/utils/spec_augment.py | 13 + .../paddlespeech/s2t/utils/tensor_utils.py | 195 +++ ernie-sat/paddlespeech/s2t/utils/text_grid.py | 128 ++ ernie-sat/paddlespeech/s2t/utils/utility.py | 140 +++ ernie-sat/paddlespeech/server/README.md | 37 + ernie-sat/paddlespeech/server/README_cn.md | 37 + ernie-sat/paddlespeech/server/__init__.py | 25 + .../paddlespeech/server/base_commands.py | 82 ++ ernie-sat/paddlespeech/server/bin/__init__.py | 17 + ernie-sat/paddlespeech/server/bin/main.py | 77 ++ .../server/bin/paddlespeech_client.py | 289 +++++ .../server/bin/paddlespeech_server.py | 198 +++ .../paddlespeech/server/conf/application.yaml | 157 +++ .../server/conf/ws_application.yaml | 51 + ernie-sat/paddlespeech/server/download.py | 329 +++++ .../paddlespeech/server/engine/__init__.py | 13 + .../server/engine/asr/__init__.py | 13 + .../server/engine/asr/online/__init__.py | 13 + .../server/engine/asr/online/asr_engine.py | 352 ++++++ .../engine/asr/paddleinference/__init__.py | 13 + .../engine/asr/paddleinference/asr_engine.py | 240 ++++ .../server/engine/asr/python/__init__.py | 13 + .../server/engine/asr/python/asr_engine.py | 100 ++ .../paddlespeech/server/engine/base_engine.py | 58 + .../server/engine/cls/__init__.py | 13 + .../engine/cls/paddleinference/__init__.py | 13 + .../engine/cls/paddleinference/cls_engine.py | 224 ++++ .../server/engine/cls/python/__init__.py | 13 + .../server/engine/cls/python/cls_engine.py | 124 ++ .../server/engine/engine_factory.py | 44 + .../paddlespeech/server/engine/engine_pool.py | 40 + .../server/engine/tts/__init__.py | 13 + .../engine/tts/paddleinference/__init__.py | 13 + .../engine/tts/paddleinference/tts_engine.py | 534 ++++++++ .../server/engine/tts/python/__init__.py | 13 + .../server/engine/tts/python/tts_engine.py | 253 ++++ ernie-sat/paddlespeech/server/entry.py | 57 + ernie-sat/paddlespeech/server/executor.py | 46 + .../paddlespeech/server/restful/__init__.py | 13 + ernie-sat/paddlespeech/server/restful/api.py | 44 + .../paddlespeech/server/restful/asr_api.py | 91 ++ .../paddlespeech/server/restful/cls_api.py | 92 ++ .../paddlespeech/server/restful/request.py | 80 ++ .../paddlespeech/server/restful/response.py | 148 +++ .../paddlespeech/server/restful/tts_api.py | 127 ++ .../server/tests/asr/http_client.py | 59 + .../tests/asr/online/microphone_client.py | 161 +++ .../tests/asr/online/websocket_client.py | 115 ++ .../server/tests/tts/test_client.py | 104 ++ ernie-sat/paddlespeech/server/util.py | 367 ++++++ .../paddlespeech/server/utils/__init__.py | 13 + .../server/utils/audio_process.py | 105 ++ ernie-sat/paddlespeech/server/utils/buffer.py | 59 + ernie-sat/paddlespeech/server/utils/config.py | 30 + ernie-sat/paddlespeech/server/utils/errors.py | 57 + .../paddlespeech/server/utils/exception.py | 30 + ernie-sat/paddlespeech/server/utils/log.py | 59 + .../server/utils/paddle_predictor.py | 98 ++ ernie-sat/paddlespeech/server/utils/util.py | 33 + ernie-sat/paddlespeech/server/utils/vad.py | 78 ++ ernie-sat/paddlespeech/server/ws/__init__.py | 13 + ernie-sat/paddlespeech/server/ws/api.py | 38 + .../paddlespeech/server/ws/asr_socket.py | 100 ++ ernie-sat/paddlespeech/t2s/__init__.py | 22 + ernie-sat/paddlespeech/t2s/audio/__init__.py | 17 + ernie-sat/paddlespeech/t2s/audio/audio.py | 102 ++ ernie-sat/paddlespeech/t2s/audio/codec.py | 51 + .../paddlespeech/t2s/audio/spec_normalizer.py | 74 ++ .../paddlespeech/t2s/datasets/__init__.py | 14 + .../paddlespeech/t2s/datasets/am_batch_fn.py | 295 +++++ ernie-sat/paddlespeech/t2s/datasets/batch.py | 188 +++ .../paddlespeech/t2s/datasets/data_table.py | 133 ++ .../paddlespeech/t2s/datasets/dataset.py | 261 ++++ .../paddlespeech/t2s/datasets/get_feats.py | 226 ++++ .../paddlespeech/t2s/datasets/ljspeech.py | 39 + .../t2s/datasets/preprocess_utils.py | 169 +++ .../t2s/datasets/vocoder_batch_fn.py | 220 ++++ ernie-sat/paddlespeech/t2s/exps/__init__.py | 13 + .../paddlespeech/t2s/exps/csmsc_test.txt | 100 ++ .../t2s/exps/fastspeech2/__init__.py | 13 + .../t2s/exps/fastspeech2/gen_gta_mel.py | 226 ++++ .../t2s/exps/fastspeech2/normalize.py | 184 +++ .../t2s/exps/fastspeech2/preprocess.py | 370 ++++++ .../t2s/exps/fastspeech2/train.py | 212 ++++ .../t2s/exps/gan_vocoder/README.md | 1 + .../t2s/exps/gan_vocoder/__init__.py | 13 + .../t2s/exps/gan_vocoder/hifigan/__init__.py | 13 + .../t2s/exps/gan_vocoder/hifigan/train.py | 275 +++++ .../gan_vocoder/multi_band_melgan/__init__.py | 13 + .../gan_vocoder/multi_band_melgan/train.py | 264 ++++ .../t2s/exps/gan_vocoder/normalize.py | 133 ++ .../gan_vocoder/parallelwave_gan/__init__.py | 13 + .../parallelwave_gan/synthesize_from_wav.py | 118 ++ .../gan_vocoder/parallelwave_gan/train.py | 264 ++++ .../t2s/exps/gan_vocoder/preprocess.py | 292 +++++ .../exps/gan_vocoder/style_melgan/__init__.py | 13 + .../exps/gan_vocoder/style_melgan/train.py | 256 ++++ .../t2s/exps/gan_vocoder/synthesize.py | 121 ++ .../t2s/exps/gan_vocoder/synthesize_fxr.py | 121 ++ ernie-sat/paddlespeech/t2s/exps/inference.py | 246 ++++ .../paddlespeech/t2s/exps/ort_predict.py | 156 +++ .../paddlespeech/t2s/exps/ort_predict_e2e.py | 183 +++ ernie-sat/paddlespeech/t2s/exps/sentences.txt | 16 + .../paddlespeech/t2s/exps/sentences_en.txt | 9 + .../t2s/exps/speedyspeech/__init__.py | 13 + .../t2s/exps/speedyspeech/gen_gta_mel.py | 244 ++++ .../t2s/exps/speedyspeech/inference.py | 115 ++ .../t2s/exps/speedyspeech/normalize.py | 166 +++ .../t2s/exps/speedyspeech/preprocess.py | 298 +++++ .../t2s/exps/speedyspeech/synthesize_e2e.py | 203 +++ .../t2s/exps/speedyspeech/train.py | 239 ++++ ernie-sat/paddlespeech/t2s/exps/syn_utils.py | 243 ++++ ernie-sat/paddlespeech/t2s/exps/synthesize.py | 200 +++ .../paddlespeech/t2s/exps/synthesize_e2e.py | 247 ++++ .../t2s/exps/synthesize_streaming.py | 273 +++++ .../t2s/exps/tacotron2/__init__.py | 13 + .../t2s/exps/tacotron2/normalize.py | 146 +++ .../t2s/exps/tacotron2/preprocess.py | 329 +++++ .../paddlespeech/t2s/exps/tacotron2/train.py | 202 +++ .../t2s/exps/transformer_tts/__init__.py | 13 + .../t2s/exps/transformer_tts/normalize.py | 146 +++ .../t2s/exps/transformer_tts/preprocess.py | 275 +++++ .../t2s/exps/transformer_tts/synthesize.py | 146 +++ .../exps/transformer_tts/synthesize_e2e.py | 165 +++ .../t2s/exps/transformer_tts/train.py | 193 +++ .../paddlespeech/t2s/exps/voice_cloning.py | 202 +++ .../t2s/exps/waveflow/__init__.py | 13 + .../paddlespeech/t2s/exps/waveflow/config.py | 55 + .../t2s/exps/waveflow/ljspeech.py | 89 ++ .../t2s/exps/waveflow/preprocess.py | 160 +++ .../t2s/exps/waveflow/synthesize.py | 87 ++ .../paddlespeech/t2s/exps/waveflow/train.py | 160 +++ .../paddlespeech/t2s/exps/wavernn/__init__.py | 13 + .../t2s/exps/wavernn/synthesize.py | 108 ++ .../paddlespeech/t2s/exps/wavernn/train.py | 212 ++++ .../paddlespeech/t2s/frontend/__init__.py | 20 + .../paddlespeech/t2s/frontend/arpabet.py | 268 ++++ .../t2s/frontend/generate_lexicon.py | 158 +++ .../t2s/frontend/normalizer/__init__.py | 15 + .../t2s/frontend/normalizer/abbrrviation.py | 13 + .../t2s/frontend/normalizer/acronyms.py | 13 + .../t2s/frontend/normalizer/normalizer.py | 34 + .../t2s/frontend/normalizer/numbers.py | 86 ++ .../t2s/frontend/normalizer/width.py | 40 + .../paddlespeech/t2s/frontend/phonectic.py | 294 +++++ .../paddlespeech/t2s/frontend/punctuation.py | 36 + .../paddlespeech/t2s/frontend/tone_sandhi.py | 348 ++++++ ernie-sat/paddlespeech/t2s/frontend/vocab.py | 120 ++ .../paddlespeech/t2s/frontend/zh_frontend.py | 314 +++++ .../t2s/frontend/zh_normalization/README.md | 16 + .../t2s/frontend/zh_normalization/__init__.py | 14 + .../frontend/zh_normalization/char_convert.py | 46 + .../frontend/zh_normalization/chronology.py | 134 ++ .../frontend/zh_normalization/constants.py | 62 + .../t2s/frontend/zh_normalization/num.py | 238 ++++ .../frontend/zh_normalization/phonecode.py | 63 + .../frontend/zh_normalization/quantifier.py | 37 + .../zh_normalization/text_normlization.py | 116 ++ ernie-sat/paddlespeech/t2s/models/__init__.py | 22 + .../t2s/models/fastspeech2/__init__.py | 15 + .../t2s/models/fastspeech2/fastspeech2.py | 1057 ++++++++++++++++ .../models/fastspeech2/fastspeech2_updater.py | 174 +++ .../t2s/models/hifigan/__init__.py | 15 + .../t2s/models/hifigan/hifigan.py | 716 +++++++++++ .../t2s/models/hifigan/hifigan_updater.py | 247 ++++ .../t2s/models/melgan/__init__.py | 17 + .../paddlespeech/t2s/models/melgan/melgan.py | 528 ++++++++ .../melgan/multi_band_melgan_updater.py | 263 ++++ .../t2s/models/melgan/style_melgan.py | 375 ++++++ .../t2s/models/melgan/style_melgan_updater.py | 227 ++++ .../t2s/models/parallel_wavegan/__init__.py | 15 + .../parallel_wavegan/parallel_wavegan.py | 450 +++++++ .../parallel_wavegan_updater.py | 228 ++++ .../t2s/models/speedyspeech/__init__.py | 15 + .../t2s/models/speedyspeech/speedyspeech.py | 254 ++++ .../speedyspeech/speedyspeech_updater.py | 172 +++ .../t2s/models/tacotron2/__init__.py | 15 + .../t2s/models/tacotron2/tacotron2.py | 441 +++++++ .../t2s/models/tacotron2/tacotron2_updater.py | 219 ++++ .../t2s/models/transformer_tts/__init__.py | 15 + .../models/transformer_tts/transformer_tts.py | 674 ++++++++++ .../transformer_tts_updater.py | 333 +++++ ernie-sat/paddlespeech/t2s/models/waveflow.py | 736 +++++++++++ .../t2s/models/wavernn/__init__.py | 15 + .../t2s/models/wavernn/wavernn.py | 582 +++++++++ .../t2s/models/wavernn/wavernn_updater.py | 201 +++ .../paddlespeech/t2s/modules/__init__.py | 17 + .../paddlespeech/t2s/modules/activation.py | 43 + .../paddlespeech/t2s/modules/causal_conv.py | 74 ++ .../t2s/modules/conformer/__init__.py | 13 + .../t2s/modules/conformer/convolution.py | 81 ++ .../t2s/modules/conformer/encoder_layer.py | 182 +++ ernie-sat/paddlespeech/t2s/modules/conv.py | 238 ++++ .../paddlespeech/t2s/modules/geometry.py | 44 + .../paddlespeech/t2s/modules/layer_norm.py | 60 + ernie-sat/paddlespeech/t2s/modules/losses.py | 1008 +++++++++++++++ .../paddlespeech/t2s/modules/masked_fill.py | 49 + .../paddlespeech/t2s/modules/nets_utils.py | 131 ++ .../paddlespeech/t2s/modules/normalizer.py | 33 + .../t2s/modules/positional_encoding.py | 67 + ernie-sat/paddlespeech/t2s/modules/pqmf.py | 127 ++ .../t2s/modules/predictor/__init__.py | 13 + .../modules/predictor/duration_predictor.py | 156 +++ .../t2s/modules/predictor/length_regulator.py | 123 ++ .../modules/predictor/variance_predictor.py | 94 ++ .../t2s/modules/residual_block.py | 179 +++ .../t2s/modules/residual_stack.py | 102 ++ .../paddlespeech/t2s/modules/style_encoder.py | 273 +++++ .../t2s/modules/tacotron2/__init__.py | 13 + .../t2s/modules/tacotron2/attentions.py | 454 +++++++ .../t2s/modules/tacotron2/decoder.py | 686 +++++++++++ .../t2s/modules/tacotron2/encoder.py | 174 +++ .../t2s/modules/tade_res_block.py | 157 +++ .../t2s/modules/transformer/__init__.py | 13 + .../t2s/modules/transformer/attention.py | 222 ++++ .../t2s/modules/transformer/decoder.py | 250 ++++ .../t2s/modules/transformer/decoder_layer.py | 144 +++ .../t2s/modules/transformer/embedding.py | 187 +++ .../t2s/modules/transformer/encoder.py | 646 ++++++++++ .../t2s/modules/transformer/encoder_layer.py | 102 ++ .../t2s/modules/transformer/lightconv.py | 141 +++ .../t2s/modules/transformer/mask.py | 47 + .../modules/transformer/multi_layer_conv.py | 110 ++ .../transformer/positionwise_feed_forward.py | 43 + .../t2s/modules/transformer/repeat.py | 39 + .../t2s/modules/transformer/subsampling.py | 71 ++ .../paddlespeech/t2s/modules/upsample.py | 181 +++ .../paddlespeech/t2s/training/__init__.py | 15 + ernie-sat/paddlespeech/t2s/training/cli.py | 62 + .../t2s/training/default_config.py | 25 + .../paddlespeech/t2s/training/experiment.py | 303 +++++ .../paddlespeech/t2s/training/extension.py | 80 ++ .../t2s/training/extensions/__init__.py | 13 + .../t2s/training/extensions/evaluator.py | 72 ++ .../t2s/training/extensions/snapshot.py | 110 ++ .../t2s/training/extensions/visualizer.py | 39 + .../paddlespeech/t2s/training/optimizer.py | 52 + .../paddlespeech/t2s/training/reporter.py | 159 +++ .../paddlespeech/t2s/training/seeding.py | 26 + .../paddlespeech/t2s/training/trainer.py | 202 +++ .../paddlespeech/t2s/training/trigger.py | 28 + .../t2s/training/triggers/__init__.py | 13 + .../t2s/training/triggers/interval_trigger.py | 39 + .../t2s/training/triggers/limit_trigger.py | 32 + .../t2s/training/triggers/time_trigger.py | 36 + .../paddlespeech/t2s/training/updater.py | 86 ++ .../t2s/training/updaters/__init__.py | 13 + .../t2s/training/updaters/standard_updater.py | 200 +++ ernie-sat/paddlespeech/t2s/utils/__init__.py | 22 + .../paddlespeech/t2s/utils/checkpoint.py | 138 +++ ernie-sat/paddlespeech/t2s/utils/display.py | 110 ++ .../paddlespeech/t2s/utils/error_rate.py | 206 ++++ ernie-sat/paddlespeech/t2s/utils/h5_utils.py | 93 ++ ernie-sat/paddlespeech/t2s/utils/internals.py | 52 + .../paddlespeech/t2s/utils/layer_tools.py | 56 + ernie-sat/paddlespeech/t2s/utils/mp_tools.py | 29 + ernie-sat/paddlespeech/t2s/utils/profile.py | 34 + ernie-sat/paddlespeech/t2s/utils/profiler.py | 110 ++ ernie-sat/paddlespeech/t2s/utils/scheduler.py | 73 ++ ernie-sat/paddlespeech/t2s/utils/timeline.py | 315 +++++ ernie-sat/paddlespeech/text/__init__.py | 13 + ernie-sat/paddlespeech/text/exps/__init__.py | 13 + .../text/exps/ernie_linear/__init__.py | 13 + .../text/exps/ernie_linear/avg_model.py | 112 ++ .../text/exps/ernie_linear/punc_restore.py | 110 ++ .../text/exps/ernie_linear/test.py | 120 ++ .../text/exps/ernie_linear/train.py | 172 +++ .../paddlespeech/text/models/__init__.py | 15 + .../text/models/ernie_crf/__init__.py | 14 + .../text/models/ernie_crf/model.py | 65 + .../text/models/ernie_linear/__init__.py | 16 + .../text/models/ernie_linear/dataset.py | 154 +++ .../text/models/ernie_linear/ernie_linear.py | 65 + .../ernie_linear/ernie_linear_updater.py | 123 ++ ernie-sat/paddlespeech/vector/__init__.py | 13 + .../paddlespeech/vector/cluster/__init__.py | 13 + .../vector/cluster/diarization.py | 1080 ++++++++++++++++ .../paddlespeech/vector/exps/__init__.py | 13 + .../vector/exps/ecapa_tdnn/extract_emb.py | 119 ++ .../vector/exps/ecapa_tdnn/test.py | 203 +++ .../vector/exps/ecapa_tdnn/train.py | 351 ++++++ .../paddlespeech/vector/exps/ge2e/__init__.py | 13 + .../vector/exps/ge2e/audio_processor.py | 246 ++++ .../paddlespeech/vector/exps/ge2e/config.py | 61 + .../vector/exps/ge2e/dataset_processors.py | 173 +++ .../vector/exps/ge2e/inference.py | 140 +++ .../vector/exps/ge2e/preprocess.py | 103 ++ .../vector/exps/ge2e/random_cycle.py | 38 + .../exps/ge2e/speaker_verification_dataset.py | 125 ++ .../paddlespeech/vector/exps/ge2e/train.py | 123 ++ ernie-sat/paddlespeech/vector/io/__init__.py | 13 + ernie-sat/paddlespeech/vector/io/augment.py | 906 ++++++++++++++ ernie-sat/paddlespeech/vector/io/batch.py | 166 +++ .../vector/io/signal_processing.py | 217 ++++ .../paddlespeech/vector/models/__init__.py | 13 + .../paddlespeech/vector/models/ecapa_tdnn.py | 520 ++++++++ .../vector/models/lstm_speaker_encoder.py | 147 +++ .../paddlespeech/vector/modules/__init__.py | 13 + ernie-sat/paddlespeech/vector/modules/loss.py | 93 ++ .../paddlespeech/vector/modules/sid_model.py | 87 ++ .../paddlespeech/vector/training/__init__.py | 13 + .../paddlespeech/vector/training/scheduler.py | 45 + .../paddlespeech/vector/training/seeding.py | 28 + .../paddlespeech/vector/utils/__init__.py | 13 + ernie-sat/paddlespeech/vector/utils/time.py | 66 + ernie-sat/phn_mapping.txt | 306 +++++ ernie-sat/prompt/dev/mfa_end | 3 + ernie-sat/prompt/dev/mfa_start | 3 + ernie-sat/prompt/dev/mfa_text | 3 + ernie-sat/prompt/dev/mfa_wav.scp | 3 + ernie-sat/prompt/dev/text | 3 + ernie-sat/prompt/dev/wav.scp | 3 + ernie-sat/prompt_wav/SSB03420111.wav | Bin 0 -> 194966 bytes ernie-sat/prompt_wav/SSB03540015.wav | Bin 0 -> 245240 bytes ernie-sat/prompt_wav/SSB03540307.wav | Bin 0 -> 246500 bytes ernie-sat/prompt_wav/SSB03540428.wav | Bin 0 -> 189672 bytes ernie-sat/prompt_wav/p243_313.wav | Bin 0 -> 332106 bytes ernie-sat/prompt_wav/p299_096.wav | Bin 0 -> 258576 bytes ernie-sat/prompt_wav/p323_083.wav | Bin 0 -> 286832 bytes .../this_was_not_the_show_for_me.wav | Bin 0 -> 63404 bytes ernie-sat/read_text.py | 78 ++ ernie-sat/run_clone_en_to_zh.sh | 21 + ernie-sat/run_gen_en.sh | 40 + ernie-sat/run_sedit_en.sh | 19 + ernie-sat/sedit_arg_parser.py | 93 ++ ernie-sat/sedit_inference_0520.py | 1086 +++++++++++++++++ ernie-sat/tmp/tmp_pkl.Prompt_003_new | Bin 0 -> 66623 bytes ernie-sat/tmp/tmp_pkl.p243_new | Bin 0 -> 113242 bytes ernie-sat/tmp/tmp_pkl.p299_096 | Bin 0 -> 108852 bytes ernie-sat/util.py | 239 ++++ ernie-sat/wavs/ori.wav | Bin 0 -> 166076 bytes ernie-sat/wavs/pred.wav | Bin 0 -> 189910 bytes ernie-sat/wavs/pred_en_edit_paddle_voc.wav | Bin 0 -> 198476 bytes ernie-sat/wavs/pred_zh.wav | Bin 0 -> 113204 bytes ernie-sat/wavs/pred_zh_fst2_voc.wav | Bin 0 -> 122204 bytes ernie-sat/wavs/task_cross_lingual_pred.wav | Bin 0 -> 117404 bytes ernie-sat/wavs/task_edit_pred.wav | Bin 0 -> 188276 bytes ernie-sat/wavs/task_synthesize_pred.wav | Bin 0 -> 177910 bytes 555 files changed, 78857 insertions(+) create mode 100644 ernie-sat/.DS_Store create mode 100644 ernie-sat/README_zh.md create mode 100644 ernie-sat/model_paddle.py create mode 100644 ernie-sat/paddlespeech/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/README.md create mode 100644 ernie-sat/paddlespeech/cli/README_cn.md create mode 100644 ernie-sat/paddlespeech/cli/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/asr/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/asr/infer.py create mode 100644 ernie-sat/paddlespeech/cli/base_commands.py create mode 100644 ernie-sat/paddlespeech/cli/cls/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/cls/infer.py create mode 100644 ernie-sat/paddlespeech/cli/download.py create mode 100644 ernie-sat/paddlespeech/cli/entry.py create mode 100644 ernie-sat/paddlespeech/cli/executor.py create mode 100644 ernie-sat/paddlespeech/cli/log.py create mode 100644 ernie-sat/paddlespeech/cli/st/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/st/infer.py create mode 100644 ernie-sat/paddlespeech/cli/stats/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/stats/infer.py create mode 100644 ernie-sat/paddlespeech/cli/text/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/text/infer.py create mode 100644 ernie-sat/paddlespeech/cli/tts/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/tts/infer.py create mode 100644 ernie-sat/paddlespeech/cli/utils.py create mode 100644 ernie-sat/paddlespeech/cli/vector/__init__.py create mode 100644 ernie-sat/paddlespeech/cli/vector/infer.py create mode 100644 ernie-sat/paddlespeech/cls/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/exps/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/deploy/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/deploy/predict.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/export_model.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/predict.py create mode 100644 ernie-sat/paddlespeech/cls/exps/panns/train.py create mode 100644 ernie-sat/paddlespeech/cls/models/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/models/panns/__init__.py create mode 100644 ernie-sat/paddlespeech/cls/models/panns/classifier.py create mode 100644 ernie-sat/paddlespeech/cls/models/panns/panns.py create mode 100644 ernie-sat/paddlespeech/s2t/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/README.md create mode 100644 ernie-sat/paddlespeech/s2t/decoders/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/beam_search/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/beam_search/batch_beam_search.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/beam_search/beam_search.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/decoders_deprecated.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/scorer_deprecated.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/swig_wrapper.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/tests/test_decoders.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/recog.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/recog_bin.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/ctc.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/ctc_prefix_score.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/length_bonus.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/ngram.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/scorers/scorer_interface.py create mode 100644 ernie-sat/paddlespeech/s2t/decoders/utils.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/client.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/record.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/runtime.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/send.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/server.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/export.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_export.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_wav.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/train.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/deepspeech2/model.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/lm/transformer/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/cacu_perplexity.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/lm/transformer/lm_cacu_perplexity.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/alignment.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/export.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/test.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/test_wav.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/bin/train.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/model.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2/trainer.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/recog.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/test.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/train.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_kaldi/model.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/bin/export.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/bin/test.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/bin/train.py create mode 100644 ernie-sat/paddlespeech/s2t/exps/u2_st/model.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/audio.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/augmentation.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/base.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/impulse_response.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/noise_perturb.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/online_bayesian_normalization.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/resample.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/shift_perturb.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/spec_augment.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/speed_perturb.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/augmentor/volume_perturb.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/featurizer/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/featurizer/audio_featurizer.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/featurizer/speech_featurizer.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/featurizer/text_featurizer.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/normalizer.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/speech.py create mode 100644 ernie-sat/paddlespeech/s2t/frontend/utility.py create mode 100644 ernie-sat/paddlespeech/s2t/io/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/io/batchfy.py create mode 100644 ernie-sat/paddlespeech/s2t/io/collator.py create mode 100644 ernie-sat/paddlespeech/s2t/io/converter.py create mode 100644 ernie-sat/paddlespeech/s2t/io/dataloader.py create mode 100644 ernie-sat/paddlespeech/s2t/io/dataset.py create mode 100644 ernie-sat/paddlespeech/s2t/io/reader.py create mode 100644 ernie-sat/paddlespeech/s2t/io/sampler.py create mode 100644 ernie-sat/paddlespeech/s2t/io/utility.py create mode 100644 ernie-sat/paddlespeech/s2t/models/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/asr_interface.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2/conv.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2/deepspeech2.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2/rnn.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2_online/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2_online/conv.py create mode 100644 ernie-sat/paddlespeech/s2t/models/ds2_online/deepspeech2.py create mode 100644 ernie-sat/paddlespeech/s2t/models/lm/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/lm/dataset.py create mode 100644 ernie-sat/paddlespeech/s2t/models/lm/transformer.py create mode 100644 ernie-sat/paddlespeech/s2t/models/lm_interface.py create mode 100644 ernie-sat/paddlespeech/s2t/models/st_interface.py create mode 100644 ernie-sat/paddlespeech/s2t/models/u2/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/u2/u2.py create mode 100644 ernie-sat/paddlespeech/s2t/models/u2/updater.py create mode 100644 ernie-sat/paddlespeech/s2t/models/u2_st/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/models/u2_st/u2_st.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/activation.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/align.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/attention.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/cmvn.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/conformer_convolution.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/crf.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/ctc.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/decoder.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/decoder_layer.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/embedding.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/encoder.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/encoder_layer.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/initializer.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/loss.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/mask.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/positionwise_feed_forward.py create mode 100644 ernie-sat/paddlespeech/s2t/modules/subsampling.py create mode 100644 ernie-sat/paddlespeech/s2t/training/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/training/cli.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/evaluator.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/extension.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/plot.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/snapshot.py create mode 100644 ernie-sat/paddlespeech/s2t/training/extensions/visualizer.py create mode 100644 ernie-sat/paddlespeech/s2t/training/gradclip.py create mode 100644 ernie-sat/paddlespeech/s2t/training/optimizer.py create mode 100644 ernie-sat/paddlespeech/s2t/training/reporter.py create mode 100644 ernie-sat/paddlespeech/s2t/training/scheduler.py create mode 100644 ernie-sat/paddlespeech/s2t/training/timer.py create mode 100644 ernie-sat/paddlespeech/s2t/training/trainer.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/compare_value_trigger.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/interval_trigger.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/limit_trigger.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/time_trigger.py create mode 100644 ernie-sat/paddlespeech/s2t/training/triggers/utils.py create mode 100644 ernie-sat/paddlespeech/s2t/training/updaters/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/training/updaters/standard_updater.py create mode 100644 ernie-sat/paddlespeech/s2t/training/updaters/trainer.py create mode 100644 ernie-sat/paddlespeech/s2t/training/updaters/updater.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/add_deltas.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/channel_selector.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/cmvn.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/functional.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/perturb.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/spec_augment.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/spectrogram.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/transform_interface.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/transformation.py create mode 100644 ernie-sat/paddlespeech/s2t/transform/wpe.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/__init__.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/asr_utils.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/bleu_score.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/check_kwargs.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/checkpoint.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/cli_readers.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/cli_utils.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/cli_writers.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/ctc_utils.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/dynamic_import.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/dynamic_pip_install.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/error_rate.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/layer_tools.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/log.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/mp_tools.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/profiler.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/socket_server.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/spec_augment.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/tensor_utils.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/text_grid.py create mode 100644 ernie-sat/paddlespeech/s2t/utils/utility.py create mode 100644 ernie-sat/paddlespeech/server/README.md create mode 100644 ernie-sat/paddlespeech/server/README_cn.md create mode 100644 ernie-sat/paddlespeech/server/__init__.py create mode 100644 ernie-sat/paddlespeech/server/base_commands.py create mode 100644 ernie-sat/paddlespeech/server/bin/__init__.py create mode 100644 ernie-sat/paddlespeech/server/bin/main.py create mode 100644 ernie-sat/paddlespeech/server/bin/paddlespeech_client.py create mode 100644 ernie-sat/paddlespeech/server/bin/paddlespeech_server.py create mode 100644 ernie-sat/paddlespeech/server/conf/application.yaml create mode 100644 ernie-sat/paddlespeech/server/conf/ws_application.yaml create mode 100644 ernie-sat/paddlespeech/server/download.py create mode 100644 ernie-sat/paddlespeech/server/engine/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/online/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/online/asr_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/paddleinference/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/paddleinference/asr_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/python/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/asr/python/asr_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/base_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/cls/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/cls/paddleinference/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/cls/paddleinference/cls_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/cls/python/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/cls/python/cls_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/engine_factory.py create mode 100644 ernie-sat/paddlespeech/server/engine/engine_pool.py create mode 100644 ernie-sat/paddlespeech/server/engine/tts/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/tts/paddleinference/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/tts/paddleinference/tts_engine.py create mode 100644 ernie-sat/paddlespeech/server/engine/tts/python/__init__.py create mode 100644 ernie-sat/paddlespeech/server/engine/tts/python/tts_engine.py create mode 100644 ernie-sat/paddlespeech/server/entry.py create mode 100644 ernie-sat/paddlespeech/server/executor.py create mode 100644 ernie-sat/paddlespeech/server/restful/__init__.py create mode 100644 ernie-sat/paddlespeech/server/restful/api.py create mode 100644 ernie-sat/paddlespeech/server/restful/asr_api.py create mode 100644 ernie-sat/paddlespeech/server/restful/cls_api.py create mode 100644 ernie-sat/paddlespeech/server/restful/request.py create mode 100644 ernie-sat/paddlespeech/server/restful/response.py create mode 100644 ernie-sat/paddlespeech/server/restful/tts_api.py create mode 100644 ernie-sat/paddlespeech/server/tests/asr/http_client.py create mode 100644 ernie-sat/paddlespeech/server/tests/asr/online/microphone_client.py create mode 100644 ernie-sat/paddlespeech/server/tests/asr/online/websocket_client.py create mode 100644 ernie-sat/paddlespeech/server/tests/tts/test_client.py create mode 100644 ernie-sat/paddlespeech/server/util.py create mode 100644 ernie-sat/paddlespeech/server/utils/__init__.py create mode 100644 ernie-sat/paddlespeech/server/utils/audio_process.py create mode 100644 ernie-sat/paddlespeech/server/utils/buffer.py create mode 100644 ernie-sat/paddlespeech/server/utils/config.py create mode 100644 ernie-sat/paddlespeech/server/utils/errors.py create mode 100644 ernie-sat/paddlespeech/server/utils/exception.py create mode 100644 ernie-sat/paddlespeech/server/utils/log.py create mode 100644 ernie-sat/paddlespeech/server/utils/paddle_predictor.py create mode 100644 ernie-sat/paddlespeech/server/utils/util.py create mode 100644 ernie-sat/paddlespeech/server/utils/vad.py create mode 100644 ernie-sat/paddlespeech/server/ws/__init__.py create mode 100644 ernie-sat/paddlespeech/server/ws/api.py create mode 100644 ernie-sat/paddlespeech/server/ws/asr_socket.py create mode 100644 ernie-sat/paddlespeech/t2s/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/audio/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/audio/audio.py create mode 100644 ernie-sat/paddlespeech/t2s/audio/codec.py create mode 100644 ernie-sat/paddlespeech/t2s/audio/spec_normalizer.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/am_batch_fn.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/batch.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/data_table.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/dataset.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/get_feats.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/ljspeech.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/preprocess_utils.py create mode 100644 ernie-sat/paddlespeech/t2s/datasets/vocoder_batch_fn.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/csmsc_test.txt create mode 100644 ernie-sat/paddlespeech/t2s/exps/fastspeech2/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/fastspeech2/gen_gta_mel.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/fastspeech2/normalize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/fastspeech2/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/fastspeech2/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/README.md create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/normalize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/synthesize_from_wav.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize_fxr.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/inference.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/ort_predict.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/ort_predict_e2e.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/sentences.txt create mode 100644 ernie-sat/paddlespeech/t2s/exps/sentences_en.txt create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/gen_gta_mel.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/inference.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/normalize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/synthesize_e2e.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/speedyspeech/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/syn_utils.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/synthesize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/synthesize_e2e.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/synthesize_streaming.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/tacotron2/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/tacotron2/normalize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/tacotron2/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/tacotron2/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/normalize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize_e2e.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/transformer_tts/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/voice_cloning.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/config.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/ljspeech.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/preprocess.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/synthesize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/waveflow/train.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/wavernn/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/wavernn/synthesize.py create mode 100644 ernie-sat/paddlespeech/t2s/exps/wavernn/train.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/arpabet.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/generate_lexicon.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/abbrrviation.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/acronyms.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/normalizer.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/numbers.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/normalizer/width.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/phonectic.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/punctuation.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/tone_sandhi.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/vocab.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_frontend.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/README.md create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/char_convert.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/chronology.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/constants.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/num.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/phonecode.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/quantifier.py create mode 100644 ernie-sat/paddlespeech/t2s/frontend/zh_normalization/text_normlization.py create mode 100644 ernie-sat/paddlespeech/t2s/models/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/fastspeech2/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2.py create mode 100644 ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/hifigan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/hifigan/hifigan.py create mode 100644 ernie-sat/paddlespeech/t2s/models/hifigan/hifigan_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/melgan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/melgan/melgan.py create mode 100644 ernie-sat/paddlespeech/t2s/models/melgan/multi_band_melgan_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/melgan/style_melgan.py create mode 100644 ernie-sat/paddlespeech/t2s/models/melgan/style_melgan_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/parallel_wavegan/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan.py create mode 100644 ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/speedyspeech/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech.py create mode 100644 ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/tacotron2/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2.py create mode 100644 ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/transformer_tts/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts.py create mode 100644 ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/models/waveflow.py create mode 100644 ernie-sat/paddlespeech/t2s/models/wavernn/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/models/wavernn/wavernn.py create mode 100644 ernie-sat/paddlespeech/t2s/models/wavernn/wavernn_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/activation.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/causal_conv.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/conformer/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/conformer/convolution.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/conformer/encoder_layer.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/conv.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/geometry.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/layer_norm.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/losses.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/masked_fill.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/nets_utils.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/normalizer.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/positional_encoding.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/pqmf.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/predictor/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/predictor/duration_predictor.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/predictor/length_regulator.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/predictor/variance_predictor.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/residual_block.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/residual_stack.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/style_encoder.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/tacotron2/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/tacotron2/attentions.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/tacotron2/decoder.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/tacotron2/encoder.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/tade_res_block.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/attention.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/decoder.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/decoder_layer.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/embedding.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/encoder.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/encoder_layer.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/lightconv.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/mask.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/multi_layer_conv.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/positionwise_feed_forward.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/repeat.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/transformer/subsampling.py create mode 100644 ernie-sat/paddlespeech/t2s/modules/upsample.py create mode 100644 ernie-sat/paddlespeech/t2s/training/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/training/cli.py create mode 100644 ernie-sat/paddlespeech/t2s/training/default_config.py create mode 100644 ernie-sat/paddlespeech/t2s/training/experiment.py create mode 100644 ernie-sat/paddlespeech/t2s/training/extension.py create mode 100644 ernie-sat/paddlespeech/t2s/training/extensions/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/training/extensions/evaluator.py create mode 100644 ernie-sat/paddlespeech/t2s/training/extensions/snapshot.py create mode 100644 ernie-sat/paddlespeech/t2s/training/extensions/visualizer.py create mode 100644 ernie-sat/paddlespeech/t2s/training/optimizer.py create mode 100644 ernie-sat/paddlespeech/t2s/training/reporter.py create mode 100644 ernie-sat/paddlespeech/t2s/training/seeding.py create mode 100644 ernie-sat/paddlespeech/t2s/training/trainer.py create mode 100644 ernie-sat/paddlespeech/t2s/training/trigger.py create mode 100644 ernie-sat/paddlespeech/t2s/training/triggers/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/training/triggers/interval_trigger.py create mode 100644 ernie-sat/paddlespeech/t2s/training/triggers/limit_trigger.py create mode 100644 ernie-sat/paddlespeech/t2s/training/triggers/time_trigger.py create mode 100644 ernie-sat/paddlespeech/t2s/training/updater.py create mode 100644 ernie-sat/paddlespeech/t2s/training/updaters/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/training/updaters/standard_updater.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/__init__.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/checkpoint.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/display.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/error_rate.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/h5_utils.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/internals.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/layer_tools.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/mp_tools.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/profile.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/profiler.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/scheduler.py create mode 100644 ernie-sat/paddlespeech/t2s/utils/timeline.py create mode 100644 ernie-sat/paddlespeech/text/__init__.py create mode 100644 ernie-sat/paddlespeech/text/exps/__init__.py create mode 100644 ernie-sat/paddlespeech/text/exps/ernie_linear/__init__.py create mode 100644 ernie-sat/paddlespeech/text/exps/ernie_linear/avg_model.py create mode 100644 ernie-sat/paddlespeech/text/exps/ernie_linear/punc_restore.py create mode 100644 ernie-sat/paddlespeech/text/exps/ernie_linear/test.py create mode 100644 ernie-sat/paddlespeech/text/exps/ernie_linear/train.py create mode 100644 ernie-sat/paddlespeech/text/models/__init__.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_crf/__init__.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_crf/model.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_linear/__init__.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_linear/dataset.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear.py create mode 100644 ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear_updater.py create mode 100644 ernie-sat/paddlespeech/vector/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/cluster/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/cluster/diarization.py create mode 100644 ernie-sat/paddlespeech/vector/exps/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/extract_emb.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/test.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/train.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/audio_processor.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/config.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/dataset_processors.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/inference.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/preprocess.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/random_cycle.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/speaker_verification_dataset.py create mode 100644 ernie-sat/paddlespeech/vector/exps/ge2e/train.py create mode 100644 ernie-sat/paddlespeech/vector/io/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/io/augment.py create mode 100644 ernie-sat/paddlespeech/vector/io/batch.py create mode 100644 ernie-sat/paddlespeech/vector/io/signal_processing.py create mode 100644 ernie-sat/paddlespeech/vector/models/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/models/ecapa_tdnn.py create mode 100644 ernie-sat/paddlespeech/vector/models/lstm_speaker_encoder.py create mode 100644 ernie-sat/paddlespeech/vector/modules/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/modules/loss.py create mode 100644 ernie-sat/paddlespeech/vector/modules/sid_model.py create mode 100644 ernie-sat/paddlespeech/vector/training/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/training/scheduler.py create mode 100644 ernie-sat/paddlespeech/vector/training/seeding.py create mode 100644 ernie-sat/paddlespeech/vector/utils/__init__.py create mode 100644 ernie-sat/paddlespeech/vector/utils/time.py create mode 100644 ernie-sat/phn_mapping.txt create mode 100644 ernie-sat/prompt/dev/mfa_end create mode 100644 ernie-sat/prompt/dev/mfa_start create mode 100644 ernie-sat/prompt/dev/mfa_text create mode 100644 ernie-sat/prompt/dev/mfa_wav.scp create mode 100644 ernie-sat/prompt/dev/text create mode 100644 ernie-sat/prompt/dev/wav.scp create mode 100755 ernie-sat/prompt_wav/SSB03420111.wav create mode 100755 ernie-sat/prompt_wav/SSB03540015.wav create mode 100755 ernie-sat/prompt_wav/SSB03540307.wav create mode 100755 ernie-sat/prompt_wav/SSB03540428.wav create mode 100644 ernie-sat/prompt_wav/p243_313.wav create mode 100644 ernie-sat/prompt_wav/p299_096.wav create mode 100644 ernie-sat/prompt_wav/p323_083.wav create mode 100644 ernie-sat/prompt_wav/this_was_not_the_show_for_me.wav create mode 100644 ernie-sat/read_text.py create mode 100644 ernie-sat/run_clone_en_to_zh.sh create mode 100644 ernie-sat/run_gen_en.sh create mode 100644 ernie-sat/run_sedit_en.sh create mode 100644 ernie-sat/sedit_arg_parser.py create mode 100644 ernie-sat/sedit_inference_0520.py create mode 100644 ernie-sat/tmp/tmp_pkl.Prompt_003_new create mode 100644 ernie-sat/tmp/tmp_pkl.p243_new create mode 100644 ernie-sat/tmp/tmp_pkl.p299_096 create mode 100644 ernie-sat/util.py create mode 100644 ernie-sat/wavs/ori.wav create mode 100644 ernie-sat/wavs/pred.wav create mode 100644 ernie-sat/wavs/pred_en_edit_paddle_voc.wav create mode 100644 ernie-sat/wavs/pred_zh.wav create mode 100644 ernie-sat/wavs/pred_zh_fst2_voc.wav create mode 100644 ernie-sat/wavs/task_cross_lingual_pred.wav create mode 100644 ernie-sat/wavs/task_edit_pred.wav create mode 100644 ernie-sat/wavs/task_synthesize_pred.wav diff --git a/ernie-sat/.DS_Store b/ernie-sat/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d786441fb926dbb0a14d94f455c5814ada534d7f GIT binary patch literal 8196 zcmeHM&2G~`5S~p#>xPy@{HVPk`NFM=(jSUAAcfE#dO$)1!2wX})Fz4K+EMH@v>{dQ zJOLbd1)hTkKs*a4_-1z-Y}cq#4+z9=DFz{b8 z!0!he7t6YqQzhlA1C>kxAd7e`3+|BzNQ|vzUCXJG5(=9tdk`8bbc!Jq9Q`({L)NvN zDyiTk6r6+}S?CN!$kBnbr8gxI!m`D?_tO0Zt4KabnQNuZLC#W5)EB;tcw0 z23>*v0OQ1{9f|a?0=A`BlE%|R;6Ej1eIdR*iu!uhU)p`Y7sa*OH&K`?UYTF8tddn) zF1>Dc!%;JK;$hTq1~0kw#EH9Yeg7;B{n@?Ke%|v&t;)?uVGujLz#B+#&-I}4>P0Vb z!%-s~25uznG*yRXEn17M%IbK$wY9lsZ*1S2tl8uH8?`liYjb-tS+rK}tUuV>KRiAe zznh$XkPacC-;r=^x_m_Q!i66-2Z0|3>@K!5FNqF8Y(MK1`i%Z0simYYoH2rHj?#`0 zw=YQdQvyFNGU-k-xgrCqpnhaN+==mf!3#VpQqKY4i(pKfzGd=VDM|7BC7`Oxja|*M{`Ts7y4CWl*5+oXk zVc;KUKoobHI}I%2?C-|xdN|j%aqZ*c!g^CBB?OgB$021p4mtaWA?|IcGN!KOR7pHR QF$V#X22&XZ{wM=K0dI{>yZ`_I literal 0 HcmV?d00001 diff --git a/ernie-sat/README_zh.md b/ernie-sat/README_zh.md new file mode 100644 index 0000000..930f6cb --- /dev/null +++ b/ernie-sat/README_zh.md @@ -0,0 +1,87 @@ + +## 使用说明 + +### 1.安装飞桨 + +我们的代码基于 Paddle(version>=2.0) + + +### 2.预训练模型 +预训练模型ERNIE-SAT的模型如下所示(链接暂无): +- [ERNIE-SAT_ZH](http://bj.bcebos.com/wenxin-models/model-ernie-sat-base-zh.tar.gz) +- [ERNIE-SAT_EN](http://bj.bcebos.com/wenxin-models/model-ernie-sat-base-en.tar.gz) +- [ERNIE-SAT_ZH_and_EN](http://bj.bcebos.com/wenxin-models/model-ernie-sat-base-en_zh.tar.gz) + + +### 3.下载 + +1. 我们使用parallel wavegan作为声码器(vocoder): + - [pwg_aishell3_ckpt_0.5.zip](https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_aishell3_ckpt_0.5.zip) + +创建download文件夹,下载上述预训练的声码器(vocoder)模型并将其解压 + +```bash +mkdir download +cd download +unzip pwg_aishell3_ckpt_0.5.zip +``` + + 2. 我们使用[FastSpeech2](https://arxiv.org/abs/2006.04558) 作为音素(phoneme)的持续时间预测器: + - [fastspeech2_conformer_baker_ckpt_0.5.zip](https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_conformer_baker_ckpt_0.5.zip) 中文场景下使用 + - [fastspeech2_nosil_ljspeech_ckpt_0.5.zip](https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_ljspeech_ckpt_0.5.zip) 英文场景下使用 + + 下载上述预训练的fastspeech2模型并将其解压 + +```bash +cd download +unzip fastspeech2_conformer_baker_ckpt_0.5.zip +unzip fastspeech2_nosil_ljspeech_ckpt_0.5.zip +``` + +### 4.推理 + +我们目前只开源了语音编辑、个性化语音合成、跨语言语音合成的推理代码,后续会逐步开源。 +注:当前采用的声码器版本与模型训练时版本(https://github.com/kan-bayashi/ParallelWaveGAN)在英文上存在差异,您可使用模型训练时版本作为您的声码器,模型将在后续更新中升级。 + +我们提供特定音频文件, 以及其对应的文本、音素相关文件: +- prompt_wav: 提供的音频文件 +- prompt/dev: 基于上述特定音频对应的文本、音素相关文件 + + +```text +prompt_wav +├── p299_096.wav # 样例语音文件1 +├── SSB03540428.wav # 样例语音文件2 +└── ... +``` + +```text +prompt/dev +├── text # 样例语音对应文本 +├── wav.scp # 样例语音路径 +├── mfa_text # 样例语音对应音素 +├── mfa_start # 样例语音中各个音素的开始时间 +└── mfa_end # 样例语音中各个音素的结束时间 +``` +1. `--am` 声学模型格式符合 {model_name}_{dataset} +2. `--am_config`, `--am_checkpoint`, `--am_stat` 和 `--phones_dict` 是声学模型的参数,对应于 fastspeech2 预训练模型中的 4 个文件。 +3. `--voc` 声码器(vocoder)格式是否符合 {model_name}_{dataset} +4. `--voc_config`, `--voc_checkpoint`, `--voc_stat` 是声码器的参数,对应于 parallel wavegan 预训练模型中的 3 个文件。 +5. `--lang` 对应模型的语言可以是 `zh` 或 `en` 。 +6. `--ngpu` 要使用的GPU数,如果 ngpu==0,则使用 cpu。 +7. ` --model_name` 模型名称 +8. ` --uid` 特定提示(prompt)语音的id +9. ` --new_str` 输入的文本(本次开源暂时先设置特定的文本) +10. ` --prefix` 特定音频对应的文本、音素相关文件的地址 +11. ` --source_language` , 源语言 +12. ` --target_language` , 目标语言 +13. ` --output_name` , 合成语音名称 +14. ` --task_name` , 任务名称, 包括:语音编辑任务、个性化语音合成任务、跨语言语音合成任务 + +运行以下脚本即可进行实验 +```shell +sh run_sedit_en.sh # 语音编辑任务(英文) +sh run_gen_en.sh # 个性化语音合成任务(英文) +sh run_clone_en_to_zh.sh # 跨语言语音合成任务(英文到中文的克隆) +``` + diff --git a/ernie-sat/model_paddle.py b/ernie-sat/model_paddle.py new file mode 100644 index 0000000..fc3caf9 --- /dev/null +++ b/ernie-sat/model_paddle.py @@ -0,0 +1,1057 @@ +import argparse +from pathlib import Path +from typing import Any, Callable, Dict, Optional +from typing import List +from typing import Sequence +from typing import Tuple +from typing import Union +import humanfriendly +from matplotlib.collections import Collection +from matplotlib.pyplot import axis +import librosa +import soundfile as sf + +import numpy as np +import paddle +import paddle.nn.functional as F +from paddle import nn +from typeguard import check_argument_types +import logging +import math +import yaml +from abc import ABC, abstractmethod +import warnings +from paddle.amp import auto_cast + +import sys, os +pypath = '..' +for dir_name in os.listdir(pypath): + dir_path = os.path.join(pypath, dir_name) + if os.path.isdir(dir_path): + sys.path.append(dir_path) + +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.nets_utils import make_non_pad_mask +from paddlespeech.t2s.modules.nets_utils import make_pad_mask +from paddlespeech.t2s.modules.predictor.duration_predictor import DurationPredictor +from paddlespeech.t2s.modules.predictor.duration_predictor import DurationPredictorLoss +from paddlespeech.t2s.modules.predictor.length_regulator import LengthRegulator +from paddlespeech.t2s.modules.predictor.variance_predictor import VariancePredictor +from paddlespeech.t2s.modules.tacotron2.decoder import Postnet +from paddlespeech.t2s.modules.transformer.encoder import CNNDecoder +from paddlespeech.t2s.modules.transformer.encoder import CNNPostnet +from paddlespeech.t2s.modules.transformer.encoder import ConformerEncoder +from paddlespeech.t2s.modules.transformer.encoder import TransformerEncoder +from paddlespeech.t2s.modules.transformer.embedding import PositionalEncoding, ScaledPositionalEncoding, RelPositionalEncoding +from paddlespeech.t2s.modules.transformer.subsampling import Conv2dSubsampling +from paddlespeech.t2s.modules.masked_fill import masked_fill +from paddlespeech.t2s.modules.transformer.attention import MultiHeadedAttention, RelPositionMultiHeadedAttention +from paddlespeech.t2s.modules.transformer.positionwise_feed_forward import PositionwiseFeedForward +from paddlespeech.t2s.modules.transformer.multi_layer_conv import Conv1dLinear, MultiLayeredConv1d +from paddlespeech.t2s.modules.conformer.convolution import ConvolutionModule +from paddlespeech.t2s.modules.transformer.repeat import repeat +from paddlespeech.t2s.modules.conformer.encoder_layer import EncoderLayer +from paddlespeech.t2s.modules.layer_norm import LayerNorm +from paddlespeech.s2t.utils.error_rate import ErrorCalculator +from paddlespeech.t2s.datasets.get_feats import LogMelFBank + +class Swish(nn.Layer): + """Construct an Swish object.""" + + def forward(self, x): + """Return Swich activation function.""" + return x * F.sigmoid(x) + + +def get_activation(act): + """Return activation function.""" + + activation_funcs = { + "hardtanh": nn.Hardtanh, + "tanh": nn.Tanh, + "relu": nn.ReLU, + "selu": nn.SELU, + "swish": Swish, + } + + return activation_funcs[act]() + +class LegacyRelPositionalEncoding(PositionalEncoding): + """Relative positional encoding module (old version). + + Details can be found in https://github.com/espnet/espnet/pull/2816. + + See : Appendix B in https://arxiv.org/abs/1901.02860 + + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int): Maximum input length. + + """ + def __init__(self, d_model: int, dropout_rate: float, max_len: int=5000): + """ + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int, optional): [Maximum input length.]. Defaults to 5000. + """ + super().__init__(d_model, dropout_rate, max_len, reverse=True) + + def extend_pe(self, x): + """Reset the positional encodings.""" + if self.pe is not None: + if paddle.shape(self.pe)[1] >= paddle.shape(x)[1]: + # if self.pe.dtype != x.dtype or self.pe.device != x.device: + # self.pe = self.pe.to(dtype=x.dtype, device=x.device) + return + pe = paddle.zeros((paddle.shape(x)[1], self.d_model)) + if self.reverse: + position = paddle.arange( + paddle.shape(x)[1] - 1, -1, -1.0, dtype=paddle.float32 + ).unsqueeze(1) + else: + position = paddle.arange(0, paddle.shape(x)[1], dtype=paddle.float32).unsqueeze(1) + div_term = paddle.exp( + paddle.arange(0, self.d_model, 2, dtype=paddle.float32) + * -(math.log(10000.0) / self.d_model) + ) + pe[:, 0::2] = paddle.sin(position * div_term) + pe[:, 1::2] = paddle.cos(position * div_term) + pe = pe.unsqueeze(0) + self.pe = pe + + def forward(self, x: paddle.Tensor) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Compute positional encoding. + Args: + x (paddle.Tensor): Input tensor (batch, time, `*`). + Returns: + paddle.Tensor: Encoded tensor (batch, time, `*`). + paddle.Tensor: Positional embedding tensor (1, time, `*`). + """ + self.extend_pe(x) + x = x * self.xscale + pos_emb = self.pe[:, :paddle.shape(x)[1]] + return self.dropout(x), self.dropout(pos_emb) + +def dump_tensor(var, do_trans = False): + wf = open('/mnt/home/xiaoran/PaddleSpeech-develop/tmp_var.out', 'w') + for num in var.shape: + wf.write(str(num) + ' ') + wf.write('\n') + if do_trans: + var = paddle.transpose(var, [1,0]) + if len(var.shape)==1: + for _var in var: + s = ("%.10f"%_var.item()) + wf.write(s+' ') + elif len(var.shape)==2: + for __var in var: + for _var in __var: + s = ("%.10f"%_var.item()) + wf.write(s+' ') + wf.write('\n') + elif len(var.shape)==3: + for ___var in var: + for __var in ___var: + for _var in __var: + s = ("%.10f"%_var.item()) + wf.write(s+' ') + wf.write('\n') + wf.write('\n') + elif len(var.shape)==4: + for ____var in var: + for ___var in ____var: + for __var in ___var: + for _var in __var: + s = ("%.10f"%_var.item()) + wf.write(s+' ') + wf.write('\n') + wf.write('\n') + wf.write('\n') + +class mySequential(nn.Sequential): + def forward(self, *inputs): + for module in self._sub_layers.values(): + if type(inputs) == tuple: + inputs = module(*inputs) + else: + inputs = module(inputs) + return inputs + +class NewMaskInputLayer(nn.Layer): + __constants__ = ['out_features'] + out_features: int + + def __init__(self, out_features: int, + device=None, dtype=None) -> None: + factory_kwargs = {'device': device, 'dtype': dtype} + super(NewMaskInputLayer, self).__init__() + self.mask_feature = paddle.create_parameter( + shape=(1,1,out_features), + dtype=paddle.float32, + default_initializer=paddle.nn.initializer.Assign(paddle.normal(shape=(1,1,out_features)))) + + def forward(self, input: paddle.Tensor, masked_position=None) -> paddle.Tensor: + masked_position = paddle.expand_as(paddle.unsqueeze(masked_position, -1), input) + masked_input = masked_fill(input, masked_position, 0) + masked_fill(paddle.expand_as(self.mask_feature, input), ~masked_position, 0) + return masked_input + +class LegacyRelPositionMultiHeadedAttention(MultiHeadedAttention): + """Multi-Head Attention layer with relative position encoding (old version). + Details can be found in https://github.com/espnet/espnet/pull/2816. + Paper: https://arxiv.org/abs/1901.02860 + + Args: + n_head (int): The number of heads. + n_feat (int): The number of features. + dropout_rate (float): Dropout rate. + zero_triu (bool): Whether to zero the upper triangular part of attention matrix. + """ + + def __init__(self, n_head, n_feat, dropout_rate, zero_triu=False): + """Construct an RelPositionMultiHeadedAttention object.""" + super().__init__(n_head, n_feat, dropout_rate) + self.zero_triu = zero_triu + # linear transformation for positional encoding + self.linear_pos = nn.Linear(n_feat, n_feat, bias_attr=False) + # these two learnable bias are used in matrix c and matrix d + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + + self.pos_bias_u = paddle.create_parameter( + shape=(self.h, self.d_k), + dtype='float32', + default_initializer=paddle.nn.initializer.XavierUniform()) + self.pos_bias_v = paddle.create_parameter( + shape=(self.h, self.d_k), + dtype='float32', + default_initializer=paddle.nn.initializer.XavierUniform()) + + def rel_shift(self, x): + """Compute relative positional encoding. + Args: + x(Tensor): Input tensor (batch, head, time1, time2). + + Returns: + Tensor:Output tensor. + """ + b, h, t1, t2 = paddle.shape(x) + zero_pad = paddle.zeros((b, h, t1, 1)) + x_padded = paddle.concat([zero_pad, x], axis=-1) + x_padded = paddle.reshape(x_padded, [b, h, t2 + 1, t1]) + # only keep the positions from 0 to time2 + x = paddle.reshape(x_padded[:, :, 1:], [b, h, t1, t2]) + + if self.zero_triu: + ones = paddle.ones((t1, t2)) + x = x * paddle.tril(ones, t2 - 1)[None, None, :, :] + + return x + + def forward(self, query, key, value, pos_emb, mask): + """Compute 'Scaled Dot Product Attention' with rel. positional encoding. + + Args: + query(Tensor): Query tensor (#batch, time1, size). + key(Tensor): Key tensor (#batch, time2, size). + value(Tensor): Value tensor (#batch, time2, size). + pos_emb(Tensor): Positional embedding tensor (#batch, time1, size). + mask(Tensor): Mask tensor (#batch, 1, time2) or (#batch, time1, time2). + + Returns: + Tensor: Output tensor (#batch, time1, d_model). + """ + q, k, v = self.forward_qkv(query, key, value) + # (batch, time1, head, d_k) + q = paddle.transpose(q, [0, 2, 1, 3]) + + n_batch_pos = paddle.shape(pos_emb)[0] + p = paddle.reshape(self.linear_pos(pos_emb), [n_batch_pos, -1, self.h, self.d_k]) + # (batch, head, time1, d_k) + p = paddle.transpose(p, [0, 2, 1, 3]) + # (batch, head, time1, d_k) + q_with_bias_u = paddle.transpose((q + self.pos_bias_u), [0, 2, 1, 3]) + # (batch, head, time1, d_k) + q_with_bias_v = paddle.transpose((q + self.pos_bias_v), [0, 2, 1, 3]) + + # compute attention score + # first compute matrix a and matrix c + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + # (batch, head, time1, time2) + matrix_ac = paddle.matmul(q_with_bias_u, paddle.transpose(k, [0, 1, 3, 2])) + + # compute matrix b and matrix d + # (batch, head, time1, time1) + matrix_bd = paddle.matmul(q_with_bias_v, paddle.transpose(p, [0, 1, 3, 2])) + matrix_bd = self.rel_shift(matrix_bd) + # (batch, head, time1, time2) + scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k) + + return self.forward_attention(v, scores, mask) + +class MLMEncoder(nn.Layer): + """Conformer encoder module. + + Args: + idim (int): Input dimension. + attention_dim (int): Dimension of attention. + attention_heads (int): The number of heads of multi head attention. + linear_units (int): The number of units of position-wise feed forward. + num_blocks (int): The number of decoder blocks. + dropout_rate (float): Dropout rate. + positional_dropout_rate (float): Dropout rate after adding positional encoding. + attention_dropout_rate (float): Dropout rate in attention. + input_layer (Union[str, paddle.nn.Layer]): Input layer type. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + positionwise_layer_type (str): "linear", "conv1d", or "conv1d-linear". + positionwise_conv_kernel_size (int): Kernel size of positionwise conv1d layer. + macaron_style (bool): Whether to use macaron style for positionwise layer. + pos_enc_layer_type (str): Encoder positional encoding layer type. + selfattention_layer_type (str): Encoder attention layer type. + activation_type (str): Encoder activation function type. + use_cnn_module (bool): Whether to use convolution module. + zero_triu (bool): Whether to zero the upper triangular part of attention matrix. + cnn_module_kernel (int): Kernerl size of convolution module. + padding_idx (int): Padding idx for input_layer=embed. + stochastic_depth_rate (float): Maximum probability to skip the encoder layer. + intermediate_layers (Union[List[int], None]): indices of intermediate CTC layer. + indices start from 1. + if not None, intermediate outputs are returned (which changes return type + signature.) + + """ + def __init__( + self, + idim, + vocab_size=0, + pre_speech_layer: int = 0, + attention_dim=256, + attention_heads=4, + linear_units=2048, + num_blocks=6, + dropout_rate=0.1, + positional_dropout_rate=0.1, + attention_dropout_rate=0.0, + input_layer="conv2d", + normalize_before=True, + concat_after=False, + positionwise_layer_type="linear", + positionwise_conv_kernel_size=1, + macaron_style=False, + pos_enc_layer_type="abs_pos", + pos_enc_class=None, + selfattention_layer_type="selfattn", + activation_type="swish", + use_cnn_module=False, + zero_triu=False, + cnn_module_kernel=31, + padding_idx=-1, + stochastic_depth_rate=0.0, + intermediate_layers=None, + text_masking = False + ): + """Construct an Encoder object.""" + super(MLMEncoder, self).__init__() + self._output_size = attention_dim + self.text_masking=text_masking + if self.text_masking: + self.text_masking_layer = NewMaskInputLayer(attention_dim) + activation = get_activation(activation_type) + if pos_enc_layer_type == "abs_pos": + pos_enc_class = PositionalEncoding + elif pos_enc_layer_type == "scaled_abs_pos": + pos_enc_class = ScaledPositionalEncoding + elif pos_enc_layer_type == "rel_pos": + assert selfattention_layer_type == "rel_selfattn" + pos_enc_class = RelPositionalEncoding + elif pos_enc_layer_type == "legacy_rel_pos": + pos_enc_class = LegacyRelPositionalEncoding + assert selfattention_layer_type == "legacy_rel_selfattn" + else: + raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type) + + self.conv_subsampling_factor = 1 + if input_layer == "linear": + self.embed = nn.Sequential( + nn.Linear(idim, attention_dim), + nn.LayerNorm(attention_dim), + nn.Dropout(dropout_rate), + nn.ReLU(), + pos_enc_class(attention_dim, positional_dropout_rate), + ) + elif input_layer == "conv2d": + self.embed = Conv2dSubsampling( + idim, + attention_dim, + dropout_rate, + pos_enc_class(attention_dim, positional_dropout_rate), + ) + self.conv_subsampling_factor = 4 + elif input_layer == "embed": + self.embed = nn.Sequential( + nn.Embedding(idim, attention_dim, padding_idx=padding_idx), + pos_enc_class(attention_dim, positional_dropout_rate), + ) + elif input_layer == "mlm": + self.segment_emb = None + self.speech_embed = mySequential( + NewMaskInputLayer(idim), + nn.Linear(idim, attention_dim), + nn.LayerNorm(attention_dim), + nn.ReLU(), + pos_enc_class(attention_dim, positional_dropout_rate) + ) + self.text_embed = nn.Sequential( + nn.Embedding(vocab_size, attention_dim, padding_idx=padding_idx), + pos_enc_class(attention_dim, positional_dropout_rate), + ) + elif input_layer=="sega_mlm": + self.segment_emb = nn.Embedding(500, attention_dim, padding_idx=padding_idx) + self.speech_embed = mySequential( + NewMaskInputLayer(idim), + nn.Linear(idim, attention_dim), + nn.LayerNorm(attention_dim), + nn.ReLU(), + pos_enc_class(attention_dim, positional_dropout_rate) + ) + self.text_embed = nn.Sequential( + nn.Embedding(vocab_size, attention_dim, padding_idx=padding_idx), + pos_enc_class(attention_dim, positional_dropout_rate), + ) + elif isinstance(input_layer, nn.Layer): + self.embed = nn.Sequential( + input_layer, + pos_enc_class(attention_dim, positional_dropout_rate), + ) + elif input_layer is None: + self.embed = nn.Sequential( + pos_enc_class(attention_dim, positional_dropout_rate) + ) + else: + raise ValueError("unknown input_layer: " + input_layer) + self.normalize_before = normalize_before + + # self-attention module definition + if selfattention_layer_type == "selfattn": + logging.info("encoder self-attention layer type = self-attention") + encoder_selfattn_layer = MultiHeadedAttention + encoder_selfattn_layer_args = ( + attention_heads, + attention_dim, + attention_dropout_rate, + ) + elif selfattention_layer_type == "legacy_rel_selfattn": + assert pos_enc_layer_type == "legacy_rel_pos" + encoder_selfattn_layer = LegacyRelPositionMultiHeadedAttention + encoder_selfattn_layer_args = ( + attention_heads, + attention_dim, + attention_dropout_rate, + ) + elif selfattention_layer_type == "rel_selfattn": + logging.info("encoder self-attention layer type = relative self-attention") + assert pos_enc_layer_type == "rel_pos" + encoder_selfattn_layer = RelPositionMultiHeadedAttention + encoder_selfattn_layer_args = ( + attention_heads, + attention_dim, + attention_dropout_rate, + zero_triu, + ) + else: + raise ValueError("unknown encoder_attn_layer: " + selfattention_layer_type) + + # feed-forward module definition + if positionwise_layer_type == "linear": + positionwise_layer = PositionwiseFeedForward + positionwise_layer_args = ( + attention_dim, + linear_units, + dropout_rate, + activation, + ) + elif positionwise_layer_type == "conv1d": + positionwise_layer = MultiLayeredConv1d + positionwise_layer_args = ( + attention_dim, + linear_units, + positionwise_conv_kernel_size, + dropout_rate, + ) + elif positionwise_layer_type == "conv1d-linear": + positionwise_layer = Conv1dLinear + positionwise_layer_args = ( + attention_dim, + linear_units, + positionwise_conv_kernel_size, + dropout_rate, + ) + else: + raise NotImplementedError("Support only linear or conv1d.") + + # convolution module definition + convolution_layer = ConvolutionModule + convolution_layer_args = (attention_dim, cnn_module_kernel, activation) + + self.encoders = repeat( + num_blocks, + lambda lnum: EncoderLayer( + attention_dim, + encoder_selfattn_layer(*encoder_selfattn_layer_args), + positionwise_layer(*positionwise_layer_args), + positionwise_layer(*positionwise_layer_args) if macaron_style else None, + convolution_layer(*convolution_layer_args) if use_cnn_module else None, + dropout_rate, + normalize_before, + concat_after, + stochastic_depth_rate * float(1 + lnum) / num_blocks, + ), + ) + self.pre_speech_layer = pre_speech_layer + self.pre_speech_encoders = repeat( + self.pre_speech_layer, + lambda lnum: EncoderLayer( + attention_dim, + encoder_selfattn_layer(*encoder_selfattn_layer_args), + positionwise_layer(*positionwise_layer_args), + positionwise_layer(*positionwise_layer_args) if macaron_style else None, + convolution_layer(*convolution_layer_args) if use_cnn_module else None, + dropout_rate, + normalize_before, + concat_after, + stochastic_depth_rate * float(1 + lnum) / self.pre_speech_layer, + ), + ) + if self.normalize_before: + self.after_norm = LayerNorm(attention_dim) + + self.intermediate_layers = intermediate_layers + + + def forward(self, speech_pad, text_pad, masked_position, speech_mask=None, text_mask=None,speech_segment_pos=None, text_segment_pos=None): + """Encode input sequence. + + """ + if masked_position is not None: + speech_pad = self.speech_embed(speech_pad, masked_position) + else: + speech_pad = self.speech_embed(speech_pad) + # pure speech input + if -2 in np.array(text_pad): + text_pad = text_pad+3 + text_mask = paddle.unsqueeze(bool(text_pad), 1) + text_segment_pos = paddle.zeros_like(text_pad) + text_pad = self.text_embed(text_pad) + text_pad = (text_pad[0] + self.segment_emb(text_segment_pos), text_pad[1]) + text_segment_pos=None + elif text_pad is not None: + text_pad = self.text_embed(text_pad) + segment_emb = None + if speech_segment_pos is not None and text_segment_pos is not None and self.segment_emb: + speech_segment_emb = self.segment_emb(speech_segment_pos) + text_segment_emb = self.segment_emb(text_segment_pos) + text_pad = (text_pad[0] + text_segment_emb, text_pad[1]) + speech_pad = (speech_pad[0] + speech_segment_emb, speech_pad[1]) + segment_emb = paddle.concat([speech_segment_emb, text_segment_emb],axis=1) + if self.pre_speech_encoders: + speech_pad, _ = self.pre_speech_encoders(speech_pad, speech_mask) + + if text_pad is not None: + xs = paddle.concat([speech_pad[0], text_pad[0]], axis=1) + xs_pos_emb = paddle.concat([speech_pad[1], text_pad[1]], axis=1) + masks = paddle.concat([speech_mask,text_mask],axis=-1) + else: + xs = speech_pad[0] + xs_pos_emb = speech_pad[1] + masks = speech_mask + + xs, masks = self.encoders((xs,xs_pos_emb), masks) + + if isinstance(xs, tuple): + xs = xs[0] + if self.normalize_before: + xs = self.after_norm(xs) + + return xs, masks #, segment_emb + + +class MLMDecoder(MLMEncoder): + + def forward(self, xs, masks, masked_position=None,segment_emb=None): + """Encode input sequence. + + Args: + xs (paddle.Tensor): Input tensor (#batch, time, idim). + masks (paddle.Tensor): Mask tensor (#batch, time). + + Returns: + paddle.Tensor: Output tensor (#batch, time, attention_dim). + paddle.Tensor: Mask tensor (#batch, time). + + """ + emb, mlm_position = None, None + if not self.training: + masked_position = None + # if isinstance(self.embed, (Conv2dSubsampling, VGG2L)): + # xs, masks = self.embed(xs, masks) + # else: + xs = self.embed(xs) + if segment_emb: + xs = (xs[0] + segment_emb, xs[1]) + if self.intermediate_layers is None: + xs, masks = self.encoders(xs, masks) + else: + intermediate_outputs = [] + for layer_idx, encoder_layer in enumerate(self.encoders): + xs, masks = encoder_layer(xs, masks) + + if ( + self.intermediate_layers is not None + and layer_idx + 1 in self.intermediate_layers + ): + encoder_output = xs + # intermediate branches also require normalization. + if self.normalize_before: + encoder_output = self.after_norm(encoder_output) + intermediate_outputs.append(encoder_output) + if isinstance(xs, tuple): + xs = xs[0] + if self.normalize_before: + xs = self.after_norm(xs) + + if self.intermediate_layers is not None: + return xs, masks, intermediate_outputs + return xs, masks + +class AbsESPnetModel(nn.Layer, ABC): + """The common abstract class among each tasks + + "ESPnetModel" is referred to a class which inherits paddle.nn.Layer, + and makes the dnn-models forward as its member field, + a.k.a delegate pattern, + and defines "loss", "stats", and "weight" for the task. + + If you intend to implement new task in ESPNet, + the model must inherit this class. + In other words, the "mediator" objects between + our training system and the your task class are + just only these three values, loss, stats, and weight. + + Example: + >>> from espnet2.tasks.abs_task import AbsTask + >>> class YourESPnetModel(AbsESPnetModel): + ... def forward(self, input, input_lengths): + ... ... + ... return loss, stats, weight + >>> class YourTask(AbsTask): + ... @classmethod + ... def build_model(cls, args: argparse.Namespace) -> YourESPnetModel: + """ + + @abstractmethod + def forward( + self, **batch: paddle.Tensor + ) -> Tuple[paddle.Tensor, Dict[str, paddle.Tensor], paddle.Tensor]: + raise NotImplementedError + + @abstractmethod + def collect_feats(self, **batch: paddle.Tensor) -> Dict[str, paddle.Tensor]: + raise NotImplementedError + +class AbsFeatsExtract(nn.Layer, ABC): + @abstractmethod + def output_size(self) -> int: + raise NotImplementedError + + @abstractmethod + def get_parameters(self) -> Dict[str, Any]: + raise NotImplementedError + + @abstractmethod + def forward( + self, input: paddle.Tensor, input_lengths: paddle.Tensor + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + raise NotImplementedError + +class AbsNormalize(nn.Layer, ABC): + @abstractmethod + def forward( + self, input: paddle.Tensor, input_lengths: paddle.Tensor = None + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + # return output, output_lengths + raise NotImplementedError + + + +def pad_to_longformer_att_window(text, max_len, max_tlen,attention_window): + round = max_len % attention_window + if round != 0: + max_tlen += (attention_window - round) + n_batch = paddle.shape(text)[0] + text_pad = paddle.zeros(shape = (n_batch, max_tlen, *paddle.shape(text[0])[1:]), dtype=text.dtype) + for i in range(n_batch): + text_pad[i, : paddle.shape(text[i])[0]] = text[i] + else: + text_pad = text[:, : max_tlen] + return text_pad, max_tlen + +class ESPnetMLMModel(AbsESPnetModel): + def __init__( + self, + token_list: Union[Tuple[str, ...], List[str]], + odim: int, + feats_extract: Optional[AbsFeatsExtract], + normalize: Optional[AbsNormalize], + encoder: nn.Layer, + decoder: Optional[nn.Layer], + postnet_layers: int = 0, + postnet_chans: int = 0, + postnet_filts: int = 0, + ignore_id: int = -1, + lsm_weight: float = 0.0, + length_normalized_loss: bool = False, + report_cer: bool = True, + report_wer: bool = True, + sym_space: str = "", + sym_blank: str = "", + masking_schema: str = "span", + mean_phn_span: int = 3, + mlm_prob: float = 0.25, + dynamic_mlm_prob = False, + decoder_seg_pos=False, + text_masking=False + ): + + super().__init__() + # note that eos is the same as sos (equivalent ID) + self.odim = odim + self.ignore_id = ignore_id + self.token_list = token_list.copy() + + self.normalize = normalize + self.encoder = encoder + + self.decoder = decoder + self.vocab_size = encoder.text_embed[0]._num_embeddings + if report_cer or report_wer: + self.error_calculator = ErrorCalculator( + token_list, sym_space, sym_blank, report_cer, report_wer + ) + else: + self.error_calculator = None + + self.feats_extract = feats_extract + self.mlm_weight = 1.0 + self.mlm_prob = mlm_prob + self.mlm_layer = 12 + self.finetune_wo_mlm =True + self.max_span = 50 + self.min_span = 4 + self.mean_phn_span = mean_phn_span + self.masking_schema = masking_schema + if self.decoder is None or not (hasattr(self.decoder, 'output_layer') and self.decoder.output_layer is not None): + self.sfc = nn.Linear(self.encoder._output_size, odim) + else: + self.sfc=None + if text_masking: + self.text_sfc = nn.Linear(self.encoder.text_embed[0]._embedding_dim, self.vocab_size, weight_attr = self.encoder.text_embed[0]._weight_attr) + self.text_mlm_loss = nn.CrossEntropyLoss(ignore_index=ignore_id) + else: + self.text_sfc = None + self.text_mlm_loss = None + self.decoder_seg_pos = decoder_seg_pos + if lsm_weight > 50: + self.l1_loss_func = nn.MSELoss(reduce=False) + else: + self.l1_loss_func = nn.L1Loss(reduction='none') + self.postnet = ( + None + if postnet_layers == 0 + else Postnet( + idim=self.encoder._output_size, + odim=odim, + n_layers=postnet_layers, + n_chans=postnet_chans, + n_filts=postnet_filts, + use_batch_norm=True, + dropout_rate=0.5, + ) + ) + + def collect_feats(self, + speech, speech_lengths, text, text_lengths, masked_position, speech_mask, text_mask, speech_segment_pos, text_segment_pos, y_masks=None + ) -> Dict[str, paddle.Tensor]: + return {"feats": speech, "feats_lengths": speech_lengths} + + def _forward(self, batch, speech_segment_pos,y_masks=None): + # feats: (Batch, Length, Dim) + # -> encoder_out: (Batch, Length2, Dim2) + speech_pad_placeholder = batch['speech_pad'] + if self.decoder is not None: + ys_in = self._add_first_frame_and_remove_last_frame(batch['speech_pad']) + encoder_out, h_masks = self.encoder(**batch) + if self.decoder is not None: + zs, _ = self.decoder(ys_in, y_masks, encoder_out, bool(h_masks), self.encoder.segment_emb(speech_segment_pos)) + speech_hidden_states = zs + else: + speech_hidden_states = encoder_out[:,:paddle.shape(batch['speech_pad'])[1], :] + if self.sfc is not None: + before_outs = paddle.reshape(self.sfc(speech_hidden_states), (paddle.shape(speech_hidden_states)[0], -1, self.odim)) + else: + before_outs = speech_hidden_states + if self.postnet is not None: + after_outs = before_outs + paddle.transpose(self.postnet( + paddle.transpose(before_outs, [0, 2, 1]) + ), (0, 2, 1)) + else: + after_outs = None + return before_outs, after_outs, speech_pad_placeholder, batch['masked_position'] + + + + + def inference( + self, + speech, text, masked_position, speech_mask, text_mask, speech_segment_pos, text_segment_pos, + span_boundary, + y_masks=None, + speech_lengths=None, text_lengths=None, + feats: Optional[paddle.Tensor] = None, + spembs: Optional[paddle.Tensor] = None, + sids: Optional[paddle.Tensor] = None, + lids: Optional[paddle.Tensor] = None, + threshold: float = 0.5, + minlenratio: float = 0.0, + maxlenratio: float = 10.0, + use_teacher_forcing: bool = False, + ) -> Dict[str, paddle.Tensor]: + + + batch = dict( + speech_pad=speech, + text_pad=text, + masked_position=masked_position, + speech_mask=speech_mask, + text_mask=text_mask, + speech_segment_pos=speech_segment_pos, + text_segment_pos=text_segment_pos, + ) + + + # # inference with teacher forcing + # hs, h_masks = self.encoder(**batch) + + outs = [batch['speech_pad'][:,:span_boundary[0]]] + z_cache = None + if use_teacher_forcing: + before,zs, _, _ = self._forward( + batch, speech_segment_pos, y_masks=y_masks) + if zs is None: + zs = before + outs+=[zs[0][span_boundary[0]:span_boundary[1]]] + outs+=[batch['speech_pad'][:,span_boundary[1]:]] + return dict(feat_gen=outs) + + # concatenate attention weights -> (#layers, #heads, T_feats, T_text) + att_ws = paddle.stack(att_ws, axis=0) + outs += [batch['speech_pad'][:,span_boundary[1]:]] + return dict(feat_gen=outs, att_w=att_ws) + + + def _add_first_frame_and_remove_last_frame(self, ys: paddle.Tensor) -> paddle.Tensor: + ys_in = paddle.concat( + [paddle.zeros(shape = (paddle.shape(ys)[0], 1, paddle.shape(ys)[2]), dtype = ys.dtype), ys[:, :-1]], axis=1 + ) + return ys_in + + +class ESPnetMLMEncAsDecoderModel(ESPnetMLMModel): + + def _forward(self, batch, speech_segment_pos, y_masks=None): + # feats: (Batch, Length, Dim) + # -> encoder_out: (Batch, Length2, Dim2) + speech_pad_placeholder = batch['speech_pad'] + encoder_out, h_masks = self.encoder(**batch) # segment_emb + if self.decoder is not None: + zs, _ = self.decoder(encoder_out, h_masks) + else: + zs = encoder_out + speech_hidden_states = zs[:,:paddle.shape(batch['speech_pad'])[1], :] + if self.sfc is not None: + before_outs = paddle.reshape(self.sfc(speech_hidden_states), (paddle.shape(speech_hidden_states)[0], -1, self.odim)) + else: + before_outs = speech_hidden_states + if self.postnet is not None: + after_outs = before_outs + paddle.transpose(self.postnet( + paddle.transpose(before_outs, [0, 2, 1]) + ), [0, 2, 1]) + else: + after_outs = None + return before_outs, after_outs, speech_pad_placeholder, batch['masked_position'] + +class ESPnetMLMDualMaksingModel(ESPnetMLMModel): + + def _calc_mlm_loss( + self, + before_outs: paddle.Tensor, + after_outs: paddle.Tensor, + text_outs: paddle.Tensor, + batch + ): + xs_pad = batch['speech_pad'] + text_pad = batch['text_pad'] + masked_position = batch['masked_position'] + text_masked_position = batch['text_masked_position'] + mlm_loss_position = masked_position>0 + loss = paddle.sum(self.l1_loss_func(paddle.reshape(before_outs, (-1, self.odim)), + paddle.reshape(xs_pad, (-1, self.odim))), axis=-1) + if after_outs is not None: + loss += paddle.sum(self.l1_loss_func(paddle.reshape(after_outs, (-1, self.odim)), + paddle.reshape(xs_pad, (-1, self.odim))), axis=-1) + loss_mlm = paddle.sum((loss * paddle.reshape(mlm_loss_position, axis=-1).float())) \ + / paddle.sum((mlm_loss_position.float()) + 1e-10) + + loss_text = paddle.sum((self.text_mlm_loss(paddle.reshape(text_outs, (-1,self.vocab_size)), paddle.reshape(text_pad, (-1))) * paddle.reshape(text_masked_position, (-1)).float())) \ + / paddle.sum((text_masked_position.float()) + 1e-10) + return loss_mlm, loss_text + + + def _forward(self, batch, speech_segment_pos, y_masks=None): + # feats: (Batch, Length, Dim) + # -> encoder_out: (Batch, Length2, Dim2) + speech_pad_placeholder = batch['speech_pad'] + encoder_out, h_masks = self.encoder(**batch) # segment_emb + if self.decoder is not None: + zs, _ = self.decoder(encoder_out, h_masks) + else: + zs = encoder_out + speech_hidden_states = zs[:,:paddle.shape(batch['speech_pad'])[1], :] + if self.text_sfc: + text_hiddent_states = zs[:,paddle.shape(batch['speech_pad'])[1]:,:] + text_outs = paddle.reshape(self.text_sfc(text_hiddent_states), (paddle.shape(text_hiddent_states)[0], -1, self.vocab_size)) + if self.sfc is not None: + before_outs = paddle.reshape(self.sfc(speech_hidden_states), + (paddle.shape(speech_hidden_states)[0], -1, self.odim)) + else: + before_outs = speech_hidden_states + if self.postnet is not None: + after_outs = before_outs + paddle.transpose(self.postnet( + paddle.transpose(before_outs, [0,2,1]) + ), [0, 2, 1]) + else: + after_outs = None + return before_outs, after_outs,text_outs, None #, speech_pad_placeholder, batch['masked_position'],batch['text_masked_position'] + +def build_model_from_file(config_file, model_file): + + state_dict = paddle.load(model_file) + model_class = ESPnetMLMDualMaksingModel if 'conformer_combine_vctk_aishell3_dual_masking' in config_file \ + else ESPnetMLMEncAsDecoderModel + + # 构建模型 + args = yaml.safe_load(Path(config_file).open("r", encoding="utf-8")) + args = argparse.Namespace(**args) + + model = build_model(args, model_class) + + model.set_state_dict(state_dict) + return model, args + + +def build_model(args: argparse.Namespace, model_class = ESPnetMLMEncAsDecoderModel) -> ESPnetMLMModel: + if isinstance(args.token_list, str): + with open(args.token_list, encoding="utf-8") as f: + token_list = [line.rstrip() for line in f] + + # Overwriting token_list to keep it as "portable". + args.token_list = list(token_list) + elif isinstance(args.token_list, (tuple, list)): + token_list = list(args.token_list) + else: + raise RuntimeError("token_list must be str or list") + vocab_size = len(token_list) + logging.info(f"Vocabulary size: {vocab_size }") + + odim = 80 + + + # Normalization layer + normalize = None + + pos_enc_class = ScaledPositionalEncoding if args.use_scaled_pos_enc else PositionalEncoding + + if "conformer" == args.encoder: + conformer_self_attn_layer_type = args.encoder_conf['selfattention_layer_type'] + conformer_pos_enc_layer_type = args.encoder_conf['pos_enc_layer_type'] + conformer_rel_pos_type = "legacy" + if conformer_rel_pos_type == "legacy": + if conformer_pos_enc_layer_type == "rel_pos": + conformer_pos_enc_layer_type = "legacy_rel_pos" + logging.warning( + "Fallback to conformer_pos_enc_layer_type = 'legacy_rel_pos' " + "due to the compatibility. If you want to use the new one, " + "please use conformer_pos_enc_layer_type = 'latest'." + ) + if conformer_self_attn_layer_type == "rel_selfattn": + conformer_self_attn_layer_type = "legacy_rel_selfattn" + logging.warning( + "Fallback to " + "conformer_self_attn_layer_type = 'legacy_rel_selfattn' " + "due to the compatibility. If you want to use the new one, " + "please use conformer_pos_enc_layer_type = 'latest'." + ) + elif conformer_rel_pos_type == "latest": + assert conformer_pos_enc_layer_type != "legacy_rel_pos" + assert conformer_self_attn_layer_type != "legacy_rel_selfattn" + else: + raise ValueError(f"Unknown rel_pos_type: {conformer_rel_pos_type}") + args.encoder_conf['selfattention_layer_type'] = conformer_self_attn_layer_type + args.encoder_conf['pos_enc_layer_type'] = conformer_pos_enc_layer_type + if "conformer"==args.decoder: + args.decoder_conf['selfattention_layer_type'] = conformer_self_attn_layer_type + args.decoder_conf['pos_enc_layer_type'] = conformer_pos_enc_layer_type + + + # Encoder + encoder_class = MLMEncoder + + if 'text_masking' in args.model_conf.keys() and args.model_conf['text_masking']: + args.encoder_conf['text_masking'] = True + else: + args.encoder_conf['text_masking'] = False + + encoder = encoder_class(args.input_size,vocab_size=vocab_size, pos_enc_class=pos_enc_class, + **args.encoder_conf) + + # Decoder + if args.decoder != 'no_decoder': + decoder_class = MLMDecoder + decoder = decoder_class( + idim=0, + input_layer=None, + **args.decoder_conf, + ) + else: + decoder = None + + # Build model + model = model_class( + feats_extract=None, # maybe should be LogMelFbank + odim=odim, + normalize=normalize, + encoder=encoder, + decoder=decoder, + token_list=token_list, + **args.model_conf, + ) + + + # Initialize + if args.init is not None: + initialize(model, args.init) + + return model diff --git a/ernie-sat/paddlespeech/__init__.py b/ernie-sat/paddlespeech/__init__.py new file mode 100644 index 0000000..b781c4a --- /dev/null +++ b/ernie-sat/paddlespeech/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import _locale + +_locale._getdefaultlocale = (lambda *args: ['en_US', 'utf8']) diff --git a/ernie-sat/paddlespeech/cli/README.md b/ernie-sat/paddlespeech/cli/README.md new file mode 100644 index 0000000..19c8220 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/README.md @@ -0,0 +1,44 @@ +# PaddleSpeech Command Line + +([简体中文](./README_cn.md)|English) + + The simplest approach to use PaddleSpeech models. + + ## Help + ```bash + paddlespeech help + ``` + ## Audio Classification + ```bash + paddlespeech cls --input input.wav + ``` + + ## Speaker Verification + + ```bash + paddlespeech vector --task spk --input input_16k.wav + ``` + + ## Automatic Speech Recognition + ``` + paddlespeech asr --lang zh --input input_16k.wav + ``` + + ## Speech Translation (English to Chinese) + + (not support for Windows now) + ```bash + paddlespeech st --input input_16k.wav + ``` + + ## Text-to-Speech + ```bash + paddlespeech tts --input "你好,欢迎使用百度飞桨深度学习框架!" --output output.wav + ``` + + ## Text Post-precessing + +- Punctuation Restoration + ```bash + paddlespeech text --task punc --input 今天的天气真不错啊你下午有空吗我想约你一起去吃饭 + ``` diff --git a/ernie-sat/paddlespeech/cli/README_cn.md b/ernie-sat/paddlespeech/cli/README_cn.md new file mode 100644 index 0000000..4b15d6c --- /dev/null +++ b/ernie-sat/paddlespeech/cli/README_cn.md @@ -0,0 +1,45 @@ +# PaddleSpeech 命令行工具 + +(简体中文|[English](./README.md)) + +`paddlespeech.cli` 模块是 PaddleSpeech 的命令行工具,它提供了最简便的方式调用 PaddleSpeech 提供的不同语音应用场景的预训练模型,用一行命令就可以进行模型预测: + + ## 命令行使用帮助 + ```bash + paddlespeech help + ``` + + ## 声音分类 + ```bash + paddlespeech cls --input input.wav + ``` + + ## 声纹识别 + + ```bash + paddlespeech vector --task spk --input input_16k.wav + ``` + + ## 语音识别 + ``` + paddlespeech asr --lang zh --input input_16k.wav + ``` + + ## 语音翻译(英-中) + + (暂不支持Windows系统) + ```bash + paddlespeech st --input input_16k.wav + ``` + + ## 语音合成 + ```bash + paddlespeech tts --input "你好,欢迎使用百度飞桨深度学习框架!" --output output.wav + ``` + + ## 文本后处理 + +- 标点恢复 + ```bash + paddlespeech text --task punc --input 今天的天气真不错啊你下午有空吗我想约你一起去吃饭 + ``` diff --git a/ernie-sat/paddlespeech/cli/__init__.py b/ernie-sat/paddlespeech/cli/__init__.py new file mode 100644 index 0000000..ddf0359 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import _locale + +from .asr import ASRExecutor +from .base_commands import BaseCommand +from .base_commands import HelpCommand +from .cls import CLSExecutor +from .st import STExecutor +from .stats import StatsExecutor +from .text import TextExecutor +from .tts import TTSExecutor +from .vector import VectorExecutor + +_locale._getdefaultlocale = (lambda *args: ['en_US', 'utf8']) diff --git a/ernie-sat/paddlespeech/cli/asr/__init__.py b/ernie-sat/paddlespeech/cli/asr/__init__.py new file mode 100644 index 0000000..8ab0991 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/asr/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import ASRExecutor diff --git a/ernie-sat/paddlespeech/cli/asr/infer.py b/ernie-sat/paddlespeech/cli/asr/infer.py new file mode 100644 index 0000000..b12b9f6 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/asr/infer.py @@ -0,0 +1,544 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import sys +from collections import OrderedDict +from typing import List +from typing import Optional +from typing import Union + +import librosa +import numpy as np +import paddle +import soundfile +from yacs.config import CfgNode + +from ..download import get_path_from_url +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.transform.transformation import Transformation +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.utility import UpdateConfig + +__all__ = ['ASRExecutor'] + +pretrained_models = { + # The tags for pretrained_models should be "{model_name}[_{dataset}][-{lang}][-...]". + # e.g. "conformer_wenetspeech-zh-16k" and "panns_cnn6-32k". + # Command line and python api use "{model_name}[_{dataset}]" as --model, usage: + # "paddlespeech asr --model conformer_wenetspeech --lang zh --sr 16000 --input ./input.wav" + "conformer_wenetspeech-zh-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/wenetspeech/asr1_conformer_wenetspeech_ckpt_0.1.1.model.tar.gz', + 'md5': + '76cb19ed857e6623856b7cd7ebbfeda4', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/conformer/checkpoints/wenetspeech', + }, + "transformer_librispeech-en-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/librispeech/asr1/asr1_transformer_librispeech_ckpt_0.1.1.model.tar.gz', + 'md5': + '2c667da24922aad391eacafe37bc1660', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/transformer/checkpoints/avg_10', + }, + "deepspeech2offline_aishell-zh-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/aishell/asr0/asr0_deepspeech2_aishell_ckpt_0.1.1.model.tar.gz', + 'md5': + '932c3593d62fe5c741b59b31318aa314', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/deepspeech2/checkpoints/avg_1', + 'lm_url': + 'https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm', + 'lm_md5': + '29e02312deb2e59b3c8686c7966d4fe3' + }, + "deepspeech2online_aishell-zh-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/aishell/asr0/asr0_deepspeech2_online_aishell_ckpt_0.2.0.model.tar.gz', + 'md5': + '23e16c69730a1cb5d735c98c83c21e16', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/deepspeech2_online/checkpoints/avg_1', + 'lm_url': + 'https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm', + 'lm_md5': + '29e02312deb2e59b3c8686c7966d4fe3' + }, + "deepspeech2offline_librispeech-en-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/librispeech/asr0/asr0_deepspeech2_librispeech_ckpt_0.1.1.model.tar.gz', + 'md5': + 'f5666c81ad015c8de03aac2bc92e5762', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/deepspeech2/checkpoints/avg_1', + 'lm_url': + 'https://deepspeech.bj.bcebos.com/en_lm/common_crawl_00.prune01111.trie.klm', + 'lm_md5': + '099a601759d467cd0a8523ff939819c5' + }, +} + +model_alias = { + "deepspeech2offline": + "paddlespeech.s2t.models.ds2:DeepSpeech2Model", + "deepspeech2online": + "paddlespeech.s2t.models.ds2_online:DeepSpeech2ModelOnline", + "conformer": + "paddlespeech.s2t.models.u2:U2Model", + "transformer": + "paddlespeech.s2t.models.u2:U2Model", + "wenetspeech": + "paddlespeech.s2t.models.u2:U2Model", +} + + +@cli_register( + name='paddlespeech.asr', description='Speech to text infer command.') +class ASRExecutor(BaseExecutor): + def __init__(self): + super(ASRExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech.asr', add_help=True) + self.parser.add_argument( + '--input', type=str, default=None, help='Audio file to recognize.') + self.parser.add_argument( + '--model', + type=str, + default='conformer_wenetspeech', + choices=[tag[:tag.index('-')] for tag in pretrained_models.keys()], + help='Choose model type of asr task.') + self.parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en, zh:[conformer_wenetspeech-zh-16k], en:[transformer_librispeech-en-16k]' + ) + self.parser.add_argument( + "--sample_rate", + type=int, + default=16000, + choices=[8000, 16000], + help='Choose the audio sample rate of the model. 8000 or 16000') + self.parser.add_argument( + '--config', + type=str, + default=None, + help='Config of asr task. Use deault config when it is None.') + self.parser.add_argument( + '--decode_method', + type=str, + default='attention_rescoring', + choices=[ + 'ctc_greedy_search', 'ctc_prefix_beam_search', 'attention', + 'attention_rescoring' + ], + help='only support transformer and conformer model') + self.parser.add_argument( + '--ckpt_path', + type=str, + default=None, + help='Checkpoint file of model.') + self.parser.add_argument( + '--yes', + '-y', + action="store_true", + default=False, + help='No additional parameters required. Once set this parameter, it means accepting the request of the program by default, which includes transforming the audio sample rate' + ) + self.parser.add_argument( + '--device', + type=str, + default=paddle.get_device(), + help='Choose device to execute model inference.') + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + + return decompressed_path + + def _init_from_path(self, + model_type: str='wenetspeech', + lang: str='zh', + sample_rate: int=16000, + cfg_path: Optional[os.PathLike]=None, + decode_method: str='attention_rescoring', + ckpt_path: Optional[os.PathLike]=None): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'model'): + logger.info('Model had been initialized.') + return + + if cfg_path is None or ckpt_path is None: + sample_rate_str = '16k' if sample_rate == 16000 else '8k' + tag = model_type + '-' + lang + '-' + sample_rate_str + res_path = self._get_pretrained_path(tag) # wenetspeech_zh + self.res_path = res_path + self.cfg_path = os.path.join(res_path, + pretrained_models[tag]['cfg_path']) + self.ckpt_path = os.path.join( + res_path, pretrained_models[tag]['ckpt_path'] + ".pdparams") + logger.info(res_path) + logger.info(self.cfg_path) + logger.info(self.ckpt_path) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.ckpt_path = os.path.abspath(ckpt_path + ".pdparams") + self.res_path = os.path.dirname( + os.path.dirname(os.path.abspath(self.cfg_path))) + + #Init body. + self.config = CfgNode(new_allowed=True) + self.config.merge_from_file(self.cfg_path) + + with UpdateConfig(self.config): + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + from paddlespeech.s2t.io.collator import SpeechCollator + self.vocab = self.config.vocab_filepath + self.config.decode.lang_model_path = os.path.join( + MODEL_HOME, 'language_model', + self.config.decode.lang_model_path) + self.collate_fn_test = SpeechCollator.from_config(self.config) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, vocab=self.vocab) + lm_url = pretrained_models[tag]['lm_url'] + lm_md5 = pretrained_models[tag]['lm_md5'] + self.download_lm( + lm_url, + os.path.dirname(self.config.decode.lang_model_path), lm_md5) + + elif "conformer" in model_type or "transformer" in model_type or "wenetspeech" in model_type: + self.config.spm_model_prefix = os.path.join( + self.res_path, self.config.spm_model_prefix) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, + vocab=self.config.vocab_filepath, + spm_model_prefix=self.config.spm_model_prefix) + self.config.decode.decoding_method = decode_method + + else: + raise Exception("wrong type") + model_name = model_type[:model_type.rindex( + '_')] # model_type: {model_name}_{dataset} + model_class = dynamic_import(model_name, model_alias) + model_conf = self.config + model = model_class.from_config(model_conf) + self.model = model + self.model.eval() + + # load model + model_dict = paddle.load(self.ckpt_path) + self.model.set_state_dict(model_dict) + + def preprocess(self, model_type: str, input: Union[str, os.PathLike]): + """ + Input preprocess and return paddle.Tensor stored in self.input. + Input content can be a text(tts), a file(asr, cls) or a streaming(not supported yet). + """ + + audio_file = input + if isinstance(audio_file, (str, os.PathLike)): + logger.info("Preprocess audio_file:" + audio_file) + + # Get the object for feature extraction + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + audio, _ = self.collate_fn_test.process_utterance( + audio_file=audio_file, transcript=" ") + audio_len = audio.shape[0] + audio = paddle.to_tensor(audio, dtype='float32') + audio_len = paddle.to_tensor(audio_len) + audio = paddle.unsqueeze(audio, axis=0) + # vocab_list = collate_fn_test.vocab_list + self._inputs["audio"] = audio + self._inputs["audio_len"] = audio_len + logger.info(f"audio feat shape: {audio.shape}") + + elif "conformer" in model_type or "transformer" in model_type or "wenetspeech" in model_type: + logger.info("get the preprocess conf") + preprocess_conf = self.config.preprocess_config + preprocess_args = {"train": False} + preprocessing = Transformation(preprocess_conf) + logger.info("read the audio file") + audio, audio_sample_rate = soundfile.read( + audio_file, dtype="int16", always_2d=True) + + if self.change_format: + if audio.shape[1] >= 2: + audio = audio.mean(axis=1, dtype=np.int16) + else: + audio = audio[:, 0] + # pcm16 -> pcm 32 + audio = self._pcm16to32(audio) + audio = librosa.resample( + audio, + orig_sr=audio_sample_rate, + target_sr=self.sample_rate) + audio_sample_rate = self.sample_rate + # pcm32 -> pcm 16 + audio = self._pcm32to16(audio) + else: + audio = audio[:, 0] + + logger.info(f"audio shape: {audio.shape}") + # fbank + audio = preprocessing(audio, **preprocess_args) + + audio_len = paddle.to_tensor(audio.shape[0]) + audio = paddle.to_tensor(audio, dtype='float32').unsqueeze(axis=0) + + self._inputs["audio"] = audio + self._inputs["audio_len"] = audio_len + logger.info(f"audio feat shape: {audio.shape}") + + else: + raise Exception("wrong type") + + @paddle.no_grad() + def infer(self, model_type: str): + """ + Model inference and result stored in self.output. + """ + + cfg = self.config.decode + audio = self._inputs["audio"] + audio_len = self._inputs["audio_len"] + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + decode_batch_size = audio.shape[0] + self.model.decoder.init_decoder( + decode_batch_size, self.text_feature.vocab_list, + cfg.decoding_method, cfg.lang_model_path, cfg.alpha, cfg.beta, + cfg.beam_size, cfg.cutoff_prob, cfg.cutoff_top_n, + cfg.num_proc_bsearch) + + result_transcripts = self.model.decode(audio, audio_len) + self.model.decoder.del_decoder() + self._outputs["result"] = result_transcripts[0] + + elif "conformer" in model_type or "transformer" in model_type: + result_transcripts = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=cfg.decoding_method, + beam_size=cfg.beam_size, + ctc_weight=cfg.ctc_weight, + decoding_chunk_size=cfg.decoding_chunk_size, + num_decoding_left_chunks=cfg.num_decoding_left_chunks, + simulate_streaming=cfg.simulate_streaming) + self._outputs["result"] = result_transcripts[0][0] + else: + raise Exception("invalid model name") + + def postprocess(self) -> Union[str, os.PathLike]: + """ + Output postprocess and return human-readable results such as texts and audio files. + """ + return self._outputs["result"] + + def download_lm(self, url, lm_dir, md5sum): + download_path = get_path_from_url( + url=url, + root_dir=lm_dir, + md5sum=md5sum, + decompress=False, ) + + def _pcm16to32(self, audio): + assert (audio.dtype == np.int16) + audio = audio.astype("float32") + bits = np.iinfo(np.int16).bits + audio = audio / (2**(bits - 1)) + return audio + + def _pcm32to16(self, audio): + assert (audio.dtype == np.float32) + bits = np.iinfo(np.int16).bits + audio = audio * (2**(bits - 1)) + audio = np.round(audio).astype("int16") + return audio + + def _check(self, audio_file: str, sample_rate: int, force_yes: bool): + self.sample_rate = sample_rate + if self.sample_rate != 16000 and self.sample_rate != 8000: + logger.error( + "invalid sample rate, please input --sr 8000 or --sr 16000") + return False + + if isinstance(audio_file, (str, os.PathLike)): + if not os.path.isfile(audio_file): + logger.error("Please input the right audio file path") + return False + + logger.info("checking the audio file format......") + try: + audio, audio_sample_rate = soundfile.read( + audio_file, dtype="int16", always_2d=True) + audio_duration = audio.shape[0] / audio_sample_rate + max_duration = 50.0 + if audio_duration >= max_duration: + logger.error("Please input audio file less then 50 seconds.\n") + return + except Exception as e: + logger.exception(e) + logger.error( + "can not open the audio file, please check the audio file format is 'wav'. \n \ + you can try to use sox to change the file format.\n \ + For example: \n \ + sample rate: 16k \n \ + sox input_audio.xx --rate 16k --bits 16 --channels 1 output_audio.wav \n \ + sample rate: 8k \n \ + sox input_audio.xx --rate 8k --bits 16 --channels 1 output_audio.wav \n \ + ") + return False + logger.info("The sample rate is %d" % audio_sample_rate) + if audio_sample_rate != self.sample_rate: + logger.warning("The sample rate of the input file is not {}.\n \ + The program will resample the wav file to {}.\n \ + If the result does not meet your expectations,\n \ + Please input the 16k 16 bit 1 channel wav file. \ + ".format(self.sample_rate, self.sample_rate)) + if force_yes is False: + while (True): + logger.info( + "Whether to change the sample rate and the channel. Y: change the sample. N: exit the prgream." + ) + content = input("Input(Y/N):") + if content.strip() == "Y" or content.strip( + ) == "y" or content.strip() == "yes" or content.strip( + ) == "Yes": + logger.info( + "change the sampele rate, channel to 16k and 1 channel" + ) + break + elif content.strip() == "N" or content.strip( + ) == "n" or content.strip() == "no" or content.strip( + ) == "No": + logger.info("Exit the program") + exit(1) + else: + logger.warning("Not regular input, please input again") + + self.change_format = True + else: + logger.info("The audio file format is right") + self.change_format = False + + return True + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + + model = parser_args.model + lang = parser_args.lang + sample_rate = parser_args.sample_rate + config = parser_args.config + ckpt_path = parser_args.ckpt_path + decode_method = parser_args.decode_method + force_yes = parser_args.yes + device = parser_args.device + + if not parser_args.verbose: + self.disable_task_loggers() + + task_source = self.get_task_source(parser_args.input) + task_results = OrderedDict() + has_exceptions = False + + for id_, input_ in task_source.items(): + try: + res = self(input_, model, lang, sample_rate, config, ckpt_path, + decode_method, force_yes, device) + task_results[id_] = res + except Exception as e: + has_exceptions = True + task_results[id_] = f'{e.__class__.__name__}: {e}' + + self.process_task_results(parser_args.input, task_results, + parser_args.job_dump_result) + + if has_exceptions: + return False + else: + return True + + @stats_wrapper + def __call__(self, + audio_file: os.PathLike, + model: str='conformer_wenetspeech', + lang: str='zh', + sample_rate: int=16000, + config: os.PathLike=None, + ckpt_path: os.PathLike=None, + decode_method: str='attention_rescoring', + force_yes: bool=False, + device=paddle.get_device()): + """ + Python API to call an executor. + """ + audio_file = os.path.abspath(audio_file) + if not self._check(audio_file, sample_rate, force_yes): + sys.exit(-1) + paddle.set_device(device) + self._init_from_path(model, lang, sample_rate, config, decode_method, + ckpt_path) + self.preprocess(model, audio_file) + self.infer(model) + res = self.postprocess() # Retrieve result of asr. + + return res diff --git a/ernie-sat/paddlespeech/cli/base_commands.py b/ernie-sat/paddlespeech/cli/base_commands.py new file mode 100644 index 0000000..97d5cd7 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/base_commands.py @@ -0,0 +1,49 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from .entry import commands +from .utils import cli_register +from .utils import get_command + +__all__ = [ + 'BaseCommand', + 'HelpCommand', +] + + +@cli_register(name='paddlespeech') +class BaseCommand: + def execute(self, argv: List[str]) -> bool: + help = get_command('paddlespeech.help') + return help().execute(argv) + + +@cli_register(name='paddlespeech.help', description='Show help for commands.') +class HelpCommand: + def execute(self, argv: List[str]) -> bool: + msg = 'Usage:\n' + msg += ' paddlespeech \n\n' + msg += 'Commands:\n' + for command, detail in commands['paddlespeech'].items(): + if command.startswith('_'): + continue + + if '_description' not in detail: + continue + msg += ' {:<15} {}\n'.format(command, + detail['_description']) + + print(msg) + return True diff --git a/ernie-sat/paddlespeech/cli/cls/__init__.py b/ernie-sat/paddlespeech/cli/cls/__init__.py new file mode 100644 index 0000000..13e316f --- /dev/null +++ b/ernie-sat/paddlespeech/cli/cls/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import CLSExecutor diff --git a/ernie-sat/paddlespeech/cli/cls/infer.py b/ernie-sat/paddlespeech/cli/cls/infer.py new file mode 100644 index 0000000..f56d8a5 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/cls/infer.py @@ -0,0 +1,295 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from collections import OrderedDict +from typing import List +from typing import Optional +from typing import Union + +import numpy as np +import paddle +import yaml + +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper +from paddleaudio import load +from paddleaudio.features import LogMelSpectrogram +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +__all__ = ['CLSExecutor'] + +pretrained_models = { + # The tags for pretrained_models should be "{model_name}[_{dataset}][-{lang}][-...]". + # e.g. "conformer_wenetspeech-zh-16k", "transformer_aishell-zh-16k" and "panns_cnn6-32k". + # Command line and python api use "{model_name}[_{dataset}]" as --model, usage: + # "paddlespeech asr --model conformer_wenetspeech --lang zh --sr 16000 --input ./input.wav" + "panns_cnn6-32k": { + 'url': 'https://paddlespeech.bj.bcebos.com/cls/panns_cnn6.tar.gz', + 'md5': '4cf09194a95df024fd12f84712cf0f9c', + 'cfg_path': 'panns.yaml', + 'ckpt_path': 'cnn6.pdparams', + 'label_file': 'audioset_labels.txt', + }, + "panns_cnn10-32k": { + 'url': 'https://paddlespeech.bj.bcebos.com/cls/panns_cnn10.tar.gz', + 'md5': 'cb8427b22176cc2116367d14847f5413', + 'cfg_path': 'panns.yaml', + 'ckpt_path': 'cnn10.pdparams', + 'label_file': 'audioset_labels.txt', + }, + "panns_cnn14-32k": { + 'url': 'https://paddlespeech.bj.bcebos.com/cls/panns_cnn14.tar.gz', + 'md5': 'e3b9b5614a1595001161d0ab95edee97', + 'cfg_path': 'panns.yaml', + 'ckpt_path': 'cnn14.pdparams', + 'label_file': 'audioset_labels.txt', + }, +} + +model_alias = { + "panns_cnn6": "paddlespeech.cls.models.panns:CNN6", + "panns_cnn10": "paddlespeech.cls.models.panns:CNN10", + "panns_cnn14": "paddlespeech.cls.models.panns:CNN14", +} + + +@cli_register( + name='paddlespeech.cls', description='Audio classification infer command.') +class CLSExecutor(BaseExecutor): + def __init__(self): + super(CLSExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech.cls', add_help=True) + self.parser.add_argument( + '--input', type=str, default=None, help='Audio file to classify.') + self.parser.add_argument( + '--model', + type=str, + default='panns_cnn14', + choices=[tag[:tag.index('-')] for tag in pretrained_models.keys()], + help='Choose model type of cls task.') + self.parser.add_argument( + '--config', + type=str, + default=None, + help='Config of cls task. Use deault config when it is None.') + self.parser.add_argument( + '--ckpt_path', + type=str, + default=None, + help='Checkpoint file of model.') + self.parser.add_argument( + '--label_file', + type=str, + default=None, + help='Label file of cls task.') + self.parser.add_argument( + '--topk', + type=int, + default=1, + help='Return topk scores of classification result.') + self.parser.add_argument( + '--device', + type=str, + default=paddle.get_device(), + help='Choose device to execute model inference.') + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + + return decompressed_path + + def _init_from_path(self, + model_type: str='panns_cnn14', + cfg_path: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None, + label_file: Optional[os.PathLike]=None): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'model'): + logger.info('Model had been initialized.') + return + + if label_file is None or ckpt_path is None: + tag = model_type + '-' + '32k' # panns_cnn14-32k + self.res_path = self._get_pretrained_path(tag) + self.cfg_path = os.path.join(self.res_path, + pretrained_models[tag]['cfg_path']) + self.label_file = os.path.join(self.res_path, + pretrained_models[tag]['label_file']) + self.ckpt_path = os.path.join(self.res_path, + pretrained_models[tag]['ckpt_path']) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.label_file = os.path.abspath(label_file) + self.ckpt_path = os.path.abspath(ckpt_path) + + # config + with open(self.cfg_path, 'r') as f: + self._conf = yaml.safe_load(f) + + # labels + self._label_list = [] + with open(self.label_file, 'r') as f: + for line in f: + self._label_list.append(line.strip()) + + # model + model_class = dynamic_import(model_type, model_alias) + model_dict = paddle.load(self.ckpt_path) + self.model = model_class(extract_embedding=False) + self.model.set_state_dict(model_dict) + self.model.eval() + + def preprocess(self, audio_file: Union[str, os.PathLike]): + """ + Input preprocess and return paddle.Tensor stored in self.input. + Input content can be a text(tts), a file(asr, cls) or a streaming(not supported yet). + """ + feat_conf = self._conf['feature'] + logger.info(feat_conf) + waveform, _ = load( + file=audio_file, + sr=feat_conf['sample_rate'], + mono=True, + dtype='float32') + if isinstance(audio_file, (str, os.PathLike)): + logger.info("Preprocessing audio_file:" + audio_file) + + # Feature extraction + feature_extractor = LogMelSpectrogram( + sr=feat_conf['sample_rate'], + n_fft=feat_conf['n_fft'], + hop_length=feat_conf['hop_length'], + window=feat_conf['window'], + win_length=feat_conf['window_length'], + f_min=feat_conf['f_min'], + f_max=feat_conf['f_max'], + n_mels=feat_conf['n_mels'], ) + feats = feature_extractor( + paddle.to_tensor(paddle.to_tensor(waveform).unsqueeze(0))) + self._inputs['feats'] = paddle.transpose(feats, [0, 2, 1]).unsqueeze( + 1) # [B, N, T] -> [B, 1, T, N] + + @paddle.no_grad() + def infer(self): + """ + Model inference and result stored in self.output. + """ + self._outputs['logits'] = self.model(self._inputs['feats']) + + def _generate_topk_label(self, result: np.ndarray, topk: int) -> str: + assert topk <= len( + self._label_list), 'Value of topk is larger than number of labels.' + + topk_idx = (-result).argsort()[:topk] + ret = '' + for idx in topk_idx: + label, score = self._label_list[idx], result[idx] + ret += f'{label} {score} ' + return ret + + def postprocess(self, topk: int) -> Union[str, os.PathLike]: + """ + Output postprocess and return human-readable results such as texts and audio files. + """ + return self._generate_topk_label( + result=self._outputs['logits'].squeeze(0).numpy(), topk=topk) + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + + model_type = parser_args.model + label_file = parser_args.label_file + cfg_path = parser_args.config + ckpt_path = parser_args.ckpt_path + topk = parser_args.topk + device = parser_args.device + + if not parser_args.verbose: + self.disable_task_loggers() + + task_source = self.get_task_source(parser_args.input) + task_results = OrderedDict() + has_exceptions = False + + for id_, input_ in task_source.items(): + try: + res = self(input_, model_type, cfg_path, ckpt_path, label_file, + topk, device) + task_results[id_] = res + except Exception as e: + has_exceptions = True + task_results[id_] = f'{e.__class__.__name__}: {e}' + + self.process_task_results(parser_args.input, task_results, + parser_args.job_dump_result) + + if has_exceptions: + return False + else: + return True + + @stats_wrapper + def __call__(self, + audio_file: os.PathLike, + model: str='panns_cnn14', + config: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None, + label_file: Optional[os.PathLike]=None, + topk: int=1, + device: str=paddle.get_device()): + """ + Python API to call an executor. + """ + audio_file = os.path.abspath(os.path.expanduser(audio_file)) + paddle.set_device(device) + self._init_from_path(model, config, ckpt_path, label_file) + self.preprocess(audio_file) + self.infer() + res = self.postprocess(topk) # Retrieve result of cls. + + return res diff --git a/ernie-sat/paddlespeech/cli/download.py b/ernie-sat/paddlespeech/cli/download.py new file mode 100644 index 0000000..0f09b6f --- /dev/null +++ b/ernie-sat/paddlespeech/cli/download.py @@ -0,0 +1,329 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import hashlib +import os +import os.path as osp +import shutil +import subprocess +import tarfile +import time +import zipfile + +import requests +from tqdm import tqdm + +from .log import logger + +__all__ = ['get_path_from_url'] + +DOWNLOAD_RETRY_LIMIT = 3 + + +def _is_url(path): + """ + Whether path is URL. + Args: + path (string): URL string or not. + """ + return path.startswith('http://') or path.startswith('https://') + + +def _map_path(url, root_dir): + # parse path after download under root_dir + fname = osp.split(url)[-1] + fpath = fname + return osp.join(root_dir, fpath) + + +def _get_unique_endpoints(trainer_endpoints): + # Sorting is to avoid different environmental variables for each card + trainer_endpoints.sort() + ips = set() + unique_endpoints = set() + for endpoint in trainer_endpoints: + ip = endpoint.split(":")[0] + if ip in ips: + continue + ips.add(ip) + unique_endpoints.add(endpoint) + logger.info("unique_endpoints {}".format(unique_endpoints)) + return unique_endpoints + + +def get_path_from_url(url, + root_dir, + md5sum=None, + check_exist=True, + decompress=True, + method='get'): + """ Download from given url to root_dir. + if file or directory specified by url is exists under + root_dir, return the path directly, otherwise download + from url and decompress it, return the path. + Args: + url (str): download url + root_dir (str): root dir for downloading, it should be + WEIGHTS_HOME or DATASET_HOME + md5sum (str): md5 sum of download package + decompress (bool): decompress zip or tar file. Default is `True` + method (str): which download method to use. Support `wget` and `get`. Default is `get`. + Returns: + str: a local path to save downloaded models & weights & datasets. + """ + + from paddle.fluid.dygraph.parallel import ParallelEnv + + assert _is_url(url), "downloading from {} not a url".format(url) + # parse path after download to decompress under root_dir + fullpath = _map_path(url, root_dir) + # Mainly used to solve the problem of downloading data from different + # machines in the case of multiple machines. Different ips will download + # data, and the same ip will only download data once. + unique_endpoints = _get_unique_endpoints(ParallelEnv().trainer_endpoints[:]) + if osp.exists(fullpath) and check_exist and _md5check(fullpath, md5sum): + logger.info("Found {}".format(fullpath)) + else: + if ParallelEnv().current_endpoint in unique_endpoints: + fullpath = _download(url, root_dir, md5sum, method=method) + else: + while not os.path.exists(fullpath): + time.sleep(1) + + if ParallelEnv().current_endpoint in unique_endpoints: + if decompress and (tarfile.is_tarfile(fullpath) or + zipfile.is_zipfile(fullpath)): + fullpath = _decompress(fullpath) + + return fullpath + + +def _get_download(url, fullname): + # using requests.get method + fname = osp.basename(fullname) + try: + req = requests.get(url, stream=True) + except Exception as e: # requests.exceptions.ConnectionError + logger.info("Downloading {} from {} failed with exception {}".format( + fname, url, str(e))) + return False + + if req.status_code != 200: + raise RuntimeError("Downloading from {} failed with code " + "{}!".format(url, req.status_code)) + + # For protecting download interupted, download to + # tmp_fullname firstly, move tmp_fullname to fullname + # after download finished + tmp_fullname = fullname + "_tmp" + total_size = req.headers.get('content-length') + with open(tmp_fullname, 'wb') as f: + if total_size: + with tqdm(total=(int(total_size) + 1023) // 1024) as pbar: + for chunk in req.iter_content(chunk_size=1024): + f.write(chunk) + pbar.update(1) + else: + for chunk in req.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + shutil.move(tmp_fullname, fullname) + + return fullname + + +def _wget_download(url, fullname): + # using wget to download url + tmp_fullname = fullname + "_tmp" + # –user-agent + command = 'wget -O {} -t {} {}'.format(tmp_fullname, DOWNLOAD_RETRY_LIMIT, + url) + subprc = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _ = subprc.communicate() + + if subprc.returncode != 0: + raise RuntimeError( + '{} failed. Please make sure `wget` is installed or {} exists'. + format(command, url)) + + shutil.move(tmp_fullname, fullname) + + return fullname + + +_download_methods = { + 'get': _get_download, + 'wget': _wget_download, +} + + +def _download(url, path, md5sum=None, method='get'): + """ + Download from url, save to path. + url (str): download url + path (str): download to given path + md5sum (str): md5 sum of download package + method (str): which download method to use. Support `wget` and `get`. Default is `get`. + """ + assert method in _download_methods, 'make sure `{}` implemented'.format( + method) + + if not osp.exists(path): + os.makedirs(path) + + fname = osp.split(url)[-1] + fullname = osp.join(path, fname) + retry_cnt = 0 + + logger.info("Downloading {} from {}".format(fname, url)) + while not (osp.exists(fullname) and _md5check(fullname, md5sum)): + if retry_cnt < DOWNLOAD_RETRY_LIMIT: + retry_cnt += 1 + else: + raise RuntimeError("Download from {} failed. " + "Retry limit reached".format(url)) + + if not _download_methods[method](url, fullname): + time.sleep(1) + continue + + return fullname + + +def _md5check(fullname, md5sum=None): + if md5sum is None: + return True + + logger.info("File {} md5 checking...".format(fullname)) + md5 = hashlib.md5() + with open(fullname, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + md5.update(chunk) + calc_md5sum = md5.hexdigest() + + if calc_md5sum != md5sum: + logger.info("File {} md5 check failed, {}(calc) != " + "{}(base)".format(fullname, calc_md5sum, md5sum)) + return False + return True + + +def _decompress(fname): + """ + Decompress for zip and tar file + """ + logger.info("Decompressing {}...".format(fname)) + + # For protecting decompressing interupted, + # decompress to fpath_tmp directory firstly, if decompress + # successed, move decompress files to fpath and delete + # fpath_tmp and remove download compress file. + + if tarfile.is_tarfile(fname): + uncompressed_path = _uncompress_file_tar(fname) + elif zipfile.is_zipfile(fname): + uncompressed_path = _uncompress_file_zip(fname) + else: + raise TypeError("Unsupport compress file type {}".format(fname)) + + return uncompressed_path + + +def _uncompress_file_zip(filepath): + files = zipfile.ZipFile(filepath, 'r') + file_list = files.namelist() + + file_dir = os.path.dirname(filepath) + + if _is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + + for item in file_list: + files.extract(item, file_dir) + + elif _is_a_single_dir(file_list): + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[0] + uncompressed_path = os.path.join(file_dir, rootpath) + + for item in file_list: + files.extract(item, file_dir) + + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + if not os.path.exists(uncompressed_path): + os.makedirs(uncompressed_path) + for item in file_list: + files.extract(item, os.path.join(file_dir, rootpath)) + + files.close() + + return uncompressed_path + + +def _uncompress_file_tar(filepath, mode="r:*"): + files = tarfile.open(filepath, mode) + file_list = files.getnames() + + file_dir = os.path.dirname(filepath) + + if _is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + for item in file_list: + files.extract(item, file_dir) + elif _is_a_single_dir(file_list): + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + for item in file_list: + files.extract(item, file_dir) + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + if not os.path.exists(uncompressed_path): + os.makedirs(uncompressed_path) + + for item in file_list: + files.extract(item, os.path.join(file_dir, rootpath)) + + files.close() + + return uncompressed_path + + +def _is_a_single_file(file_list): + if len(file_list) == 1 and file_list[0].find(os.sep) < -1: + return True + return False + + +def _is_a_single_dir(file_list): + new_file_list = [] + for file_path in file_list: + if '/' in file_path: + file_path = file_path.replace('/', os.sep) + elif '\\' in file_path: + file_path = file_path.replace('\\', os.sep) + new_file_list.append(file_path) + + file_name = new_file_list[0].split(os.sep)[0] + for i in range(1, len(new_file_list)): + if file_name != new_file_list[i].split(os.sep)[0]: + return False + return True diff --git a/ernie-sat/paddlespeech/cli/entry.py b/ernie-sat/paddlespeech/cli/entry.py new file mode 100644 index 0000000..32123ec --- /dev/null +++ b/ernie-sat/paddlespeech/cli/entry.py @@ -0,0 +1,41 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from collections import defaultdict + +__all__ = ['commands'] + + +def _CommandDict(): + return defaultdict(_CommandDict) + + +def _execute(): + com = commands + + idx = 0 + for _argv in (['paddlespeech'] + sys.argv[1:]): + if _argv not in com: + break + idx += 1 + com = com[_argv] + + # The method 'execute' of a command instance returns 'True' for a success + # while 'False' for a failure. Here converts this result into a exit status + # in bash: 0 for a success and 1 for a failure. + status = 0 if com['_entry']().execute(sys.argv[idx:]) else 1 + return status + + +commands = _CommandDict() diff --git a/ernie-sat/paddlespeech/cli/executor.py b/ernie-sat/paddlespeech/cli/executor.py new file mode 100644 index 0000000..064939a --- /dev/null +++ b/ernie-sat/paddlespeech/cli/executor.py @@ -0,0 +1,229 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import sys +from abc import ABC +from abc import abstractmethod +from collections import OrderedDict +from typing import Any +from typing import Dict +from typing import List +from typing import Union + +import paddle + +from .log import logger + + +class BaseExecutor(ABC): + """ + An abstract executor of paddlespeech tasks. + """ + + def __init__(self): + self._inputs = OrderedDict() + self._outputs = OrderedDict() + + @abstractmethod + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + + Args: + tag (str): A tag of pretrained model. + + Returns: + os.PathLike: The path on which resources of pretrained model locate. + """ + pass + + @abstractmethod + def _init_from_path(self, *args, **kwargs): + """ + Init model and other resources from arguments. This method should be called by `__call__()`. + """ + pass + + @abstractmethod + def preprocess(self, input: Any, *args, **kwargs): + """ + Input preprocess and return paddle.Tensor stored in self._inputs. + Input content can be a text(tts), a file(asr, cls), a stream(not supported yet) or anything needed. + + Args: + input (Any): Input text/file/stream or other content. + """ + pass + + @paddle.no_grad() + @abstractmethod + def infer(self, *args, **kwargs): + """ + Model inference and put results into self._outputs. + This method get input tensors from self._inputs, and write output tensors into self._outputs. + """ + pass + + @abstractmethod + def postprocess(self, *args, **kwargs) -> Union[str, os.PathLike]: + """ + Output postprocess and return results. + This method get model output from self._outputs and convert it into human-readable results. + + Returns: + Union[str, os.PathLike]: Human-readable results such as texts and audio files. + """ + pass + + @abstractmethod + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. This method can only be accessed by a command line such as `paddlespeech asr`. + + Args: + argv (List[str]): Arguments from command line. + + Returns: + int: Result of the command execution. `True` for a success and `False` for a failure. + """ + pass + + @abstractmethod + def __call__(self, *arg, **kwargs): + """ + Python API to call an executor. + """ + pass + + def get_task_source(self, input_: Union[str, os.PathLike, None] + ) -> Dict[str, Union[str, os.PathLike]]: + """ + Get task input source from command line input. + + Args: + input_ (Union[str, os.PathLike, None]): Input from command line. + + Returns: + Dict[str, Union[str, os.PathLike]]: A dict with ids and inputs. + """ + if self._is_job_input(input_): + ret = self._get_job_contents(input_) + else: + ret = OrderedDict() + + if input_ is None: # Take input from stdin + for i, line in enumerate(sys.stdin): + line = line.strip() + if len(line.split(' ')) == 1: + ret[str(i + 1)] = line + elif len(line.split(' ')) == 2: + id_, info = line.split(' ') + ret[id_] = info + else: # No valid input info from one line. + continue + else: + ret[1] = input_ + return ret + + def process_task_results(self, + input_: Union[str, os.PathLike, None], + results: Dict[str, os.PathLike], + job_dump_result: bool=False): + """ + Handling task results and redirect stdout if needed. + + Args: + input_ (Union[str, os.PathLike, None]): Input from command line. + results (Dict[str, os.PathLike]): Task outputs. + job_dump_result (bool, optional): if True, dumps job results into file. Defaults to False. + """ + + if not self._is_job_input(input_) and len( + results) == 1: # Only one input sample + raw_text = list(results.values())[0] + else: + raw_text = self._format_task_results(results) + + print(raw_text, end='') # Stdout + + if self._is_job_input( + input_) and job_dump_result: # Dump to *.job.done + try: + job_output_file = os.path.abspath(input_) + '.done' + sys.stdout = open(job_output_file, 'w') + print(raw_text, end='') + logger.info(f'Results had been saved to: {job_output_file}') + finally: + sys.stdout.close() + + def _is_job_input(self, input_: Union[str, os.PathLike]) -> bool: + """ + Check if current input file is a job input or not. + + Args: + input_ (Union[str, os.PathLike]): Input file of current task. + + Returns: + bool: return `True` for job input, `False` otherwise. + """ + return input_ and os.path.isfile(input_) and (input_.endswith('.job') or + input_.endswith('.txt')) + + def _get_job_contents( + self, job_input: os.PathLike) -> Dict[str, Union[str, os.PathLike]]: + """ + Read a job input file and return its contents in a dictionary. + + Args: + job_input (os.PathLike): The job input file. + + Returns: + Dict[str, str]: Contents of job input. + """ + job_contents = OrderedDict() + with open(job_input) as f: + for line in f: + line = line.strip() + if not line: + continue + k, v = line.split(' ') + job_contents[k] = v + return job_contents + + def _format_task_results( + self, results: Dict[str, Union[str, os.PathLike]]) -> str: + """ + Convert task results to raw text. + + Args: + results (Dict[str, str]): A dictionary of task results. + + Returns: + str: A string object contains task results. + """ + ret = '' + for k, v in results.items(): + ret += f'{k} {v}\n' + return ret + + def disable_task_loggers(self): + """ + Disable all loggers in current task. + """ + loggers = [ + logging.getLogger(name) for name in logging.root.manager.loggerDict + ] + for l in loggers: + l.disabled = True diff --git a/ernie-sat/paddlespeech/cli/log.py b/ernie-sat/paddlespeech/cli/log.py new file mode 100644 index 0000000..8644064 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/log.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import logging + +__all__ = [ + 'logger', +] + + +class Logger(object): + def __init__(self, name: str=None): + name = 'PaddleSpeech' if not name else name + self.logger = logging.getLogger(name) + + log_config = { + 'DEBUG': 10, + 'INFO': 20, + 'TRAIN': 21, + 'EVAL': 22, + 'WARNING': 30, + 'ERROR': 40, + 'CRITICAL': 50, + 'EXCEPTION': 100, + } + for key, level in log_config.items(): + logging.addLevelName(level, key) + if key == 'EXCEPTION': + self.__dict__[key.lower()] = self.logger.exception + else: + self.__dict__[key.lower()] = functools.partial(self.__call__, + level) + + self.format = logging.Formatter( + fmt='[%(asctime)-15s] [%(levelname)8s] - %(message)s') + + self.handler = logging.StreamHandler() + self.handler.setFormatter(self.format) + + self.logger.addHandler(self.handler) + self.logger.setLevel(logging.DEBUG) + self.logger.propagate = False + + def __call__(self, log_level: str, msg: str): + self.logger.log(log_level, msg) + + +logger = Logger() diff --git a/ernie-sat/paddlespeech/cli/st/__init__.py b/ernie-sat/paddlespeech/cli/st/__init__.py new file mode 100644 index 0000000..8cdb4e3 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/st/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import STExecutor diff --git a/ernie-sat/paddlespeech/cli/st/infer.py b/ernie-sat/paddlespeech/cli/st/infer.py new file mode 100644 index 0000000..e64fc57 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/st/infer.py @@ -0,0 +1,380 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import subprocess +from collections import OrderedDict +from typing import List +from typing import Optional +from typing import Union + +import kaldiio +import numpy as np +import paddle +import soundfile +from kaldiio import WriteHelper +from yacs.config import CfgNode + +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.utility import UpdateConfig + +__all__ = ["STExecutor"] + +pretrained_models = { + "fat_st_ted-en-zh": { + "url": + "https://paddlespeech.bj.bcebos.com/s2t/ted_en_zh/st1/st1_transformer_mtl_noam_ted-en-zh_ckpt_0.1.1.model.tar.gz", + "md5": + "d62063f35a16d91210a71081bd2dd557", + "cfg_path": + "model.yaml", + "ckpt_path": + "exp/transformer_mtl_noam/checkpoints/fat_st_ted-en-zh.pdparams", + } +} + +model_alias = {"fat_st": "paddlespeech.s2t.models.u2_st:U2STModel"} + +kaldi_bins = { + "url": + "https://paddlespeech.bj.bcebos.com/s2t/ted_en_zh/st1/kaldi_bins.tar.gz", + "md5": + "c0682303b3f3393dbf6ed4c4e35a53eb", +} + + +@cli_register( + name="paddlespeech.st", description="Speech translation infer command.") +class STExecutor(BaseExecutor): + def __init__(self): + super(STExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog="paddlespeech.st", add_help=True) + self.parser.add_argument( + "--input", type=str, default=None, help="Audio file to translate.") + self.parser.add_argument( + "--model", + type=str, + default="fat_st_ted", + choices=[tag[:tag.index('-')] for tag in pretrained_models.keys()], + help="Choose model type of st task.") + self.parser.add_argument( + "--src_lang", + type=str, + default="en", + help="Choose model source language.") + self.parser.add_argument( + "--tgt_lang", + type=str, + default="zh", + help="Choose model target language.") + self.parser.add_argument( + "--sample_rate", + type=int, + default=16000, + choices=[16000], + help='Choose the audio sample rate of the model. 8000 or 16000') + self.parser.add_argument( + "--config", + type=str, + default=None, + help="Config of st task. Use deault config when it is None.") + self.parser.add_argument( + "--ckpt_path", + type=str, + default=None, + help="Checkpoint file of model.") + self.parser.add_argument( + "--device", + type=str, + default=paddle.get_device(), + help="Choose device to execute model inference.") + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + "Use pretrained model stored in: {}".format(decompressed_path)) + + return decompressed_path + + def _set_kaldi_bins(self) -> os.PathLike: + """ + Download and returns kaldi_bins resources path of current task. + """ + decompressed_path = download_and_decompress(kaldi_bins, MODEL_HOME) + decompressed_path = os.path.abspath(decompressed_path) + logger.info("Kaldi_bins stored in: {}".format(decompressed_path)) + if "LD_LIBRARY_PATH" in os.environ: + os.environ["LD_LIBRARY_PATH"] += f":{decompressed_path}" + else: + os.environ["LD_LIBRARY_PATH"] = f"{decompressed_path}" + os.environ["PATH"] += f":{decompressed_path}" + return decompressed_path + + def _init_from_path(self, + model_type: str="fat_st_ted", + src_lang: str="en", + tgt_lang: str="zh", + cfg_path: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'model'): + logger.info('Model had been initialized.') + return + + if cfg_path is None or ckpt_path is None: + tag = model_type + "-" + src_lang + "-" + tgt_lang + res_path = self._get_pretrained_path(tag) + self.cfg_path = os.path.join(res_path, + pretrained_models[tag]["cfg_path"]) + self.ckpt_path = os.path.join(res_path, + pretrained_models[tag]["ckpt_path"]) + logger.info(res_path) + logger.info(self.cfg_path) + logger.info(self.ckpt_path) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.ckpt_path = os.path.abspath(ckpt_path) + res_path = os.path.dirname( + os.path.dirname(os.path.abspath(self.cfg_path))) + + #Init body. + self.config = CfgNode(new_allowed=True) + self.config.merge_from_file(self.cfg_path) + self.config.decode.decoding_method = "fullsentence" + + with UpdateConfig(self.config): + self.config.cmvn_path = os.path.join(res_path, + self.config.cmvn_path) + self.config.spm_model_prefix = os.path.join( + res_path, self.config.spm_model_prefix) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, + vocab=self.config.vocab_filepath, + spm_model_prefix=self.config.spm_model_prefix) + + model_conf = self.config + model_name = model_type[:model_type.rindex( + '_')] # model_type: {model_name}_{dataset} + model_class = dynamic_import(model_name, model_alias) + self.model = model_class.from_config(model_conf) + self.model.eval() + + # load model + params_path = self.ckpt_path + model_dict = paddle.load(params_path) + self.model.set_state_dict(model_dict) + + # set kaldi bins + self._set_kaldi_bins() + + def _check(self, audio_file: str, sample_rate: int): + _, audio_sample_rate = soundfile.read( + audio_file, dtype="int16", always_2d=True) + if audio_sample_rate != sample_rate: + raise Exception("invalid sample rate") + sys.exit(-1) + + def preprocess(self, wav_file: Union[str, os.PathLike], model_type: str): + """ + Input preprocess and return paddle.Tensor stored in self.input. + Input content can be a file(wav). + """ + audio_file = os.path.abspath(wav_file) + logger.info("Preprocess audio_file:" + audio_file) + + if "fat_st" in model_type: + cmvn = self.config.cmvn_path + utt_name = "_tmp" + + # Get the object for feature extraction + fbank_extract_command = [ + "compute-fbank-feats", "--num-mel-bins=80", "--verbose=2", + "--sample-frequency=16000", "scp:-", "ark:-" + ] + fbank_extract_process = subprocess.Popen( + fbank_extract_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + fbank_extract_process.stdin.write( + f"{utt_name} {wav_file}".encode("utf8")) + fbank_extract_process.stdin.close() + fbank_feat = dict( + kaldiio.load_ark(fbank_extract_process.stdout))[utt_name] + + extract_command = ["compute-kaldi-pitch-feats", "scp:-", "ark:-"] + pitch_extract_process = subprocess.Popen( + extract_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + pitch_extract_process.stdin.write( + f"{utt_name} {wav_file}".encode("utf8")) + process_command = ["process-kaldi-pitch-feats", "ark:", "ark:-"] + pitch_process = subprocess.Popen( + process_command, + stdin=pitch_extract_process.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + pitch_extract_process.stdin.close() + pitch_feat = dict(kaldiio.load_ark(pitch_process.stdout))[utt_name] + concated_feat = np.concatenate((fbank_feat, pitch_feat), axis=1) + raw_feat = f"{utt_name}.raw" + with WriteHelper( + f"ark,scp:{raw_feat}.ark,{raw_feat}.scp") as writer: + writer(utt_name, concated_feat) + cmvn_command = [ + "apply-cmvn", "--norm-vars=true", cmvn, f"scp:{raw_feat}.scp", + "ark:-" + ] + cmvn_process = subprocess.Popen( + cmvn_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process_command = [ + "copy-feats", "--compress=true", "ark:-", "ark:-" + ] + process = subprocess.Popen( + process_command, + stdin=cmvn_process.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + norm_feat = dict(kaldiio.load_ark(process.stdout))[utt_name] + self._inputs["audio"] = paddle.to_tensor(norm_feat).unsqueeze(0) + self._inputs["audio_len"] = paddle.to_tensor( + self._inputs["audio"].shape[1], dtype="int64") + else: + raise ValueError("Wrong model type.") + + @paddle.no_grad() + def infer(self, model_type: str): + """ + Model inference and result stored in self.output. + """ + cfg = self.config.decode + audio = self._inputs["audio"] + audio_len = self._inputs["audio_len"] + if model_type == "fat_st_ted": + hyps = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=cfg.decoding_method, + beam_size=cfg.beam_size, + word_reward=cfg.word_reward, + decoding_chunk_size=cfg.decoding_chunk_size, + num_decoding_left_chunks=cfg.num_decoding_left_chunks, + simulate_streaming=cfg.simulate_streaming) + self._outputs["result"] = hyps + else: + raise ValueError("Wrong model type.") + + def postprocess(self, model_type: str) -> Union[str, os.PathLike]: + """ + Output postprocess and return human-readable results such as texts and audio files. + """ + if model_type == "fat_st_ted": + return self._outputs["result"] + else: + raise ValueError("Wrong model type.") + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + + model = parser_args.model + src_lang = parser_args.src_lang + tgt_lang = parser_args.tgt_lang + sample_rate = parser_args.sample_rate + config = parser_args.config + ckpt_path = parser_args.ckpt_path + device = parser_args.device + + if not parser_args.verbose: + self.disable_task_loggers() + + task_source = self.get_task_source(parser_args.input) + task_results = OrderedDict() + has_exceptions = False + + for id_, input_ in task_source.items(): + try: + res = self(input_, model, src_lang, tgt_lang, sample_rate, + config, ckpt_path, device) + task_results[id_] = res + except Exception as e: + has_exceptions = True + task_results[id_] = f'{e.__class__.__name__}: {e}' + + self.process_task_results(parser_args.input, task_results, + parser_args.job_dump_result) + + if has_exceptions: + return False + else: + return True + + @stats_wrapper + def __call__(self, + audio_file: os.PathLike, + model: str='fat_st_ted', + src_lang: str='en', + tgt_lang: str='zh', + sample_rate: int=16000, + config: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None, + device: str=paddle.get_device()): + """ + Python API to call an executor. + """ + audio_file = os.path.abspath(audio_file) + self._check(audio_file, sample_rate) + paddle.set_device(device) + self._init_from_path(model, src_lang, tgt_lang, config, ckpt_path) + self.preprocess(audio_file, model) + self.infer(model) + res = self.postprocess(model) + + return res diff --git a/ernie-sat/paddlespeech/cli/stats/__init__.py b/ernie-sat/paddlespeech/cli/stats/__init__.py new file mode 100644 index 0000000..9fe6c4a --- /dev/null +++ b/ernie-sat/paddlespeech/cli/stats/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import StatsExecutor diff --git a/ernie-sat/paddlespeech/cli/stats/infer.py b/ernie-sat/paddlespeech/cli/stats/infer.py new file mode 100644 index 0000000..4ef5044 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/stats/infer.py @@ -0,0 +1,193 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from typing import List + +from prettytable import PrettyTable + +from ..log import logger +from ..utils import cli_register +from ..utils import stats_wrapper + +__all__ = ['StatsExecutor'] + +model_name_format = { + 'asr': 'Model-Language-Sample Rate', + 'cls': 'Model-Sample Rate', + 'st': 'Model-Source language-Target language', + 'text': 'Model-Task-Language', + 'tts': 'Model-Language' +} + + +@cli_register( + name='paddlespeech.stats', + description='Get speech tasks support models list.') +class StatsExecutor(): + def __init__(self): + super(StatsExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech.stats', add_help=True) + self.parser.add_argument( + '--task', + type=str, + default='asr', + choices=['asr', 'cls', 'st', 'text', 'tts'], + help='Choose speech task.', + required=True) + self.task_choices = ['asr', 'cls', 'st', 'text', 'tts'] + + def show_support_models(self, pretrained_models: dict): + fields = model_name_format[self.task].split("-") + table = PrettyTable(fields) + for key in pretrained_models: + table.add_row(key.split("-")) + print(table) + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + self.task = parser_args.task + if self.task not in self.task_choices: + logger.error( + "Please input correct speech task, choices = ['asr', 'cls', 'st', 'text', 'tts']" + ) + return False + + elif self.task == 'asr': + try: + from ..asr.infer import pretrained_models + logger.info( + "Here is the list of ASR pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + return True + except BaseException: + logger.error("Failed to get the list of ASR pretrained models.") + return False + + elif self.task == 'cls': + try: + from ..cls.infer import pretrained_models + logger.info( + "Here is the list of CLS pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + return True + except BaseException: + logger.error("Failed to get the list of CLS pretrained models.") + return False + + elif self.task == 'st': + try: + from ..st.infer import pretrained_models + logger.info( + "Here is the list of ST pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + return True + except BaseException: + logger.error("Failed to get the list of ST pretrained models.") + return False + + elif self.task == 'text': + try: + from ..text.infer import pretrained_models + logger.info( + "Here is the list of TEXT pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + return True + except BaseException: + logger.error( + "Failed to get the list of TEXT pretrained models.") + return False + + elif self.task == 'tts': + try: + from ..tts.infer import pretrained_models + logger.info( + "Here is the list of TTS pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + return True + except BaseException: + logger.error("Failed to get the list of TTS pretrained models.") + return False + + @stats_wrapper + def __call__( + self, + task: str=None, ): + """ + Python API to call an executor. + """ + self.task = task + if self.task not in self.task_choices: + print( + "Please input correct speech task, choices = ['asr', 'cls', 'st', 'text', 'tts']" + ) + + elif self.task == 'asr': + try: + from ..asr.infer import pretrained_models + print( + "Here is the list of ASR pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + except BaseException: + print("Failed to get the list of ASR pretrained models.") + + elif self.task == 'cls': + try: + from ..cls.infer import pretrained_models + print( + "Here is the list of CLS pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + except BaseException: + print("Failed to get the list of CLS pretrained models.") + + elif self.task == 'st': + try: + from ..st.infer import pretrained_models + print( + "Here is the list of ST pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + except BaseException: + print("Failed to get the list of ST pretrained models.") + + elif self.task == 'text': + try: + from ..text.infer import pretrained_models + print( + "Here is the list of TEXT pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + except BaseException: + print("Failed to get the list of TEXT pretrained models.") + + elif self.task == 'tts': + try: + from ..tts.infer import pretrained_models + print( + "Here is the list of TTS pretrained models released by PaddleSpeech that can be used by command line and python API" + ) + self.show_support_models(pretrained_models) + except BaseException: + print("Failed to get the list of TTS pretrained models.") diff --git a/ernie-sat/paddlespeech/cli/text/__init__.py b/ernie-sat/paddlespeech/cli/text/__init__.py new file mode 100644 index 0000000..f4573fa --- /dev/null +++ b/ernie-sat/paddlespeech/cli/text/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import TextExecutor diff --git a/ernie-sat/paddlespeech/cli/text/infer.py b/ernie-sat/paddlespeech/cli/text/infer.py new file mode 100644 index 0000000..dcf306c --- /dev/null +++ b/ernie-sat/paddlespeech/cli/text/infer.py @@ -0,0 +1,322 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import re +from collections import OrderedDict +from typing import List +from typing import Optional +from typing import Union + +import paddle + +from ...s2t.utils.dynamic_import import dynamic_import +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper + +__all__ = ['TextExecutor'] + +pretrained_models = { + # The tags for pretrained_models should be "{model_name}[_{dataset}][-{lang}][-...]". + # e.g. "conformer_wenetspeech-zh-16k", "transformer_aishell-zh-16k" and "panns_cnn6-32k". + # Command line and python api use "{model_name}[_{dataset}]" as --model, usage: + # "paddlespeech asr --model conformer_wenetspeech --lang zh --sr 16000 --input ./input.wav" + "ernie_linear_p7_wudao-punc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/text/ernie_linear_p7_wudao-punc-zh.tar.gz', + 'md5': + '12283e2ddde1797c5d1e57036b512746', + 'cfg_path': + 'ckpt/model_config.json', + 'ckpt_path': + 'ckpt/model_state.pdparams', + 'vocab_file': + 'punc_vocab.txt', + }, + "ernie_linear_p3_wudao-punc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/text/ernie_linear_p3_wudao-punc-zh.tar.gz', + 'md5': + '448eb2fdf85b6a997e7e652e80c51dd2', + 'cfg_path': + 'ckpt/model_config.json', + 'ckpt_path': + 'ckpt/model_state.pdparams', + 'vocab_file': + 'punc_vocab.txt', + }, +} + +model_alias = { + "ernie_linear_p7": "paddlespeech.text.models:ErnieLinear", + "ernie_linear_p3": "paddlespeech.text.models:ErnieLinear", +} + +tokenizer_alias = { + "ernie_linear_p7": "paddlenlp.transformers:ErnieTokenizer", + "ernie_linear_p3": "paddlenlp.transformers:ErnieTokenizer", +} + + +@cli_register(name='paddlespeech.text', description='Text infer command.') +class TextExecutor(BaseExecutor): + def __init__(self): + super(TextExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech.text', add_help=True) + self.parser.add_argument( + '--input', type=str, default=None, help='Input text.') + self.parser.add_argument( + '--task', + type=str, + default='punc', + choices=['punc'], + help='Choose text task.') + self.parser.add_argument( + '--model', + type=str, + default='ernie_linear_p7_wudao', + choices=[tag[:tag.index('-')] for tag in pretrained_models.keys()], + help='Choose model type of text task.') + self.parser.add_argument( + '--lang', + type=str, + default='zh', + choices=['zh', 'en'], + help='Choose model language.') + self.parser.add_argument( + '--config', + type=str, + default=None, + help='Config of cls task. Use deault config when it is None.') + self.parser.add_argument( + '--ckpt_path', + type=str, + default=None, + help='Checkpoint file of model.') + self.parser.add_argument( + '--punc_vocab', + type=str, + default=None, + help='Vocabulary file of punctuation restoration task.') + self.parser.add_argument( + '--device', + type=str, + default=paddle.get_device(), + help='Choose device to execute model inference.') + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + + return decompressed_path + + def _init_from_path(self, + task: str='punc', + model_type: str='ernie_linear_p7_wudao', + lang: str='zh', + cfg_path: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None, + vocab_file: Optional[os.PathLike]=None): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'model'): + logger.info('Model had been initialized.') + return + + self.task = task + + if cfg_path is None or ckpt_path is None or vocab_file is None: + tag = '-'.join([model_type, task, lang]) + self.res_path = self._get_pretrained_path(tag) + self.cfg_path = os.path.join(self.res_path, + pretrained_models[tag]['cfg_path']) + self.ckpt_path = os.path.join(self.res_path, + pretrained_models[tag]['ckpt_path']) + self.vocab_file = os.path.join(self.res_path, + pretrained_models[tag]['vocab_file']) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.ckpt_path = os.path.abspath(ckpt_path) + self.vocab_file = os.path.abspath(vocab_file) + + model_name = model_type[:model_type.rindex('_')] + if self.task == 'punc': + # punc list + self._punc_list = [] + with open(self.vocab_file, 'r') as f: + for line in f: + self._punc_list.append(line.strip()) + + # model + model_class = dynamic_import(model_name, model_alias) + tokenizer_class = dynamic_import(model_name, tokenizer_alias) + self.model = model_class( + cfg_path=self.cfg_path, ckpt_path=self.ckpt_path) + self.tokenizer = tokenizer_class.from_pretrained('ernie-1.0') + else: + raise NotImplementedError + + self.model.eval() + + def _clean_text(self, text): + text = text.lower() + text = re.sub('[^A-Za-z0-9\u4e00-\u9fa5]', '', text) + text = re.sub(f'[{"".join([p for p in self._punc_list][1:])}]', '', + text) + return text + + def preprocess(self, text: Union[str, os.PathLike]): + """ + Input preprocess and return paddle.Tensor stored in self.input. + Input content can be a text(tts), a file(asr, cls) or a streaming(not supported yet). + """ + if self.task == 'punc': + clean_text = self._clean_text(text) + assert len(clean_text) > 0, f'Invalid input string: {text}' + + tokenized_input = self.tokenizer( + list(clean_text), return_length=True, is_split_into_words=True) + + self._inputs['input_ids'] = tokenized_input['input_ids'] + self._inputs['seg_ids'] = tokenized_input['token_type_ids'] + self._inputs['seq_len'] = tokenized_input['seq_len'] + else: + raise NotImplementedError + + @paddle.no_grad() + def infer(self): + """ + Model inference and result stored in self.output. + """ + if self.task == 'punc': + input_ids = paddle.to_tensor(self._inputs['input_ids']).unsqueeze(0) + seg_ids = paddle.to_tensor(self._inputs['seg_ids']).unsqueeze(0) + logits, _ = self.model(input_ids, seg_ids) + preds = paddle.argmax(logits, axis=-1).squeeze(0) + + self._outputs['preds'] = preds + else: + raise NotImplementedError + + def postprocess(self) -> Union[str, os.PathLike]: + """ + Output postprocess and return human-readable results such as texts and audio files. + """ + if self.task == 'punc': + input_ids = self._inputs['input_ids'] + seq_len = self._inputs['seq_len'] + preds = self._outputs['preds'] + + tokens = self.tokenizer.convert_ids_to_tokens( + input_ids[1:seq_len - 1]) + labels = preds[1:seq_len - 1].tolist() + assert len(tokens) == len(labels) + + text = '' + for t, l in zip(tokens, labels): + text += t + if l != 0: # Non punc. + text += self._punc_list[l] + + return text + else: + raise NotImplementedError + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + + task = parser_args.task + model_type = parser_args.model + lang = parser_args.lang + cfg_path = parser_args.config + ckpt_path = parser_args.ckpt_path + punc_vocab = parser_args.punc_vocab + device = parser_args.device + + if not parser_args.verbose: + self.disable_task_loggers() + + task_source = self.get_task_source(parser_args.input) + task_results = OrderedDict() + has_exceptions = False + + for id_, input_ in task_source.items(): + try: + res = self(input_, task, model_type, lang, cfg_path, ckpt_path, + punc_vocab, device) + task_results[id_] = res + except Exception as e: + has_exceptions = True + task_results[id_] = f'{e.__class__.__name__}: {e}' + + self.process_task_results(parser_args.input, task_results, + parser_args.job_dump_result) + + if has_exceptions: + return False + else: + return True + + @stats_wrapper + def __call__( + self, + text: str, + task: str='punc', + model: str='ernie_linear_p7_wudao', + lang: str='zh', + config: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None, + punc_vocab: Optional[os.PathLike]=None, + device: str=paddle.get_device(), ): + """ + Python API to call an executor. + """ + paddle.set_device(device) + self._init_from_path(task, model, lang, config, ckpt_path, punc_vocab) + self.preprocess(text) + self.infer() + res = self.postprocess() # Retrieve result of text task. + + return res diff --git a/ernie-sat/paddlespeech/cli/tts/__init__.py b/ernie-sat/paddlespeech/cli/tts/__init__.py new file mode 100644 index 0000000..4cc3c42 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/tts/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import TTSExecutor diff --git a/ernie-sat/paddlespeech/cli/tts/infer.py b/ernie-sat/paddlespeech/cli/tts/infer.py new file mode 100644 index 0000000..c7a1edc --- /dev/null +++ b/ernie-sat/paddlespeech/cli/tts/infer.py @@ -0,0 +1,838 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import time +from collections import OrderedDict +from typing import Any +from typing import List +from typing import Optional +from typing import Union + +import numpy as np +import paddle +import soundfile as sf +import yaml +from yacs.config import CfgNode + +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.t2s.frontend import English +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.t2s.modules.normalizer import ZScore + +__all__ = ['TTSExecutor'] + +pretrained_models = { + # speedyspeech + "speedyspeech_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/speedyspeech/speedyspeech_nosil_baker_ckpt_0.5.zip', + 'md5': + '9edce23b1a87f31b814d9477bf52afbc', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_11400.pdz', + 'speech_stats': + 'feats_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + 'tones_dict': + 'tone_id_map.txt', + }, + + # fastspeech2 + "fastspeech2_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_baker_ckpt_0.4.zip', + 'md5': + '637d28a5e53aa60275612ba4393d5f22', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_76000.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + }, + "fastspeech2_ljspeech-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_ljspeech_ckpt_0.5.zip', + 'md5': + 'ffed800c93deaf16ca9b3af89bfcd747', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_100000.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + }, + "fastspeech2_aishell3-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_aishell3_ckpt_0.4.zip', + 'md5': + 'f4dd4a5f49a4552b77981f544ab3392e', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_96400.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + 'speaker_dict': + 'speaker_id_map.txt', + }, + "fastspeech2_vctk-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_vctk_ckpt_0.5.zip', + 'md5': + '743e5024ca1e17a88c5c271db9779ba4', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_66200.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + 'speaker_dict': + 'speaker_id_map.txt', + }, + # tacotron2 + "tacotron2_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/tacotron2/tacotron2_csmsc_ckpt_0.2.0.zip', + 'md5': + '0df4b6f0bcbe0d73c5ed6df8867ab91a', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_30600.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + }, + "tacotron2_ljspeech-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/tacotron2/tacotron2_ljspeech_ckpt_0.2.0.zip', + 'md5': + '6a5eddd81ae0e81d16959b97481135f3', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_60300.pdz', + 'speech_stats': + 'speech_stats.npy', + 'phones_dict': + 'phone_id_map.txt', + }, + + # pwgan + "pwgan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_baker_ckpt_0.4.zip', + 'md5': + '2e481633325b5bdf0a3823c714d2c117', + 'config': + 'pwg_default.yaml', + 'ckpt': + 'pwg_snapshot_iter_400000.pdz', + 'speech_stats': + 'pwg_stats.npy', + }, + "pwgan_ljspeech-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_ljspeech_ckpt_0.5.zip', + 'md5': + '53610ba9708fd3008ccaf8e99dacbaf0', + 'config': + 'pwg_default.yaml', + 'ckpt': + 'pwg_snapshot_iter_400000.pdz', + 'speech_stats': + 'pwg_stats.npy', + }, + "pwgan_aishell3-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_aishell3_ckpt_0.5.zip', + 'md5': + 'd7598fa41ad362d62f85ffc0f07e3d84', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_1000000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + "pwgan_vctk-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_vctk_ckpt_0.1.1.zip', + 'md5': + 'b3da1defcde3e578be71eb284cb89f2c', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_1500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + # mb_melgan + "mb_melgan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/mb_melgan/mb_melgan_csmsc_ckpt_0.1.1.zip', + 'md5': + 'ee5f0604e20091f0d495b6ec4618b90d', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_1000000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + # style_melgan + "style_melgan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/style_melgan/style_melgan_csmsc_ckpt_0.1.1.zip', + 'md5': + '5de2d5348f396de0c966926b8c462755', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_1500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + # hifigan + "hifigan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/hifigan/hifigan_csmsc_ckpt_0.1.1.zip', + 'md5': + 'dd40a3d88dfcf64513fba2f0f961ada6', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_2500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + "hifigan_ljspeech-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/hifigan/hifigan_ljspeech_ckpt_0.2.0.zip', + 'md5': + '70e9131695decbca06a65fe51ed38a72', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_2500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + "hifigan_aishell3-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/hifigan/hifigan_aishell3_ckpt_0.2.0.zip', + 'md5': + '3bb49bc75032ed12f79c00c8cc79a09a', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_2500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + "hifigan_vctk-en": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/hifigan/hifigan_vctk_ckpt_0.2.0.zip', + 'md5': + '7da8f88359bca2457e705d924cf27bd4', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_2500000.pdz', + 'speech_stats': + 'feats_stats.npy', + }, + + # wavernn + "wavernn_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/wavernn/wavernn_csmsc_ckpt_0.2.0.zip', + 'md5': + 'ee37b752f09bcba8f2af3b777ca38e13', + 'config': + 'default.yaml', + 'ckpt': + 'snapshot_iter_400000.pdz', + 'speech_stats': + 'feats_stats.npy', + } +} + +model_alias = { + # acoustic model + "speedyspeech": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeech", + "speedyspeech_inference": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeechInference", + "fastspeech2": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2", + "fastspeech2_inference": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2Inference", + "tacotron2": + "paddlespeech.t2s.models.tacotron2:Tacotron2", + "tacotron2_inference": + "paddlespeech.t2s.models.tacotron2:Tacotron2Inference", + # voc + "pwgan": + "paddlespeech.t2s.models.parallel_wavegan:PWGGenerator", + "pwgan_inference": + "paddlespeech.t2s.models.parallel_wavegan:PWGInference", + "mb_melgan": + "paddlespeech.t2s.models.melgan:MelGANGenerator", + "mb_melgan_inference": + "paddlespeech.t2s.models.melgan:MelGANInference", + "style_melgan": + "paddlespeech.t2s.models.melgan:StyleMelGANGenerator", + "style_melgan_inference": + "paddlespeech.t2s.models.melgan:StyleMelGANInference", + "hifigan": + "paddlespeech.t2s.models.hifigan:HiFiGANGenerator", + "hifigan_inference": + "paddlespeech.t2s.models.hifigan:HiFiGANInference", + "wavernn": + "paddlespeech.t2s.models.wavernn:WaveRNN", + "wavernn_inference": + "paddlespeech.t2s.models.wavernn:WaveRNNInference", +} + + +@cli_register( + name='paddlespeech.tts', description='Text to Speech infer command.') +class TTSExecutor(BaseExecutor): + def __init__(self): + super().__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech.tts', add_help=True) + self.parser.add_argument( + '--input', type=str, default=None, help='Input text to generate.') + # acoustic model + self.parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'speedyspeech_csmsc', + 'fastspeech2_csmsc', + 'fastspeech2_ljspeech', + 'fastspeech2_aishell3', + 'fastspeech2_vctk', + 'tacotron2_csmsc', + 'tacotron2_ljspeech', + ], + help='Choose acoustic model type of tts task.') + self.parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + self.parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + self.parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + self.parser.add_argument( + "--phones_dict", + type=str, + default=None, + help="phone vocabulary file.") + self.parser.add_argument( + "--tones_dict", + type=str, + default=None, + help="tone vocabulary file.") + self.parser.add_argument( + "--speaker_dict", + type=str, + default=None, + help="speaker id map file.") + self.parser.add_argument( + '--spk_id', + type=int, + default=0, + help='spk id for multi speaker acoustic model') + # vocoder + self.parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=[ + 'pwgan_csmsc', + 'pwgan_ljspeech', + 'pwgan_aishell3', + 'pwgan_vctk', + 'mb_melgan_csmsc', + 'style_melgan_csmsc', + 'hifigan_csmsc', + 'hifigan_ljspeech', + 'hifigan_aishell3', + 'hifigan_vctk', + 'wavernn_csmsc', + ], + help='Choose vocoder type of tts task.') + + self.parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + self.parser.add_argument( + '--voc_ckpt', + type=str, + default=None, + help='Checkpoint file of voc.') + self.parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + # other + self.parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en') + self.parser.add_argument( + '--device', + type=str, + default=paddle.get_device(), + help='Choose device to execute model inference.') + + self.parser.add_argument( + '--output', type=str, default='output.wav', help='output file name') + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + return decompressed_path + + def _init_from_path( + self, + am: str='fastspeech2_csmsc', + am_config: Optional[os.PathLike]=None, + am_ckpt: Optional[os.PathLike]=None, + am_stat: Optional[os.PathLike]=None, + phones_dict: Optional[os.PathLike]=None, + tones_dict: Optional[os.PathLike]=None, + speaker_dict: Optional[os.PathLike]=None, + voc: str='pwgan_csmsc', + voc_config: Optional[os.PathLike]=None, + voc_ckpt: Optional[os.PathLike]=None, + voc_stat: Optional[os.PathLike]=None, + lang: str='zh', ): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'am_inference') and hasattr(self, 'voc_inference'): + logger.info('Models had been initialized.') + return + # am + am_tag = am + '-' + lang + if am_ckpt is None or am_config is None or am_stat is None or phones_dict is None: + am_res_path = self._get_pretrained_path(am_tag) + self.am_res_path = am_res_path + self.am_config = os.path.join(am_res_path, + pretrained_models[am_tag]['config']) + self.am_ckpt = os.path.join(am_res_path, + pretrained_models[am_tag]['ckpt']) + self.am_stat = os.path.join( + am_res_path, pretrained_models[am_tag]['speech_stats']) + # must have phones_dict in acoustic + self.phones_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['phones_dict']) + print("self.phones_dict:", self.phones_dict) + logger.info(am_res_path) + logger.info(self.am_config) + logger.info(self.am_ckpt) + else: + self.am_config = os.path.abspath(am_config) + self.am_ckpt = os.path.abspath(am_ckpt) + self.am_stat = os.path.abspath(am_stat) + self.phones_dict = os.path.abspath(phones_dict) + self.am_res_path = os.path.dirname(os.path.abspath(self.am_config)) + print("self.phones_dict:", self.phones_dict) + + # for speedyspeech + self.tones_dict = None + if 'tones_dict' in pretrained_models[am_tag]: + self.tones_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['tones_dict']) + if tones_dict: + self.tones_dict = tones_dict + + # for multi speaker fastspeech2 + self.speaker_dict = None + if 'speaker_dict' in pretrained_models[am_tag]: + self.speaker_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['speaker_dict']) + if speaker_dict: + self.speaker_dict = speaker_dict + + # voc + voc_tag = voc + '-' + lang + if voc_ckpt is None or voc_config is None or voc_stat is None: + voc_res_path = self._get_pretrained_path(voc_tag) + self.voc_res_path = voc_res_path + self.voc_config = os.path.join(voc_res_path, + pretrained_models[voc_tag]['config']) + self.voc_ckpt = os.path.join(voc_res_path, + pretrained_models[voc_tag]['ckpt']) + self.voc_stat = os.path.join( + voc_res_path, pretrained_models[voc_tag]['speech_stats']) + logger.info(voc_res_path) + logger.info(self.voc_config) + logger.info(self.voc_ckpt) + else: + self.voc_config = os.path.abspath(voc_config) + self.voc_ckpt = os.path.abspath(voc_ckpt) + self.voc_stat = os.path.abspath(voc_stat) + self.voc_res_path = os.path.dirname( + os.path.abspath(self.voc_config)) + + # Init body. + with open(self.am_config) as f: + self.am_config = CfgNode(yaml.safe_load(f)) + with open(self.voc_config) as f: + self.voc_config = CfgNode(yaml.safe_load(f)) + + with open(self.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + tone_size = None + if self.tones_dict: + with open(self.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + spk_num = None + if self.speaker_dict: + with open(self.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + print("spk_num:", spk_num) + + # frontend + if lang == 'zh': + self.frontend = Frontend( + phone_vocab_path=self.phones_dict, + tone_vocab_path=self.tones_dict) + + elif lang == 'en': + self.frontend = English(phone_vocab_path=self.phones_dict) + print("frontend done!") + + # acoustic model + odim = self.am_config.n_mels + # model: {model_name}_{dataset} + am_name = am[:am.rindex('_')] + + am_class = dynamic_import(am_name, model_alias) + am_inference_class = dynamic_import(am_name + '_inference', model_alias) + + if am_name == 'fastspeech2': + am = am_class( + idim=vocab_size, + odim=odim, + spk_num=spk_num, + **self.am_config["model"]) + elif am_name == 'speedyspeech': + am = am_class( + vocab_size=vocab_size, + tone_size=tone_size, + **self.am_config["model"]) + elif am_name == 'tacotron2': + am = am_class(idim=vocab_size, odim=odim, **self.am_config["model"]) + + am.set_state_dict(paddle.load(self.am_ckpt)["main_params"]) + am.eval() + am_mu, am_std = np.load(self.am_stat) + am_mu = paddle.to_tensor(am_mu) + am_std = paddle.to_tensor(am_std) + am_normalizer = ZScore(am_mu, am_std) + self.am_inference = am_inference_class(am_normalizer, am) + self.am_inference.eval() + print("acoustic model done!") + + # vocoder + # model: {model_name}_{dataset} + voc_name = voc[:voc.rindex('_')] + voc_class = dynamic_import(voc_name, model_alias) + voc_inference_class = dynamic_import(voc_name + '_inference', + model_alias) + if voc_name != 'wavernn': + voc = voc_class(**self.voc_config["generator_params"]) + voc.set_state_dict(paddle.load(self.voc_ckpt)["generator_params"]) + voc.remove_weight_norm() + voc.eval() + else: + voc = voc_class(**self.voc_config["model"]) + voc.set_state_dict(paddle.load(self.voc_ckpt)["main_params"]) + voc.eval() + voc_mu, voc_std = np.load(self.voc_stat) + voc_mu = paddle.to_tensor(voc_mu) + voc_std = paddle.to_tensor(voc_std) + voc_normalizer = ZScore(voc_mu, voc_std) + self.voc_inference = voc_inference_class(voc_normalizer, voc) + self.voc_inference.eval() + print("voc done!") + + def preprocess(self, input: Any, *args, **kwargs): + """ + Input preprocess and return paddle.Tensor stored in self._inputs. + Input content can be a text(tts), a file(asr, cls), a stream(not supported yet) or anything needed. + + Args: + input (Any): Input text/file/stream or other content. + """ + pass + + @paddle.no_grad() + def infer(self, + text: str, + lang: str='zh', + am: str='fastspeech2_csmsc', + spk_id: int=0): + """ + Model inference and result stored in self.output. + """ + am_name = am[:am.rindex('_')] + am_dataset = am[am.rindex('_') + 1:] + get_tone_ids = False + merge_sentences = False + frontend_st = time.time() + if am_name == 'speedyspeech': + get_tone_ids = True + if lang == 'zh': + input_ids = self.frontend.get_input_ids( + text, + merge_sentences=merge_sentences, + get_tone_ids=get_tone_ids) + phone_ids = input_ids["phone_ids"] + if get_tone_ids: + tone_ids = input_ids["tone_ids"] + elif lang == 'en': + input_ids = self.frontend.get_input_ids( + text, merge_sentences=merge_sentences) + phone_ids = input_ids["phone_ids"] + else: + print("lang should in {'zh', 'en'}!") + self.frontend_time = time.time() - frontend_st + + self.am_time = 0 + self.voc_time = 0 + flags = 0 + for i in range(len(phone_ids)): + am_st = time.time() + part_phone_ids = phone_ids[i] + # am + if am_name == 'speedyspeech': + part_tone_ids = tone_ids[i] + mel = self.am_inference(part_phone_ids, part_tone_ids) + # fastspeech2 + else: + # multi speaker + if am_dataset in {"aishell3", "vctk"}: + mel = self.am_inference( + part_phone_ids, spk_id=paddle.to_tensor(spk_id)) + else: + mel = self.am_inference(part_phone_ids) + self.am_time += (time.time() - am_st) + # voc + voc_st = time.time() + wav = self.voc_inference(mel) + if flags == 0: + wav_all = wav + flags = 1 + else: + wav_all = paddle.concat([wav_all, wav]) + self.voc_time += (time.time() - voc_st) + self._outputs['wav'] = wav_all + + def postprocess(self, output: str='output.wav') -> Union[str, os.PathLike]: + """ + Output postprocess and return results. + This method get model output from self._outputs and convert it into human-readable results. + + Returns: + Union[str, os.PathLike]: Human-readable results such as texts and audio files. + """ + output = os.path.abspath(os.path.expanduser(output)) + sf.write( + output, self._outputs['wav'].numpy(), samplerate=self.am_config.fs) + return output + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + + args = self.parser.parse_args(argv) + + am = args.am + am_config = args.am_config + am_ckpt = args.am_ckpt + am_stat = args.am_stat + phones_dict = args.phones_dict + print("phones_dict:", phones_dict) + tones_dict = args.tones_dict + speaker_dict = args.speaker_dict + voc = args.voc + voc_config = args.voc_config + voc_ckpt = args.voc_ckpt + voc_stat = args.voc_stat + lang = args.lang + device = args.device + spk_id = args.spk_id + + if not args.verbose: + self.disable_task_loggers() + + task_source = self.get_task_source(args.input) + task_results = OrderedDict() + has_exceptions = False + + for id_, input_ in task_source.items(): + if len(task_source) > 1: + assert isinstance(args.output, + str) and args.output.endswith('.wav') + output = args.output.replace('.wav', f'_{id_}.wav') + else: + output = args.output + + try: + res = self( + text=input_, + # acoustic model related + am=am, + am_config=am_config, + am_ckpt=am_ckpt, + am_stat=am_stat, + phones_dict=phones_dict, + tones_dict=tones_dict, + speaker_dict=speaker_dict, + spk_id=spk_id, + # vocoder related + voc=voc, + voc_config=voc_config, + voc_ckpt=voc_ckpt, + voc_stat=voc_stat, + # other + lang=lang, + device=device, + output=output) + task_results[id_] = res + except Exception as e: + has_exceptions = True + task_results[id_] = f'{e.__class__.__name__}: {e}' + + self.process_task_results(args.input, task_results, + args.job_dump_result) + + if has_exceptions: + return False + else: + return True + + @stats_wrapper + def __call__(self, + text: str, + am: str='fastspeech2_csmsc', + am_config: Optional[os.PathLike]=None, + am_ckpt: Optional[os.PathLike]=None, + am_stat: Optional[os.PathLike]=None, + spk_id: int=0, + phones_dict: Optional[os.PathLike]=None, + tones_dict: Optional[os.PathLike]=None, + speaker_dict: Optional[os.PathLike]=None, + voc: str='pwgan_csmsc', + voc_config: Optional[os.PathLike]=None, + voc_ckpt: Optional[os.PathLike]=None, + voc_stat: Optional[os.PathLike]=None, + lang: str='zh', + device: str=paddle.get_device(), + output: str='output.wav'): + """ + Python API to call an executor. + """ + paddle.set_device(device) + self._init_from_path( + am=am, + am_config=am_config, + am_ckpt=am_ckpt, + am_stat=am_stat, + phones_dict=phones_dict, + tones_dict=tones_dict, + speaker_dict=speaker_dict, + voc=voc, + voc_config=voc_config, + voc_ckpt=voc_ckpt, + voc_stat=voc_stat, + lang=lang) + + self.infer(text=text, lang=lang, am=am, spk_id=spk_id) + + res = self.postprocess(output=output) + + return res diff --git a/ernie-sat/paddlespeech/cli/utils.py b/ernie-sat/paddlespeech/cli/utils.py new file mode 100644 index 0000000..f7d64b9 --- /dev/null +++ b/ernie-sat/paddlespeech/cli/utils.py @@ -0,0 +1,340 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import hashlib +import inspect +import json +import os +import tarfile +import threading +import time +import uuid +import zipfile +from typing import Any +from typing import Dict + +import paddle +import requests +import yaml +from paddle.framework import load + +import paddleaudio +from . import download +from .entry import commands +try: + from .. import __version__ +except ImportError: + __version__ = "0.0.0" # for develop branch + +requests.adapters.DEFAULT_RETRIES = 3 + +__all__ = [ + 'cli_register', + 'get_command', + 'download_and_decompress', + 'load_state_dict_from_url', + 'stats_wrapper', +] + + +def cli_register(name: str, description: str='') -> Any: + def _warpper(command): + items = name.split('.') + + com = commands + for item in items: + com = com[item] + com['_entry'] = command + if description: + com['_description'] = description + return command + + return _warpper + + +def get_command(name: str) -> Any: + items = name.split('.') + com = commands + for item in items: + com = com[item] + + return com['_entry'] + + +def _get_uncompress_path(filepath: os.PathLike) -> os.PathLike: + file_dir = os.path.dirname(filepath) + is_zip_file = False + if tarfile.is_tarfile(filepath): + files = tarfile.open(filepath, "r:*") + file_list = files.getnames() + elif zipfile.is_zipfile(filepath): + files = zipfile.ZipFile(filepath, 'r') + file_list = files.namelist() + is_zip_file = True + else: + return file_dir + + if download._is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + elif download._is_a_single_dir(file_list): + if is_zip_file: + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[0] + else: + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + + files.close() + return uncompressed_path + + +def download_and_decompress(archive: Dict[str, str], path: str) -> os.PathLike: + """ + Download archieves and decompress to specific path. + """ + if not os.path.isdir(path): + os.makedirs(path) + + assert 'url' in archive and 'md5' in archive, \ + 'Dictionary keys of "url" and "md5" are required in the archive, but got: {}'.format(list(archive.keys())) + + filepath = os.path.join(path, os.path.basename(archive['url'])) + if os.path.isfile(filepath) and download._md5check(filepath, + archive['md5']): + uncompress_path = _get_uncompress_path(filepath) + if not os.path.isdir(uncompress_path): + download._decompress(filepath) + else: + StatsWorker( + task='download', + version=__version__, + extra_info={ + 'download_url': archive['url'], + 'paddle_version': paddle.__version__ + }).start() + uncompress_path = download.get_path_from_url(archive['url'], path, + archive['md5']) + + return uncompress_path + + +def load_state_dict_from_url(url: str, path: str, md5: str=None) -> os.PathLike: + """ + Download and load a state dict from url + """ + if not os.path.isdir(path): + os.makedirs(path) + + download.get_path_from_url(url, path, md5) + return load(os.path.join(path, os.path.basename(url))) + + +def _get_user_home(): + return os.path.expanduser('~') + + +def _get_paddlespcceh_home(): + if 'PPSPEECH_HOME' in os.environ: + home_path = os.environ['PPSPEECH_HOME'] + if os.path.exists(home_path): + if os.path.isdir(home_path): + return home_path + else: + raise RuntimeError( + 'The environment variable PPSPEECH_HOME {} is not a directory.'. + format(home_path)) + else: + return home_path + return os.path.join(_get_user_home(), '.paddlespeech') + + +def _get_sub_home(directory): + home = os.path.join(_get_paddlespcceh_home(), directory) + if not os.path.exists(home): + os.makedirs(home) + return home + + +PPSPEECH_HOME = _get_paddlespcceh_home() +MODEL_HOME = _get_sub_home('models') +CONF_HOME = _get_sub_home('conf') + + +def _md5(text: str): + '''Calculate the md5 value of the input text.''' + md5code = hashlib.md5(text.encode()) + return md5code.hexdigest() + + +class ConfigCache: + def __init__(self): + self._data = {} + self._initialize() + self.file = os.path.join(CONF_HOME, 'cache.yaml') + if not os.path.exists(self.file): + self.flush() + return + + with open(self.file, 'r') as file: + try: + cfg = yaml.load(file, Loader=yaml.FullLoader) + self._data.update(cfg) + except Exception as e: + self.flush() + + @property + def cache_info(self): + return self._data['cache_info'] + + def _initialize(self): + # Set default configuration values. + cache_info = _md5(str(uuid.uuid1())[-12:]) + "-" + str(int(time.time())) + self._data['cache_info'] = cache_info + + def flush(self): + '''Flush the current configuration into the configuration file.''' + with open(self.file, 'w') as file: + cfg = json.loads(json.dumps(self._data)) + yaml.dump(cfg, file) + + +stats_api = "http://paddlepaddle.org.cn/paddlehub/stat" +cache_info = ConfigCache().cache_info + + +class StatsWorker(threading.Thread): + def __init__(self, + task="asr", + model=None, + version=__version__, + extra_info={}): + threading.Thread.__init__(self) + self._task = task + self._model = model + self._version = version + self._extra_info = extra_info + + def run(self): + params = { + 'task': self._task, + 'version': self._version, + 'from': 'ppspeech' + } + if self._model: + params['model'] = self._model + + self._extra_info.update({ + 'cache_info': cache_info, + }) + params.update({"extra": json.dumps(self._extra_info)}) + + try: + requests.get(stats_api, params) + except Exception: + pass + + return + + +def _note_one_stat(cls_name, params={}): + task = cls_name.replace('Executor', '').lower() # XXExecutor + extra_info = { + 'paddle_version': paddle.__version__, + } + + if 'model' in params: + model = params['model'] + else: + model = None + + if 'audio_file' in params: + try: + _, sr = paddleaudio.load(params['audio_file']) + except Exception: + sr = -1 + + if task == 'asr': + extra_info.update({ + 'lang': params['lang'], + 'inp_sr': sr, + 'model_sr': params['sample_rate'], + }) + elif task == 'st': + extra_info.update({ + 'lang': + params['src_lang'] + '-' + params['tgt_lang'], + 'inp_sr': + sr, + 'model_sr': + params['sample_rate'], + }) + elif task == 'tts': + model = params['am'] + extra_info.update({ + 'lang': params['lang'], + 'vocoder': params['voc'], + }) + elif task == 'cls': + extra_info.update({ + 'inp_sr': sr, + }) + elif task == 'text': + extra_info.update({ + 'sub_task': params['task'], + 'lang': params['lang'], + }) + else: + return + + StatsWorker( + task=task, + model=model, + version=__version__, + extra_info=extra_info, ).start() + + +def _parse_args(func, *args, **kwargs): + # FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) + argspec = inspect.getfullargspec(func) + + keys = argspec[0] + if keys[0] == 'self': # Remove self pointer. + keys = keys[1:] + + default_values = argspec[3] + values = [None] * (len(keys) - len(default_values)) + values.extend(list(default_values)) + params = dict(zip(keys, values)) + + for idx, v in enumerate(args): + params[keys[idx]] = v + for k, v in kwargs.items(): + params[k] = v + + return params + + +def stats_wrapper(executor_func): + def _warpper(self, *args, **kwargs): + try: + _note_one_stat( + type(self).__name__, _parse_args(executor_func, *args, + **kwargs)) + except Exception: + pass + return executor_func(self, *args, **kwargs) + + return _warpper diff --git a/ernie-sat/paddlespeech/cli/vector/__init__.py b/ernie-sat/paddlespeech/cli/vector/__init__.py new file mode 100644 index 0000000..038596a --- /dev/null +++ b/ernie-sat/paddlespeech/cli/vector/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .infer import VectorExecutor diff --git a/ernie-sat/paddlespeech/cli/vector/infer.py b/ernie-sat/paddlespeech/cli/vector/infer.py new file mode 100644 index 0000000..68e832a --- /dev/null +++ b/ernie-sat/paddlespeech/cli/vector/infer.py @@ -0,0 +1,519 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import sys +from collections import OrderedDict +from typing import Dict +from typing import List +from typing import Optional +from typing import Union + +import paddle +import soundfile +from yacs.config import CfgNode + +from ..executor import BaseExecutor +from ..log import logger +from ..utils import cli_register +from ..utils import download_and_decompress +from ..utils import MODEL_HOME +from ..utils import stats_wrapper +from paddleaudio.backends import load as load_audio +from paddleaudio.compliance.librosa import melspectrogram +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.vector.io.batch import feature_normalize +from paddlespeech.vector.modules.sid_model import SpeakerIdetification + +pretrained_models = { + # The tags for pretrained_models should be "{model_name}[-{dataset}][-{sr}][-...]". + # e.g. "ecapatdnn_voxceleb12-16k". + # Command line and python api use "{model_name}[-{dataset}]" as --model, usage: + # "paddlespeech vector --task spk --model ecapatdnn_voxceleb12-16k --sr 16000 --input ./input.wav" + "ecapatdnn_voxceleb12-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/vector/voxceleb/sv0_ecapa_tdnn_voxceleb12_ckpt_0_2_0.tar.gz', + 'md5': + 'cc33023c54ab346cd318408f43fcaf95', + 'cfg_path': + 'conf/model.yaml', # the yaml config path + 'ckpt_path': + 'model/model', # the format is ${dir}/{model_name}, + # so the first 'model' is dir, the second 'model' is the name + # this means we have a model stored as model/model.pdparams + }, +} + +model_alias = { + "ecapatdnn": "paddlespeech.vector.models.ecapa_tdnn:EcapaTdnn", +} + + +@cli_register( + name="paddlespeech.vector", + description="Speech to vector embedding infer command.") +class VectorExecutor(BaseExecutor): + def __init__(self): + super(VectorExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog="paddlespeech.vector", add_help=True) + + self.parser.add_argument( + "--model", + type=str, + default="ecapatdnn_voxceleb12", + choices=["ecapatdnn_voxceleb12"], + help="Choose model type of vector task.") + self.parser.add_argument( + "--task", + type=str, + default="spk", + choices=["spk", "score"], + help="task type in vector domain") + self.parser.add_argument( + "--input", + type=str, + default=None, + help="Audio file to extract embedding.") + self.parser.add_argument( + "--sample_rate", + type=int, + default=16000, + choices=[16000], + help="Choose the audio sample rate of the model. 8000 or 16000") + self.parser.add_argument( + "--ckpt_path", + type=str, + default=None, + help="Checkpoint file of model.") + self.parser.add_argument( + '--config', + type=str, + default=None, + help='Config of asr task. Use deault config when it is None.') + self.parser.add_argument( + "--device", + type=str, + default=paddle.get_device(), + help="Choose device to execute model inference.") + self.parser.add_argument( + '-d', + '--job_dump_result', + action='store_true', + help='Save job result into file.') + + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Increase logger verbosity of current task.') + + def execute(self, argv: List[str]) -> bool: + """Command line entry for vector model + + Args: + argv (List[str]): command line args list + + Returns: + bool: + False: some audio occurs error + True: all audio process success + """ + # stage 0: parse the args and get the required args + parser_args = self.parser.parse_args(argv) + model = parser_args.model + sample_rate = parser_args.sample_rate + config = parser_args.config + ckpt_path = parser_args.ckpt_path + device = parser_args.device + + # stage 1: configurate the verbose flag + if not parser_args.verbose: + self.disable_task_loggers() + + # stage 2: read the input data and store them as a list + task_source = self.get_task_source(parser_args.input) + logger.info(f"task source: {task_source}") + + # stage 3: process the audio one by one + # we do action according the task type + task_result = OrderedDict() + has_exceptions = False + for id_, input_ in task_source.items(): + try: + # extract the speaker audio embedding + if parser_args.task == "spk": + logger.info("do vector spk task") + res = self(input_, model, sample_rate, config, ckpt_path, + device) + task_result[id_] = res + elif parser_args.task == "score": + logger.info("do vector score task") + logger.info(f"input content {input_}") + if len(input_.split()) != 2: + logger.error( + f"vector score task input {input_} wav num is not two," + "that is {len(input_.split())}") + sys.exit(-1) + + # get the enroll and test embedding + enroll_audio, test_audio = input_.split() + logger.info( + f"score task, enroll audio: {enroll_audio}, test audio: {test_audio}" + ) + enroll_embedding = self(enroll_audio, model, sample_rate, + config, ckpt_path, device) + test_embedding = self(test_audio, model, sample_rate, + config, ckpt_path, device) + + # get the score + res = self.get_embeddings_score(enroll_embedding, + test_embedding) + task_result[id_] = res + except Exception as e: + has_exceptions = True + task_result[id_] = f'{e.__class__.__name__}: {e}' + + logger.info("task result as follows: ") + logger.info(f"{task_result}") + + # stage 4: process the all the task results + self.process_task_results(parser_args.input, task_result, + parser_args.job_dump_result) + + # stage 5: return the exception flag + # if return False, somen audio process occurs error + if has_exceptions: + return False + else: + return True + + def _get_job_contents( + self, job_input: os.PathLike) -> Dict[str, Union[str, os.PathLike]]: + """ + Read a job input file and return its contents in a dictionary. + Refactor from the Executor._get_job_contents + + Args: + job_input (os.PathLike): The job input file. + + Returns: + Dict[str, str]: Contents of job input. + """ + job_contents = OrderedDict() + with open(job_input) as f: + for line in f: + line = line.strip() + if not line: + continue + k = line.split(' ')[0] + v = ' '.join(line.split(' ')[1:]) + job_contents[k] = v + return job_contents + + def get_embeddings_score(self, enroll_embedding, test_embedding): + """get the enroll embedding and test embedding score + + Args: + enroll_embedding (numpy.array): shape: (emb_size), enroll audio embedding + test_embedding (numpy.array): shape: (emb_size), test audio embedding + + Returns: + score: the score between enroll embedding and test embedding + """ + if not hasattr(self, "score_func"): + self.score_func = paddle.nn.CosineSimilarity(axis=0) + logger.info("create the cosine score function ") + + score = self.score_func( + paddle.to_tensor(enroll_embedding), + paddle.to_tensor(test_embedding)) + + return score.item() + + @stats_wrapper + def __call__(self, + audio_file: os.PathLike, + model: str='ecapatdnn_voxceleb12', + sample_rate: int=16000, + config: os.PathLike=None, + ckpt_path: os.PathLike=None, + device=paddle.get_device()): + """Extract the audio embedding + + Args: + audio_file (os.PathLike): audio path, + whose format must be wav and sample rate must be matched the model + model (str, optional): mode type, which is been loaded from the pretrained model list. + Defaults to 'ecapatdnn-voxceleb12'. + sample_rate (int, optional): model sample rate. Defaults to 16000. + config (os.PathLike, optional): yaml config. Defaults to None. + ckpt_path (os.PathLike, optional): pretrained model path. Defaults to None. + device (optional): paddle running host device. Defaults to paddle.get_device(). + + Returns: + dict: return the audio embedding and the embedding shape + """ + # stage 0: check the audio format + audio_file = os.path.abspath(audio_file) + if not self._check(audio_file, sample_rate): + sys.exit(-1) + + # stage 1: set the paddle runtime host device + logger.info(f"device type: {device}") + paddle.device.set_device(device) + + # stage 2: read the specific pretrained model + self._init_from_path(model, sample_rate, config, ckpt_path) + + # stage 3: preprocess the audio and get the audio feat + self.preprocess(model, audio_file) + + # stage 4: infer the model and get the audio embedding + self.infer(model) + + # stage 5: process the result and set them to output dict + res = self.postprocess() + + return res + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """get the neural network path from the pretrained model list + we stored all the pretained mode in the variable `pretrained_models` + + Args: + tag (str): model tag in the pretrained model list + + Returns: + os.PathLike: the downloaded pretrained model path in the disk + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, \ + 'The model "{}" you want to use has not been supported,'\ + 'please choose other models.\n' \ + 'The support models includes\n\t\t{}'.format(tag, "\n\t\t".join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + + return decompressed_path + + def _init_from_path(self, + model_type: str='ecapatdnn_voxceleb12', + sample_rate: int=16000, + cfg_path: Optional[os.PathLike]=None, + ckpt_path: Optional[os.PathLike]=None): + """Init the neural network from the model path + + Args: + model_type (str, optional): model tag in the pretrained model list. + Defaults to 'ecapatdnn_voxceleb12'. + sample_rate (int, optional): model sample rate. + Defaults to 16000. + cfg_path (Optional[os.PathLike], optional): yaml config file path. + Defaults to None. + ckpt_path (Optional[os.PathLike], optional): the pretrained model path, which is stored in the disk. + Defaults to None. + """ + # stage 0: avoid to init the mode again + if hasattr(self, "model"): + logger.info("Model has been initialized") + return + + # stage 1: get the model and config path + # if we want init the network from the model stored in the disk, + # we must pass the config path and the ckpt model path + if cfg_path is None or ckpt_path is None: + # get the mode from pretrained list + sample_rate_str = "16k" if sample_rate == 16000 else "8k" + tag = model_type + "-" + sample_rate_str + logger.info(f"load the pretrained model: {tag}") + # get the model from the pretrained list + # we download the pretrained model and store it in the res_path + res_path = self._get_pretrained_path(tag) + self.res_path = res_path + + self.cfg_path = os.path.join(res_path, + pretrained_models[tag]['cfg_path']) + self.ckpt_path = os.path.join( + res_path, pretrained_models[tag]['ckpt_path'] + '.pdparams') + else: + # get the model from disk + self.cfg_path = os.path.abspath(cfg_path) + self.ckpt_path = os.path.abspath(ckpt_path + ".pdparams") + self.res_path = os.path.dirname( + os.path.dirname(os.path.abspath(self.cfg_path))) + + logger.info(f"start to read the ckpt from {self.ckpt_path}") + logger.info(f"read the config from {self.cfg_path}") + logger.info(f"get the res path {self.res_path}") + + # stage 2: read and config and init the model body + self.config = CfgNode(new_allowed=True) + self.config.merge_from_file(self.cfg_path) + + # stage 3: get the model name to instance the model network with dynamic_import + logger.info("start to dynamic import the model class") + model_name = model_type[:model_type.rindex('_')] + logger.info(f"model name {model_name}") + model_class = dynamic_import(model_name, model_alias) + model_conf = self.config.model + backbone = model_class(**model_conf) + model = SpeakerIdetification( + backbone=backbone, num_class=self.config.num_speakers) + self.model = model + self.model.eval() + + # stage 4: load the model parameters + logger.info("start to set the model parameters to model") + model_dict = paddle.load(self.ckpt_path) + self.model.set_state_dict(model_dict) + + logger.info("create the model instance success") + + @paddle.no_grad() + def infer(self, model_type: str): + """Infer the model to get the embedding + + Args: + model_type (str): speaker verification model type + """ + # stage 0: get the feat and length from _inputs + feats = self._inputs["feats"] + lengths = self._inputs["lengths"] + logger.info("start to do backbone network model forward") + logger.info( + f"feats shape:{feats.shape}, lengths shape: {lengths.shape}") + + # stage 1: get the audio embedding + # embedding from (1, emb_size, 1) -> (emb_size) + embedding = self.model.backbone(feats, lengths).squeeze().numpy() + logger.info(f"embedding size: {embedding.shape}") + + # stage 2: put the embedding and dim info to _outputs property + # the embedding type is numpy.array + self._outputs["embedding"] = embedding + + def postprocess(self) -> Union[str, os.PathLike]: + """Return the audio embedding info + + Returns: + Union[str, os.PathLike]: audio embedding info + """ + embedding = self._outputs["embedding"] + return embedding + + def preprocess(self, model_type: str, input_file: Union[str, os.PathLike]): + """Extract the audio feat + + Args: + model_type (str): speaker verification model type + input_file (Union[str, os.PathLike]): audio file path + """ + audio_file = input_file + if isinstance(audio_file, (str, os.PathLike)): + logger.info(f"Preprocess audio file: {audio_file}") + + # stage 1: load the audio sample points + # Note: this process must match the training process + waveform, sr = load_audio(audio_file) + logger.info(f"load the audio sample points, shape is: {waveform.shape}") + + # stage 2: get the audio feat + # Note: Now we only support fbank feature + try: + feat = melspectrogram( + x=waveform, + sr=self.config.sr, + n_mels=self.config.n_mels, + window_size=self.config.window_size, + hop_length=self.config.hop_size) + logger.info(f"extract the audio feat, shape is: {feat.shape}") + except Exception as e: + logger.info(f"feat occurs exception {e}") + sys.exit(-1) + + feat = paddle.to_tensor(feat).unsqueeze(0) + # in inference period, the lengths is all one without padding + lengths = paddle.ones([1]) + + # stage 3: we do feature normalize, + # Now we assume that the feat must do normalize + feat = feature_normalize(feat, mean_norm=True, std_norm=False) + + # stage 4: store the feat and length in the _inputs, + # which will be used in other function + logger.info(f"feats shape: {feat.shape}") + self._inputs["feats"] = feat + self._inputs["lengths"] = lengths + + logger.info("audio extract the feat success") + + def _check(self, audio_file: str, sample_rate: int): + """Check if the model sample match the audio sample rate + + Args: + audio_file (str): audio file path, which will be extracted the embedding + sample_rate (int): the desired model sample rate + + Returns: + bool: return if the audio sample rate matches the model sample rate + """ + self.sample_rate = sample_rate + if self.sample_rate != 16000 and self.sample_rate != 8000: + logger.error( + "invalid sample rate, please input --sr 8000 or --sr 16000") + return False + + if isinstance(audio_file, (str, os.PathLike)): + if not os.path.isfile(audio_file): + logger.error("Please input the right audio file path") + return False + + logger.info("checking the aduio file format......") + try: + audio, audio_sample_rate = soundfile.read( + audio_file, dtype="float32", always_2d=True) + except Exception as e: + logger.exception(e) + logger.error( + "can not open the audio file, please check the audio file format is 'wav'. \n \ + you can try to use sox to change the file format.\n \ + For example: \n \ + sample rate: 16k \n \ + sox input_audio.xx --rate 16k --bits 16 --channels 1 output_audio.wav \n \ + sample rate: 8k \n \ + sox input_audio.xx --rate 8k --bits 16 --channels 1 output_audio.wav \n \ + ") + return False + + logger.info(f"The sample rate is {audio_sample_rate}") + + if audio_sample_rate != self.sample_rate: + logger.error("The sample rate of the input file is not {}.\n \ + The program will resample the wav file to {}.\n \ + If the result does not meet your expectations,\n \ + Please input the 16k 16 bit 1 channel wav file. \ + ".format(self.sample_rate, self.sample_rate)) + sys.exit(-1) + else: + logger.info("The audio file format is right") + + return True diff --git a/ernie-sat/paddlespeech/cls/__init__.py b/ernie-sat/paddlespeech/cls/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/cls/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/cls/exps/__init__.py b/ernie-sat/paddlespeech/cls/exps/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/cls/exps/panns/__init__.py b/ernie-sat/paddlespeech/cls/exps/panns/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/cls/exps/panns/deploy/__init__.py b/ernie-sat/paddlespeech/cls/exps/panns/deploy/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/deploy/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/cls/exps/panns/deploy/predict.py b/ernie-sat/paddlespeech/cls/exps/panns/deploy/predict.py new file mode 100644 index 0000000..d4e5c22 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/deploy/predict.py @@ -0,0 +1,145 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os + +import numpy as np +from paddle import inference +from scipy.special import softmax + +from paddleaudio.backends import load as load_audio +from paddleaudio.datasets import ESC50 +from paddleaudio.features import melspectrogram + +# yapf: disable +parser = argparse.ArgumentParser() +parser.add_argument("--model_dir", type=str, required=True, default="./export", help="The directory to static model.") +parser.add_argument('--device', choices=['cpu', 'gpu', 'xpu'], default="gpu", help="Select which device to train model, defaults to gpu.") +parser.add_argument("--wav", type=str, required=True, help="Audio file to infer.") +parser.add_argument("--batch_size", type=int, default=1, help="Batch size per GPU/CPU for training.") +parser.add_argument('--use_tensorrt', type=eval, default=False, choices=[True, False], help='Enable to use tensorrt to speed up.') +parser.add_argument("--precision", type=str, default="fp32", choices=["fp32", "fp16"], help='The tensorrt precision.') +parser.add_argument('--cpu_threads', type=int, default=10, help='Number of threads to predict when using cpu.') +parser.add_argument('--enable_mkldnn', type=eval, default=False, choices=[True, False], help='Enable to use mkldnn to speed up when using cpu.') +parser.add_argument("--log_dir", type=str, default="./log", help="The path to save log.") +args = parser.parse_args() +# yapf: enable + + +def extract_features(files: str, **kwargs): + waveforms = [] + srs = [] + max_length = float('-inf') + for file in files: + waveform, sr = load_audio(file, sr=None) + max_length = max(max_length, len(waveform)) + waveforms.append(waveform) + srs.append(sr) + + feats = [] + for i in range(len(waveforms)): + # padding + if len(waveforms[i]) < max_length: + pad_width = max_length - len(waveforms[i]) + waveforms[i] = np.pad(waveforms[i], pad_width=(0, pad_width)) + + feat = melspectrogram(waveforms[i], sr, **kwargs).transpose() + feats.append(feat) + + return np.stack(feats, axis=0) + + +class Predictor(object): + def __init__(self, + model_dir, + device="gpu", + batch_size=1, + use_tensorrt=False, + precision="fp32", + cpu_threads=10, + enable_mkldnn=False): + self.batch_size = batch_size + + model_file = os.path.join(model_dir, "inference.pdmodel") + params_file = os.path.join(model_dir, "inference.pdiparams") + + assert os.path.isfile(model_file) and os.path.isfile( + params_file), 'Please check model and parameter files.' + + config = inference.Config(model_file, params_file) + if device == "gpu": + # set GPU configs accordingly + # such as intialize the gpu memory, enable tensorrt + config.enable_use_gpu(100, 0) + precision_map = { + "fp16": inference.PrecisionType.Half, + "fp32": inference.PrecisionType.Float32, + } + precision_mode = precision_map[precision] + + if use_tensorrt: + config.enable_tensorrt_engine( + max_batch_size=batch_size, + min_subgraph_size=30, + precision_mode=precision_mode) + elif device == "cpu": + # set CPU configs accordingly, + # such as enable_mkldnn, set_cpu_math_library_num_threads + config.disable_gpu() + if enable_mkldnn: + # cache 10 different shapes for mkldnn to avoid memory leak + config.set_mkldnn_cache_capacity(10) + config.enable_mkldnn() + config.set_cpu_math_library_num_threads(cpu_threads) + elif device == "xpu": + # set XPU configs accordingly + config.enable_xpu(100) + + config.switch_use_feed_fetch_ops(False) + self.predictor = inference.create_predictor(config) + self.input_handles = [ + self.predictor.get_input_handle(name) + for name in self.predictor.get_input_names() + ] + self.output_handle = self.predictor.get_output_handle( + self.predictor.get_output_names()[0]) + + def predict(self, wavs): + feats = extract_features(wavs) + + self.input_handles[0].copy_from_cpu(feats) + self.predictor.run() + logits = self.output_handle.copy_to_cpu() + probs = softmax(logits, axis=1) + indices = np.argmax(probs, axis=1) + + return indices + + +if __name__ == "__main__": + # Define predictor to do prediction. + predictor = Predictor(args.model_dir, args.device, args.batch_size, + args.use_tensorrt, args.precision, args.cpu_threads, + args.enable_mkldnn) + + wavs = [args.wav] + + for i in range(len(wavs)): + wavs[i] = os.path.abspath(os.path.expanduser(wavs[i])) + assert os.path.isfile( + wavs[i]), f'Please check input wave file: {wavs[i]}' + + results = predictor.predict(wavs) + for idx, wav in enumerate(wavs): + print(f'Wav: {wav} \t Label: {ESC50.label_list[results[idx]]}') diff --git a/ernie-sat/paddlespeech/cls/exps/panns/export_model.py b/ernie-sat/paddlespeech/cls/exps/panns/export_model.py new file mode 100644 index 0000000..c295c6a --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/export_model.py @@ -0,0 +1,45 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os + +import paddle + +from paddleaudio.datasets import ESC50 +from paddlespeech.cls.models import cnn14 +from paddlespeech.cls.models import SoundClassifier + +# yapf: disable +parser = argparse.ArgumentParser(__doc__) +parser.add_argument("--checkpoint", type=str, required=True, help="Checkpoint of model.") +parser.add_argument("--output_dir", type=str, default='./export', help="Path to save static model and its parameters.") +args = parser.parse_args() +# yapf: enable + +if __name__ == '__main__': + model = SoundClassifier( + backbone=cnn14(pretrained=False, extract_embedding=True), + num_class=len(ESC50.label_list)) + model.set_state_dict(paddle.load(args.checkpoint)) + model.eval() + + model = paddle.jit.to_static( + model, + input_spec=[ + paddle.static.InputSpec( + shape=[None, None, 64], dtype=paddle.float32) + ]) + + # Save in static graph model. + paddle.jit.save(model, os.path.join(args.output_dir, "inference")) diff --git a/ernie-sat/paddlespeech/cls/exps/panns/predict.py b/ernie-sat/paddlespeech/cls/exps/panns/predict.py new file mode 100644 index 0000000..ffe42d3 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/predict.py @@ -0,0 +1,72 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os + +import paddle +import paddle.nn.functional as F +import yaml + +from paddleaudio.backends import load as load_audio +from paddleaudio.features import LogMelSpectrogram +from paddleaudio.utils import logger +from paddlespeech.cls.models import SoundClassifier +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +# yapf: disable +parser = argparse.ArgumentParser(__doc__) +parser.add_argument("--cfg_path", type=str, required=True) +args = parser.parse_args() +# yapf: enable + + +def extract_features(file: str, **feat_conf) -> paddle.Tensor: + file = os.path.abspath(os.path.expanduser(file)) + waveform, _ = load_audio(file, sr=feat_conf['sr']) + feature_extractor = LogMelSpectrogram(**feat_conf) + feat = feature_extractor(paddle.to_tensor(waveform).unsqueeze(0)) + feat = paddle.transpose(feat, [0, 2, 1]) + return feat + + +if __name__ == '__main__': + + args.cfg_path = os.path.abspath(os.path.expanduser(args.cfg_path)) + with open(args.cfg_path, 'r') as f: + config = yaml.safe_load(f) + + model_conf = config['model'] + data_conf = config['data'] + feat_conf = config['feature'] + predicting_conf = config['predicting'] + + ds_class = dynamic_import(data_conf['dataset']) + backbone_class = dynamic_import(model_conf['backbone']) + + model = SoundClassifier( + backbone=backbone_class(pretrained=False, extract_embedding=True), + num_class=len(ds_class.label_list)) + model.set_state_dict(paddle.load(predicting_conf['checkpoint'])) + model.eval() + + feat = extract_features(predicting_conf['audio_file'], **feat_conf) + logits = model(feat) + probs = F.softmax(logits, axis=1).numpy() + + sorted_indices = (-probs[0]).argsort() + + msg = f"[{predicting_conf['audio_file']}]\n" + for idx in sorted_indices[:predicting_conf['top_k']]: + msg += f'{ds_class.label_list[idx]}: {probs[0][idx]}\n' + logger.info(msg) diff --git a/ernie-sat/paddlespeech/cls/exps/panns/train.py b/ernie-sat/paddlespeech/cls/exps/panns/train.py new file mode 100644 index 0000000..7e29221 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/exps/panns/train.py @@ -0,0 +1,172 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os + +import paddle +import yaml + +from paddleaudio.features import LogMelSpectrogram +from paddleaudio.utils import logger +from paddleaudio.utils import Timer +from paddlespeech.cls.models import SoundClassifier +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +# yapf: disable +parser = argparse.ArgumentParser(__doc__) +parser.add_argument("--cfg_path", type=str, required=True) +args = parser.parse_args() +# yapf: enable + +if __name__ == "__main__": + nranks = paddle.distributed.get_world_size() + if paddle.distributed.get_world_size() > 1: + paddle.distributed.init_parallel_env() + local_rank = paddle.distributed.get_rank() + + args.cfg_path = os.path.abspath(os.path.expanduser(args.cfg_path)) + with open(args.cfg_path, 'r') as f: + config = yaml.safe_load(f) + + model_conf = config['model'] + data_conf = config['data'] + feat_conf = config['feature'] + training_conf = config['training'] + + # Dataset + ds_class = dynamic_import(data_conf['dataset']) + train_ds = ds_class(**data_conf['train']) + dev_ds = ds_class(**data_conf['dev']) + train_sampler = paddle.io.DistributedBatchSampler( + train_ds, + batch_size=training_conf['batch_size'], + shuffle=True, + drop_last=False) + train_loader = paddle.io.DataLoader( + train_ds, + batch_sampler=train_sampler, + num_workers=training_conf['num_workers'], + return_list=True, + use_buffer_reader=True, ) + + # Feature + feature_extractor = LogMelSpectrogram(**feat_conf) + + # Model + backbone_class = dynamic_import(model_conf['backbone']) + backbone = backbone_class(pretrained=True, extract_embedding=True) + model = SoundClassifier(backbone, num_class=data_conf['num_classes']) + model = paddle.DataParallel(model) + optimizer = paddle.optimizer.Adam( + learning_rate=training_conf['learning_rate'], + parameters=model.parameters()) + criterion = paddle.nn.loss.CrossEntropyLoss() + + steps_per_epoch = len(train_sampler) + timer = Timer(steps_per_epoch * training_conf['epochs']) + timer.start() + + for epoch in range(1, training_conf['epochs'] + 1): + model.train() + + avg_loss = 0 + num_corrects = 0 + num_samples = 0 + for batch_idx, batch in enumerate(train_loader): + waveforms, labels = batch + feats = feature_extractor( + waveforms + ) # Need a padding when lengths of waveforms differ in a batch. + feats = paddle.transpose(feats, [0, 2, 1]) # To [N, length, n_mels] + + logits = model(feats) + + loss = criterion(logits, labels) + loss.backward() + optimizer.step() + if isinstance(optimizer._learning_rate, + paddle.optimizer.lr.LRScheduler): + optimizer._learning_rate.step() + optimizer.clear_grad() + + # Calculate loss + avg_loss += loss.numpy()[0] + + # Calculate metrics + preds = paddle.argmax(logits, axis=1) + num_corrects += (preds == labels).numpy().sum() + num_samples += feats.shape[0] + + timer.count() + + if (batch_idx + 1 + ) % training_conf['log_freq'] == 0 and local_rank == 0: + lr = optimizer.get_lr() + avg_loss /= training_conf['log_freq'] + avg_acc = num_corrects / num_samples + + print_msg = 'Epoch={}/{}, Step={}/{}'.format( + epoch, training_conf['epochs'], batch_idx + 1, + steps_per_epoch) + print_msg += ' loss={:.4f}'.format(avg_loss) + print_msg += ' acc={:.4f}'.format(avg_acc) + print_msg += ' lr={:.6f} step/sec={:.2f} | ETA {}'.format( + lr, timer.timing, timer.eta) + logger.train(print_msg) + + avg_loss = 0 + num_corrects = 0 + num_samples = 0 + + if epoch % training_conf[ + 'save_freq'] == 0 and batch_idx + 1 == steps_per_epoch and local_rank == 0: + dev_sampler = paddle.io.BatchSampler( + dev_ds, + batch_size=training_conf['batch_size'], + shuffle=False, + drop_last=False) + dev_loader = paddle.io.DataLoader( + dev_ds, + batch_sampler=dev_sampler, + num_workers=training_conf['num_workers'], + return_list=True, ) + + model.eval() + num_corrects = 0 + num_samples = 0 + with logger.processing('Evaluation on validation dataset'): + for batch_idx, batch in enumerate(dev_loader): + waveforms, labels = batch + feats = feature_extractor(waveforms) + feats = paddle.transpose(feats, [0, 2, 1]) + + logits = model(feats) + + preds = paddle.argmax(logits, axis=1) + num_corrects += (preds == labels).numpy().sum() + num_samples += feats.shape[0] + + print_msg = '[Evaluation result]' + print_msg += ' dev_acc={:.4f}'.format(num_corrects / num_samples) + + logger.eval(print_msg) + + # Save model + save_dir = os.path.join(training_conf['checkpoint_dir'], + 'epoch_{}'.format(epoch)) + logger.info('Saving model checkpoint to {}'.format(save_dir)) + paddle.save(model.state_dict(), + os.path.join(save_dir, 'model.pdparams')) + paddle.save(optimizer.state_dict(), + os.path.join(save_dir, 'model.pdopt')) diff --git a/ernie-sat/paddlespeech/cls/models/__init__.py b/ernie-sat/paddlespeech/cls/models/__init__.py new file mode 100644 index 0000000..4bfadda --- /dev/null +++ b/ernie-sat/paddlespeech/cls/models/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .panns import * diff --git a/ernie-sat/paddlespeech/cls/models/panns/__init__.py b/ernie-sat/paddlespeech/cls/models/panns/__init__.py new file mode 100644 index 0000000..638f772 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/models/panns/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .classifier import * +from .panns import * diff --git a/ernie-sat/paddlespeech/cls/models/panns/classifier.py b/ernie-sat/paddlespeech/cls/models/panns/classifier.py new file mode 100644 index 0000000..df64158 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/models/panns/classifier.py @@ -0,0 +1,36 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle.nn as nn + + +class SoundClassifier(nn.Layer): + """ + Model for sound classification which uses panns pretrained models to extract + embeddings from audio files. + """ + + def __init__(self, backbone, num_class, dropout=0.1): + super(SoundClassifier, self).__init__() + self.backbone = backbone + self.dropout = nn.Dropout(dropout) + self.fc = nn.Linear(self.backbone.emb_size, num_class) + + def forward(self, x): + # x: (batch_size, num_frames, num_melbins) -> (batch_size, 1, num_frames, num_melbins) + x = x.unsqueeze(1) + x = self.backbone(x) + x = self.dropout(x) + logits = self.fc(x) + + return logits diff --git a/ernie-sat/paddlespeech/cls/models/panns/panns.py b/ernie-sat/paddlespeech/cls/models/panns/panns.py new file mode 100644 index 0000000..6d2dac5 --- /dev/null +++ b/ernie-sat/paddlespeech/cls/models/panns/panns.py @@ -0,0 +1,309 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import paddle.nn as nn +import paddle.nn.functional as F + +from paddleaudio.utils.download import load_state_dict_from_url +from paddleaudio.utils.env import MODEL_HOME + +__all__ = ['CNN14', 'CNN10', 'CNN6', 'cnn14', 'cnn10', 'cnn6'] + +pretrained_model_urls = { + 'cnn14': 'https://bj.bcebos.com/paddleaudio/models/panns_cnn14.pdparams', + 'cnn10': 'https://bj.bcebos.com/paddleaudio/models/panns_cnn10.pdparams', + 'cnn6': 'https://bj.bcebos.com/paddleaudio/models/panns_cnn6.pdparams', +} + + +class ConvBlock(nn.Layer): + def __init__(self, in_channels, out_channels): + super(ConvBlock, self).__init__() + + self.conv1 = nn.Conv2D( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=(3, 3), + stride=(1, 1), + padding=(1, 1), + bias_attr=False) + self.conv2 = nn.Conv2D( + in_channels=out_channels, + out_channels=out_channels, + kernel_size=(3, 3), + stride=(1, 1), + padding=(1, 1), + bias_attr=False) + self.bn1 = nn.BatchNorm2D(out_channels) + self.bn2 = nn.BatchNorm2D(out_channels) + + def forward(self, x, pool_size=(2, 2), pool_type='avg'): + x = self.conv1(x) + x = self.bn1(x) + x = F.relu(x) + + x = self.conv2(x) + x = self.bn2(x) + x = F.relu(x) + + if pool_type == 'max': + x = F.max_pool2d(x, kernel_size=pool_size) + elif pool_type == 'avg': + x = F.avg_pool2d(x, kernel_size=pool_size) + elif pool_type == 'avg+max': + x = F.avg_pool2d( + x, kernel_size=pool_size) + F.max_pool2d( + x, kernel_size=pool_size) + else: + raise Exception( + f'Pooling type of {pool_type} is not supported. It must be one of "max", "avg" and "avg+max".' + ) + return x + + +class ConvBlock5x5(nn.Layer): + def __init__(self, in_channels, out_channels): + super(ConvBlock5x5, self).__init__() + + self.conv1 = nn.Conv2D( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=(5, 5), + stride=(1, 1), + padding=(2, 2), + bias_attr=False) + self.bn1 = nn.BatchNorm2D(out_channels) + + def forward(self, x, pool_size=(2, 2), pool_type='avg'): + x = self.conv1(x) + x = self.bn1(x) + x = F.relu(x) + + if pool_type == 'max': + x = F.max_pool2d(x, kernel_size=pool_size) + elif pool_type == 'avg': + x = F.avg_pool2d(x, kernel_size=pool_size) + elif pool_type == 'avg+max': + x = F.avg_pool2d( + x, kernel_size=pool_size) + F.max_pool2d( + x, kernel_size=pool_size) + else: + raise Exception( + f'Pooling type of {pool_type} is not supported. It must be one of "max", "avg" and "avg+max".' + ) + return x + + +class CNN14(nn.Layer): + """ + The CNN14(14-layer CNNs) mainly consist of 6 convolutional blocks while each convolutional + block consists of 2 convolutional layers with a kernel size of 3 × 3. + + Reference: + PANNs: Large-Scale Pretrained Audio Neural Networks for Audio Pattern Recognition + https://arxiv.org/pdf/1912.10211.pdf + """ + emb_size = 2048 + + def __init__(self, extract_embedding: bool=True): + + super(CNN14, self).__init__() + self.bn0 = nn.BatchNorm2D(64) + self.conv_block1 = ConvBlock(in_channels=1, out_channels=64) + self.conv_block2 = ConvBlock(in_channels=64, out_channels=128) + self.conv_block3 = ConvBlock(in_channels=128, out_channels=256) + self.conv_block4 = ConvBlock(in_channels=256, out_channels=512) + self.conv_block5 = ConvBlock(in_channels=512, out_channels=1024) + self.conv_block6 = ConvBlock(in_channels=1024, out_channels=2048) + + self.fc1 = nn.Linear(2048, self.emb_size) + self.fc_audioset = nn.Linear(self.emb_size, 527) + self.extract_embedding = extract_embedding + + def forward(self, x): + x.stop_gradient = False + x = x.transpose([0, 3, 2, 1]) + x = self.bn0(x) + x = x.transpose([0, 3, 2, 1]) + + x = self.conv_block1(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block2(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block3(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block4(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block5(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block6(x, pool_size=(1, 1), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = x.mean(axis=3) + x = x.max(axis=2) + x.mean(axis=2) + + x = F.dropout(x, p=0.5, training=self.training) + x = F.relu(self.fc1(x)) + + if self.extract_embedding: + output = F.dropout(x, p=0.5, training=self.training) + else: + output = F.sigmoid(self.fc_audioset(x)) + return output + + +class CNN10(nn.Layer): + """ + The CNN10(14-layer CNNs) mainly consist of 4 convolutional blocks while each convolutional + block consists of 2 convolutional layers with a kernel size of 3 × 3. + + Reference: + PANNs: Large-Scale Pretrained Audio Neural Networks for Audio Pattern Recognition + https://arxiv.org/pdf/1912.10211.pdf + """ + emb_size = 512 + + def __init__(self, extract_embedding: bool=True): + + super(CNN10, self).__init__() + self.bn0 = nn.BatchNorm2D(64) + self.conv_block1 = ConvBlock(in_channels=1, out_channels=64) + self.conv_block2 = ConvBlock(in_channels=64, out_channels=128) + self.conv_block3 = ConvBlock(in_channels=128, out_channels=256) + self.conv_block4 = ConvBlock(in_channels=256, out_channels=512) + + self.fc1 = nn.Linear(512, self.emb_size) + self.fc_audioset = nn.Linear(self.emb_size, 527) + self.extract_embedding = extract_embedding + + def forward(self, x): + x.stop_gradient = False + x = x.transpose([0, 3, 2, 1]) + x = self.bn0(x) + x = x.transpose([0, 3, 2, 1]) + + x = self.conv_block1(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block2(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block3(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block4(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = x.mean(axis=3) + x = x.max(axis=2) + x.mean(axis=2) + + x = F.dropout(x, p=0.5, training=self.training) + x = F.relu(self.fc1(x)) + + if self.extract_embedding: + output = F.dropout(x, p=0.5, training=self.training) + else: + output = F.sigmoid(self.fc_audioset(x)) + return output + + +class CNN6(nn.Layer): + """ + The CNN14(14-layer CNNs) mainly consist of 4 convolutional blocks while each convolutional + block consists of 1 convolutional layers with a kernel size of 5 × 5. + + Reference: + PANNs: Large-Scale Pretrained Audio Neural Networks for Audio Pattern Recognition + https://arxiv.org/pdf/1912.10211.pdf + """ + emb_size = 512 + + def __init__(self, extract_embedding: bool=True): + + super(CNN6, self).__init__() + self.bn0 = nn.BatchNorm2D(64) + self.conv_block1 = ConvBlock5x5(in_channels=1, out_channels=64) + self.conv_block2 = ConvBlock5x5(in_channels=64, out_channels=128) + self.conv_block3 = ConvBlock5x5(in_channels=128, out_channels=256) + self.conv_block4 = ConvBlock5x5(in_channels=256, out_channels=512) + + self.fc1 = nn.Linear(512, self.emb_size) + self.fc_audioset = nn.Linear(self.emb_size, 527) + self.extract_embedding = extract_embedding + + def forward(self, x): + x.stop_gradient = False + x = x.transpose([0, 3, 2, 1]) + x = self.bn0(x) + x = x.transpose([0, 3, 2, 1]) + + x = self.conv_block1(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block2(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block3(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = self.conv_block4(x, pool_size=(2, 2), pool_type='avg') + x = F.dropout(x, p=0.2, training=self.training) + + x = x.mean(axis=3) + x = x.max(axis=2) + x.mean(axis=2) + + x = F.dropout(x, p=0.5, training=self.training) + x = F.relu(self.fc1(x)) + + if self.extract_embedding: + output = F.dropout(x, p=0.5, training=self.training) + else: + output = F.sigmoid(self.fc_audioset(x)) + return output + + +def cnn14(pretrained: bool=False, extract_embedding: bool=True) -> CNN14: + model = CNN14(extract_embedding=extract_embedding) + if pretrained: + state_dict = load_state_dict_from_url( + url=pretrained_model_urls['cnn14'], + path=os.path.join(MODEL_HOME, 'panns')) + model.set_state_dict(state_dict) + return model + + +def cnn10(pretrained: bool=False, extract_embedding: bool=True) -> CNN10: + model = CNN10(extract_embedding=extract_embedding) + if pretrained: + state_dict = load_state_dict_from_url( + url=pretrained_model_urls['cnn10'], + path=os.path.join(MODEL_HOME, 'panns')) + model.set_state_dict(state_dict) + return model + + +def cnn6(pretrained: bool=False, extract_embedding: bool=True) -> CNN6: + model = CNN6(extract_embedding=extract_embedding) + if pretrained: + state_dict = load_state_dict_from_url( + url=pretrained_model_urls['cnn6'], + path=os.path.join(MODEL_HOME, 'panns')) + model.set_state_dict(state_dict) + return model diff --git a/ernie-sat/paddlespeech/s2t/__init__.py b/ernie-sat/paddlespeech/s2t/__init__.py new file mode 100644 index 0000000..855ceef --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/__init__.py @@ -0,0 +1,507 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any +from typing import List +from typing import Tuple +from typing import Union + +import paddle +from paddle import nn +from paddle.fluid import core +from paddle.nn import functional as F + +from paddlespeech.s2t.utils.log import Log + +#TODO(Hui Zhang): remove fluid import +logger = Log(__name__).getlog() + +########### hcak logging ############# +logger.warn = logger.warning + +########### hcak paddle ############# +paddle.half = 'float16' +paddle.float = 'float32' +paddle.double = 'float64' +paddle.short = 'int16' +paddle.int = 'int32' +paddle.long = 'int64' +paddle.uint16 = 'uint16' +paddle.cdouble = 'complex128' + + +def convert_dtype_to_string(tensor_dtype): + """ + Convert the data type in numpy to the data type in Paddle + Args: + tensor_dtype(core.VarDesc.VarType): the data type in numpy. + Returns: + core.VarDesc.VarType: the data type in Paddle. + """ + dtype = tensor_dtype + if dtype == core.VarDesc.VarType.FP32: + return paddle.float32 + elif dtype == core.VarDesc.VarType.FP64: + return paddle.float64 + elif dtype == core.VarDesc.VarType.FP16: + return paddle.float16 + elif dtype == core.VarDesc.VarType.INT32: + return paddle.int32 + elif dtype == core.VarDesc.VarType.INT16: + return paddle.int16 + elif dtype == core.VarDesc.VarType.INT64: + return paddle.int64 + elif dtype == core.VarDesc.VarType.BOOL: + return paddle.bool + elif dtype == core.VarDesc.VarType.BF16: + # since there is still no support for bfloat16 in NumPy, + # uint16 is used for casting bfloat16 + return paddle.uint16 + elif dtype == core.VarDesc.VarType.UINT8: + return paddle.uint8 + elif dtype == core.VarDesc.VarType.INT8: + return paddle.int8 + elif dtype == core.VarDesc.VarType.COMPLEX64: + return paddle.complex64 + elif dtype == core.VarDesc.VarType.COMPLEX128: + return paddle.complex128 + else: + raise ValueError("Not supported tensor dtype %s" % dtype) + + +if not hasattr(paddle, 'softmax'): + logger.debug("register user softmax to paddle, remove this when fixed!") + setattr(paddle, 'softmax', paddle.nn.functional.softmax) + +if not hasattr(paddle, 'log_softmax'): + logger.debug("register user log_softmax to paddle, remove this when fixed!") + setattr(paddle, 'log_softmax', paddle.nn.functional.log_softmax) + +if not hasattr(paddle, 'sigmoid'): + logger.debug("register user sigmoid to paddle, remove this when fixed!") + setattr(paddle, 'sigmoid', paddle.nn.functional.sigmoid) + +if not hasattr(paddle, 'log_sigmoid'): + logger.debug("register user log_sigmoid to paddle, remove this when fixed!") + setattr(paddle, 'log_sigmoid', paddle.nn.functional.log_sigmoid) + +if not hasattr(paddle, 'relu'): + logger.debug("register user relu to paddle, remove this when fixed!") + setattr(paddle, 'relu', paddle.nn.functional.relu) + + +def cat(xs, dim=0): + return paddle.concat(xs, axis=dim) + + +if not hasattr(paddle, 'cat'): + logger.debug( + "override cat of paddle if exists or register, remove this when fixed!") + paddle.cat = cat + + +########### hcak paddle.Tensor ############# +def item(x: paddle.Tensor): + return x.numpy().item() + + +if not hasattr(paddle.Tensor, 'item'): + logger.debug( + "override item of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.item = item + + +def func_long(x: paddle.Tensor): + return paddle.cast(x, paddle.long) + + +if not hasattr(paddle.Tensor, 'long'): + logger.debug( + "override long of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.long = func_long + +if not hasattr(paddle.Tensor, 'numel'): + logger.debug( + "override numel of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.numel = paddle.numel + + +def new_full(x: paddle.Tensor, + size: Union[List[int], Tuple[int], paddle.Tensor], + fill_value: Union[float, int, bool, paddle.Tensor], + dtype=None): + return paddle.full(size, fill_value, dtype=x.dtype) + + +if not hasattr(paddle.Tensor, 'new_full'): + logger.debug( + "override new_full of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.new_full = new_full + + +def eq(xs: paddle.Tensor, ys: Union[paddle.Tensor, float]) -> paddle.Tensor: + if convert_dtype_to_string(xs.dtype) == paddle.bool: + xs = xs.astype(paddle.int) + return xs.equal( + paddle.to_tensor( + ys, dtype=convert_dtype_to_string(xs.dtype), place=xs.place)) + + +if not hasattr(paddle.Tensor, 'eq'): + logger.debug( + "override eq of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.eq = eq + +if not hasattr(paddle, 'eq'): + logger.debug( + "override eq of paddle if exists or register, remove this when fixed!") + paddle.eq = eq + + +def contiguous(xs: paddle.Tensor) -> paddle.Tensor: + return xs + + +if not hasattr(paddle.Tensor, 'contiguous'): + logger.debug( + "override contiguous of paddle.Tensor if exists or register, remove this when fixed!" + ) + paddle.Tensor.contiguous = contiguous + + +def size(xs: paddle.Tensor, *args: int) -> paddle.Tensor: + nargs = len(args) + assert (nargs <= 1) + s = paddle.shape(xs) + if nargs == 1: + return s[args[0]] + else: + return s + + +#`to_static` do not process `size` property, maybe some `paddle` api dependent on it. +logger.debug( + "override size of paddle.Tensor " + "(`to_static` do not process `size` property, maybe some `paddle` api dependent on it), remove this when fixed!" +) +paddle.Tensor.size = size + + +def view(xs: paddle.Tensor, *args: int) -> paddle.Tensor: + return xs.reshape(args) + + +if not hasattr(paddle.Tensor, 'view'): + logger.debug("register user view to paddle.Tensor, remove this when fixed!") + paddle.Tensor.view = view + + +def view_as(xs: paddle.Tensor, ys: paddle.Tensor) -> paddle.Tensor: + return xs.reshape(ys.size()) + + +if not hasattr(paddle.Tensor, 'view_as'): + logger.debug( + "register user view_as to paddle.Tensor, remove this when fixed!") + paddle.Tensor.view_as = view_as + + +def is_broadcastable(shp1, shp2): + for a, b in zip(shp1[::-1], shp2[::-1]): + if a == 1 or b == 1 or a == b: + pass + else: + return False + return True + + +def masked_fill(xs: paddle.Tensor, + mask: paddle.Tensor, + value: Union[float, int]): + assert is_broadcastable(xs.shape, mask.shape) is True, (xs.shape, + mask.shape) + bshape = paddle.broadcast_shape(xs.shape, mask.shape) + mask = mask.broadcast_to(bshape) + trues = paddle.ones_like(xs) * value + xs = paddle.where(mask, trues, xs) + return xs + + +if not hasattr(paddle.Tensor, 'masked_fill'): + logger.debug( + "register user masked_fill to paddle.Tensor, remove this when fixed!") + paddle.Tensor.masked_fill = masked_fill + + +def masked_fill_(xs: paddle.Tensor, + mask: paddle.Tensor, + value: Union[float, int]) -> paddle.Tensor: + assert is_broadcastable(xs.shape, mask.shape) is True + bshape = paddle.broadcast_shape(xs.shape, mask.shape) + mask = mask.broadcast_to(bshape) + trues = paddle.ones_like(xs) * value + ret = paddle.where(mask, trues, xs) + paddle.assign(ret.detach(), output=xs) + return xs + + +if not hasattr(paddle.Tensor, 'masked_fill_'): + logger.debug( + "register user masked_fill_ to paddle.Tensor, remove this when fixed!") + paddle.Tensor.masked_fill_ = masked_fill_ + + +def fill_(xs: paddle.Tensor, value: Union[float, int]) -> paddle.Tensor: + val = paddle.full_like(xs, value) + paddle.assign(val.detach(), output=xs) + return xs + + +if not hasattr(paddle.Tensor, 'fill_'): + logger.debug( + "register user fill_ to paddle.Tensor, remove this when fixed!") + paddle.Tensor.fill_ = fill_ + + +def repeat(xs: paddle.Tensor, *size: Any) -> paddle.Tensor: + return paddle.tile(xs, size) + + +if not hasattr(paddle.Tensor, 'repeat'): + logger.debug( + "register user repeat to paddle.Tensor, remove this when fixed!") + paddle.Tensor.repeat = repeat + +if not hasattr(paddle.Tensor, 'softmax'): + logger.debug( + "register user softmax to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'softmax', paddle.nn.functional.softmax) + +if not hasattr(paddle.Tensor, 'sigmoid'): + logger.debug( + "register user sigmoid to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'sigmoid', paddle.nn.functional.sigmoid) + +if not hasattr(paddle.Tensor, 'relu'): + logger.debug("register user relu to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'relu', paddle.nn.functional.relu) + + +def type_as(x: paddle.Tensor, other: paddle.Tensor) -> paddle.Tensor: + return x.astype(other.dtype) + + +if not hasattr(paddle.Tensor, 'type_as'): + logger.debug( + "register user type_as to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'type_as', type_as) + + +def to(x: paddle.Tensor, *args, **kwargs) -> paddle.Tensor: + assert len(args) == 1 + if isinstance(args[0], str): # dtype + return x.astype(args[0]) + elif isinstance(args[0], paddle.Tensor): # Tensor + return x.astype(args[0].dtype) + else: # Device + return x + + +if not hasattr(paddle.Tensor, 'to'): + logger.debug("register user to to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'to', to) + + +def func_float(x: paddle.Tensor) -> paddle.Tensor: + return x.astype(paddle.float) + + +if not hasattr(paddle.Tensor, 'float'): + logger.debug( + "register user float to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'float', func_float) + + +def func_int(x: paddle.Tensor) -> paddle.Tensor: + return x.astype(paddle.int) + + +if not hasattr(paddle.Tensor, 'int'): + logger.debug("register user int to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'int', func_int) + + +def tolist(x: paddle.Tensor) -> List[Any]: + return x.numpy().tolist() + + +if not hasattr(paddle.Tensor, 'tolist'): + logger.debug( + "register user tolist to paddle.Tensor, remove this when fixed!") + setattr(paddle.Tensor, 'tolist', tolist) + +########### hack paddle.nn ############# +from paddle.nn import Layer +from typing import Optional +from typing import Mapping +from typing import Iterable +from typing import Tuple +from typing import Iterator +from collections import OrderedDict, abc as container_abcs + + +class LayerDict(paddle.nn.Layer): + r"""Holds submodules in a dictionary. + + :class:`~paddle.nn.LayerDict` can be indexed like a regular Python dictionary, + but modules it contains are properly registered, and will be visible by all + :class:`~paddle.nn.Layer` methods. + + :class:`~paddle.nn.LayerDict` is an **ordered** dictionary that respects + + * the order of insertion, and + + * in :meth:`~paddle.nn.LayerDict.update`, the order of the merged + ``OrderedDict``, ``dict`` (started from Python 3.6) or another + :class:`~paddle.nn.LayerDict` (the argument to + :meth:`~paddle.nn.LayerDict.update`). + + Note that :meth:`~paddle.nn.LayerDict.update` with other unordered mapping + types (e.g., Python's plain ``dict`` before Python version 3.6) does not + preserve the order of the merged mapping. + + Args: + modules (iterable, optional): a mapping (dictionary) of (string: module) + or an iterable of key-value pairs of type (string, module) + + Example:: + + class MyModule(nn.Layer): + def __init__(self): + super(MyModule, self).__init__() + self.choices = nn.LayerDict({ + 'conv': nn.Conv2d(10, 10, 3), + 'pool': nn.MaxPool2d(3) + }) + self.activations = nn.LayerDict([ + ['lrelu', nn.LeakyReLU()], + ['prelu', nn.PReLU()] + ]) + + def forward(self, x, choice, act): + x = self.choices[choice](x) + x = self.activations[act](x) + return x + """ + + def __init__(self, modules: Optional[Mapping[str, Layer]]=None) -> None: + super(LayerDict, self).__init__() + if modules is not None: + self.update(modules) + + def __getitem__(self, key: str) -> Layer: + return self._modules[key] + + def __setitem__(self, key: str, module: Layer) -> None: + self.add_module(key, module) + + def __delitem__(self, key: str) -> None: + del self._modules[key] + + def __len__(self) -> int: + return len(self._modules) + + def __iter__(self) -> Iterator[str]: + return iter(self._modules) + + def __contains__(self, key: str) -> bool: + return key in self._modules + + def clear(self) -> None: + """Remove all items from the LayerDict. + """ + self._modules.clear() + + def pop(self, key: str) -> Layer: + r"""Remove key from the LayerDict and return its module. + + Args: + key (string): key to pop from the LayerDict + """ + v = self[key] + del self[key] + return v + + def keys(self) -> Iterable[str]: + r"""Return an iterable of the LayerDict keys. + """ + return self._modules.keys() + + def items(self) -> Iterable[Tuple[str, Layer]]: + r"""Return an iterable of the LayerDict key/value pairs. + """ + return self._modules.items() + + def values(self) -> Iterable[Layer]: + r"""Return an iterable of the LayerDict values. + """ + return self._modules.values() + + def update(self, modules: Mapping[str, Layer]) -> None: + r"""Update the :class:`~paddle.nn.LayerDict` with the key-value pairs from a + mapping or an iterable, overwriting existing keys. + + .. note:: + If :attr:`modules` is an ``OrderedDict``, a :class:`~paddle.nn.LayerDict`, or + an iterable of key-value pairs, the order of new elements in it is preserved. + + Args: + modules (iterable): a mapping (dictionary) from string to :class:`~paddle.nn.Layer`, + or an iterable of key-value pairs of type (string, :class:`~paddle.nn.Layer`) + """ + if not isinstance(modules, container_abcs.Iterable): + raise TypeError("LayerDict.update should be called with an " + "iterable of key/value pairs, but got " + type( + modules).__name__) + + if isinstance(modules, + (OrderedDict, LayerDict, container_abcs.Mapping)): + for key, module in modules.items(): + self[key] = module + else: + # modules here can be a list with two items + for j, m in enumerate(modules): + if not isinstance(m, container_abcs.Iterable): + raise TypeError("LayerDict update sequence element " + "#" + str(j) + " should be Iterable; is" + + type(m).__name__) + if not len(m) == 2: + raise ValueError("LayerDict update sequence element " + "#" + str(j) + " has length " + str( + len(m)) + "; 2 is required") + # modules can be Mapping (what it's typed at), or a list: [(name1, module1), (name2, module2)] + # that's too cumbersome to type correctly with overloads, so we add an ignore here + self[m[0]] = m[1] # type: ignore[assignment] + + # remove forward alltogether to fallback on Module's _forward_unimplemented + + +if not hasattr(paddle.nn, 'LayerDict'): + logger.debug( + "register user LayerDict to paddle.nn, remove this when fixed!") + setattr(paddle.nn, 'LayerDict', LayerDict) diff --git a/ernie-sat/paddlespeech/s2t/decoders/README.md b/ernie-sat/paddlespeech/s2t/decoders/README.md new file mode 100644 index 0000000..0b91ddd --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/README.md @@ -0,0 +1,14 @@ +# Decoders +we borrow a lot of code from Espnet Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + +## Reference +### CTC Prefix Beam Search +* [Sequence Modeling With CTC](https://distill.pub/2017/ctc/) +* [First-Pass Large Vocabulary Continuous Speech Recognition using Bi-Directional Recurrent DNNs](https://arxiv.org/pdf/1408.2873.pdf) + +### CTC Prefix Score & Join CTC/ATT One-passing Decoding +* [Hybrid CTC/Attention Architecture for End-to-End Speech Recognition](http://www.ifp.illinois.edu/speech/speech_web_lg/slides/2019/watanabe_hybridCTCAttention_2017.pdf) +* [Vectorized Beam Search for CTC-Attention-based Speech Recognition](https://www.isca-speech.org/archive/pdfs/interspeech_2019/seki19b_interspeech.pdf) + +### Streaming Join CTC/ATT Beam Search +* [STREAMING TRANSFORMER ASR WITH BLOCKWISE SYNCHRONOUS BEAM SEARCH](https://arxiv.org/abs/2006.14941) diff --git a/ernie-sat/paddlespeech/s2t/decoders/__init__.py b/ernie-sat/paddlespeech/s2t/decoders/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/decoders/beam_search/__init__.py b/ernie-sat/paddlespeech/s2t/decoders/beam_search/__init__.py new file mode 100644 index 0000000..79a1e9d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/beam_search/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .batch_beam_search import BatchBeamSearch +from .beam_search import beam_search +from .beam_search import BeamSearch +from .beam_search import Hypothesis diff --git a/ernie-sat/paddlespeech/s2t/decoders/beam_search/batch_beam_search.py b/ernie-sat/paddlespeech/s2t/decoders/beam_search/batch_beam_search.py new file mode 100644 index 0000000..ed9790c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/beam_search/batch_beam_search.py @@ -0,0 +1,18 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference espnet Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + + +class BatchBeamSearch(): + pass diff --git a/ernie-sat/paddlespeech/s2t/decoders/beam_search/beam_search.py b/ernie-sat/paddlespeech/s2t/decoders/beam_search/beam_search.py new file mode 100644 index 0000000..f331cb1 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/beam_search/beam_search.py @@ -0,0 +1,531 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Beam search module.""" +from itertools import chain +from typing import Any +from typing import Dict +from typing import List +from typing import NamedTuple +from typing import Tuple +from typing import Union + +import paddle + +from ..scorers.scorer_interface import PartialScorerInterface +from ..scorers.scorer_interface import ScorerInterface +from ..utils import end_detect +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + + +class Hypothesis(NamedTuple): + """Hypothesis data type.""" + + yseq: paddle.Tensor # (T,) + score: Union[float, paddle.Tensor] = 0 + scores: Dict[str, Union[float, paddle.Tensor]] = dict() + states: Dict[str, Any] = dict() + + def asdict(self) -> dict: + """Convert data to JSON-friendly dict.""" + return self._replace( + yseq=self.yseq.tolist(), + score=float(self.score), + scores={k: float(v) + for k, v in self.scores.items()}, )._asdict() + + +class BeamSearch(paddle.nn.Layer): + """Beam search implementation.""" + + def __init__( + self, + scorers: Dict[str, ScorerInterface], + weights: Dict[str, float], + beam_size: int, + vocab_size: int, + sos: int, + eos: int, + token_list: List[str]=None, + pre_beam_ratio: float=1.5, + pre_beam_score_key: str=None, ): + """Initialize beam search. + + Args: + scorers (dict[str, ScorerInterface]): Dict of decoder modules + e.g., Decoder, CTCPrefixScorer, LM + The scorer will be ignored if it is `None` + weights (dict[str, float]): Dict of weights for each scorers + The scorer will be ignored if its weight is 0 + beam_size (int): The number of hypotheses kept during search + vocab_size (int): The number of vocabulary + sos (int): Start of sequence id + eos (int): End of sequence id + token_list (list[str]): List of tokens for debug log + pre_beam_score_key (str): key of scores to perform pre-beam search + pre_beam_ratio (float): beam size in the pre-beam search + will be `int(pre_beam_ratio * beam_size)` + + """ + super().__init__() + # set scorers + self.weights = weights + self.scorers = dict() # all = full + partial + self.full_scorers = dict() # full tokens + self.part_scorers = dict() # partial tokens + # this module dict is required for recursive cast + # `self.to(device, dtype)` in `recog.py` + self.nn_dict = paddle.nn.LayerDict() # nn.Layer + for k, v in scorers.items(): + w = weights.get(k, 0) + if w == 0 or v is None: + continue + assert isinstance( + v, ScorerInterface + ), f"{k} ({type(v)}) does not implement ScorerInterface" + self.scorers[k] = v + if isinstance(v, PartialScorerInterface): + self.part_scorers[k] = v + else: + self.full_scorers[k] = v + if isinstance(v, paddle.nn.Layer): + self.nn_dict[k] = v + + # set configurations + self.sos = sos + self.eos = eos + self.token_list = token_list + # pre_beam_size > beam_size + self.pre_beam_size = int(pre_beam_ratio * beam_size) + self.beam_size = beam_size + self.n_vocab = vocab_size + if (pre_beam_score_key is not None and pre_beam_score_key != "full" and + pre_beam_score_key not in self.full_scorers): + raise KeyError( + f"{pre_beam_score_key} is not found in {self.full_scorers}") + # selected `key` scorer to do pre beam search + self.pre_beam_score_key = pre_beam_score_key + # do_pre_beam when need, valid and has part_scorers + self.do_pre_beam = (self.pre_beam_score_key is not None and + self.pre_beam_size < self.n_vocab and + len(self.part_scorers) > 0) + + def init_hyp(self, x: paddle.Tensor) -> List[Hypothesis]: + """Get an initial hypothesis data. + + Args: + x (paddle.Tensor): The encoder output feature, (T, D) + + Returns: + Hypothesis: The initial hypothesis. + + """ + init_states = dict() + init_scores = dict() + for k, d in self.scorers.items(): + init_states[k] = d.init_state(x) + init_scores[k] = 0.0 + return [ + Hypothesis( + yseq=paddle.to_tensor([self.sos], place=x.place), + score=0.0, + scores=init_scores, + states=init_states, ) + ] + + @staticmethod + def append_token(xs: paddle.Tensor, + x: Union[int, paddle.Tensor]) -> paddle.Tensor: + """Append new token to prefix tokens. + + Args: + xs (paddle.Tensor): The prefix token, (T,) + x (int): The new token to append + + Returns: + paddle.Tensor: (T+1,), New tensor contains: xs + [x] with xs.dtype and xs.device + + """ + x = paddle.to_tensor([x], dtype=xs.dtype) if isinstance(x, int) else x + return paddle.concat((xs, x)) + + def score_full(self, hyp: Hypothesis, x: paddle.Tensor + ) -> Tuple[Dict[str, paddle.Tensor], Dict[str, Any]]: + """Score new hypothesis by `self.full_scorers`. + + Args: + hyp (Hypothesis): Hypothesis with prefix tokens to score + x (paddle.Tensor): Corresponding input feature, (T, D) + + Returns: + Tuple[Dict[str, paddle.Tensor], Dict[str, Any]]: Tuple of + score dict of `hyp` that has string keys of `self.full_scorers` + and tensor score values of shape: `(self.n_vocab,)`, + and state dict that has string keys + and state values of `self.full_scorers` + + """ + scores = dict() + states = dict() + for k, d in self.full_scorers.items(): + # scores[k] shape (self.n_vocab,) + scores[k], states[k] = d.score(hyp.yseq, hyp.states[k], x) + return scores, states + + def score_partial(self, + hyp: Hypothesis, + ids: paddle.Tensor, + x: paddle.Tensor + ) -> Tuple[Dict[str, paddle.Tensor], Dict[str, Any]]: + """Score new hypothesis by `self.part_scorers`. + + Args: + hyp (Hypothesis): Hypothesis with prefix tokens to score + ids (paddle.Tensor): 1D tensor of new partial tokens to score, + len(ids) < n_vocab + x (paddle.Tensor): Corresponding input feature, (T, D) + + Returns: + Tuple[Dict[str, paddle.Tensor], Dict[str, Any]]: Tuple of + score dict of `hyp` that has string keys of `self.part_scorers` + and tensor score values of shape: `(len(ids),)`, + and state dict that has string keys + and state values of `self.part_scorers` + + """ + scores = dict() + states = dict() + for k, d in self.part_scorers.items(): + # scores[k] shape (len(ids),) + scores[k], states[k] = d.score_partial(hyp.yseq, ids, hyp.states[k], + x) + return scores, states + + def beam(self, weighted_scores: paddle.Tensor, + ids: paddle.Tensor) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Compute topk full token ids and partial token ids. + + Args: + weighted_scores (paddle.Tensor): The weighted sum scores for each tokens. + Its shape is `(self.n_vocab,)`. + ids (paddle.Tensor): The partial token ids(Global) to compute topk. + + Returns: + Tuple[paddle.Tensor, paddle.Tensor]: + The topk full token ids and partial token ids. + Their shapes are `(self.beam_size,)`. + i.e. (global ids, global relative local ids). + + """ + # no pre beam performed, `ids` equal to `weighted_scores` + if weighted_scores.size(0) == ids.size(0): + top_ids = weighted_scores.topk( + self.beam_size)[1] # index in n_vocab + return top_ids, top_ids + + # mask pruned in pre-beam not to select in topk + tmp = weighted_scores[ids] + weighted_scores[:] = -float("inf") + weighted_scores[ids] = tmp + # top_ids no equal to local_ids, since ids shape not same + top_ids = weighted_scores.topk(self.beam_size)[1] # index in n_vocab + local_ids = weighted_scores[ids].topk( + self.beam_size)[1] # index in len(ids) + return top_ids, local_ids + + @staticmethod + def merge_scores( + prev_scores: Dict[str, float], + next_full_scores: Dict[str, paddle.Tensor], + full_idx: int, + next_part_scores: Dict[str, paddle.Tensor], + part_idx: int, ) -> Dict[str, paddle.Tensor]: + """Merge scores for new hypothesis. + + Args: + prev_scores (Dict[str, float]): + The previous hypothesis scores by `self.scorers` + next_full_scores (Dict[str, paddle.Tensor]): scores by `self.full_scorers` + full_idx (int): The next token id for `next_full_scores` + next_part_scores (Dict[str, paddle.Tensor]): + scores of partial tokens by `self.part_scorers` + part_idx (int): The new token id for `next_part_scores` + + Returns: + Dict[str, paddle.Tensor]: The new score dict. + Its keys are names of `self.full_scorers` and `self.part_scorers`. + Its values are scalar tensors by the scorers. + + """ + new_scores = dict() + for k, v in next_full_scores.items(): + new_scores[k] = prev_scores[k] + v[full_idx] + for k, v in next_part_scores.items(): + new_scores[k] = prev_scores[k] + v[part_idx] + return new_scores + + def merge_states(self, states: Any, part_states: Any, part_idx: int) -> Any: + """Merge states for new hypothesis. + + Args: + states: states of `self.full_scorers` + part_states: states of `self.part_scorers` + part_idx (int): The new token id for `part_scores` + + Returns: + Dict[str, paddle.Tensor]: The new score dict. + Its keys are names of `self.full_scorers` and `self.part_scorers`. + Its values are states of the scorers. + + """ + new_states = dict() + for k, v in states.items(): + new_states[k] = v + for k, d in self.part_scorers.items(): + new_states[k] = d.select_state(part_states[k], part_idx) + return new_states + + def search(self, running_hyps: List[Hypothesis], + x: paddle.Tensor) -> List[Hypothesis]: + """Search new tokens for running hypotheses and encoded speech x. + + Args: + running_hyps (List[Hypothesis]): Running hypotheses on beam + x (paddle.Tensor): Encoded speech feature (T, D) + + Returns: + List[Hypotheses]: Best sorted hypotheses + + """ + best_hyps = [] + part_ids = paddle.arange(self.n_vocab) # no pre-beam + for hyp in running_hyps: + # scoring + weighted_scores = paddle.zeros([self.n_vocab], dtype=x.dtype) + scores, states = self.score_full(hyp, x) + for k in self.full_scorers: + weighted_scores += self.weights[k] * scores[k] + # partial scoring + if self.do_pre_beam: + pre_beam_scores = (weighted_scores + if self.pre_beam_score_key == "full" else + scores[self.pre_beam_score_key]) + part_ids = paddle.topk(pre_beam_scores, self.pre_beam_size)[1] + part_scores, part_states = self.score_partial(hyp, part_ids, x) + for k in self.part_scorers: + weighted_scores[part_ids] += self.weights[k] * part_scores[k] + # add previous hyp score + weighted_scores += hyp.score + + # update hyps + for j, part_j in zip(*self.beam(weighted_scores, part_ids)): + # `part_j` is `j` relative id in `part_scores` + # will be (2 x beam at most) + best_hyps.append( + Hypothesis( + score=weighted_scores[j], + yseq=self.append_token(hyp.yseq, j), + scores=self.merge_scores(hyp.scores, scores, j, + part_scores, part_j), + states=self.merge_states(states, part_states, part_j), + )) + + # sort and prune 2 x beam -> beam + best_hyps = sorted( + best_hyps, key=lambda x: x.score, + reverse=True)[:min(len(best_hyps), self.beam_size)] + return best_hyps + + def forward(self, + x: paddle.Tensor, + maxlenratio: float=0.0, + minlenratio: float=0.0) -> List[Hypothesis]: + """Perform beam search. + + Args: + x (paddle.Tensor): Encoded speech feature (T, D) + maxlenratio (float): Input length ratio to obtain max output length. + If maxlenratio=0.0 (default), it uses a end-detect function + to automatically find maximum hypothesis lengths + If maxlenratio<0.0, its absolute value is interpreted + as a constant max output length. + minlenratio (float): Input length ratio to obtain min output length. + + Returns: + list[Hypothesis]: N-best decoding results + + """ + # set length bounds + if maxlenratio == 0: + maxlen = x.shape[0] + elif maxlenratio < 0: + maxlen = -1 * int(maxlenratio) + else: + maxlen = max(1, int(maxlenratio * x.size(0))) + minlen = int(minlenratio * x.size(0)) + logger.info("decoder input length: " + str(x.shape[0])) + logger.info("max output length: " + str(maxlen)) + logger.info("min output length: " + str(minlen)) + + # main loop of prefix search + running_hyps = self.init_hyp(x) + ended_hyps = [] + for i in range(maxlen): + logger.debug("position " + str(i)) + best = self.search(running_hyps, x) + # post process of one iteration + running_hyps = self.post_process(i, maxlen, maxlenratio, best, + ended_hyps) + # end detection + if maxlenratio == 0.0 and end_detect( + [h.asdict() for h in ended_hyps], i): + logger.info(f"end detected at {i}") + break + if len(running_hyps) == 0: + logger.info("no hypothesis. Finish decoding.") + break + else: + logger.debug(f"remained hypotheses: {len(running_hyps)}") + + nbest_hyps = sorted(ended_hyps, key=lambda x: x.score, reverse=True) + # check the number of hypotheses reaching to eos + if len(nbest_hyps) == 0: + logger.warning("there is no N-best results, perform recognition " + "again with smaller minlenratio.") + return ([] if minlenratio < 0.1 else + self.forward(x, maxlenratio, max(0.0, minlenratio - 0.1))) + + # report the best result + best = nbest_hyps[0] + for k, v in best.scores.items(): + logger.info( + f"{float(v):6.2f} * {self.weights[k]:3} = {float(v) * self.weights[k]:6.2f} for {k}" + ) + logger.info(f"total log probability: {float(best.score):.2f}") + logger.info( + f"normalized log probability: {float(best.score) / len(best.yseq):.2f}" + ) + logger.info(f"total number of ended hypotheses: {len(nbest_hyps)}") + if self.token_list is not None: + # logger.info( + # "best hypo: " + # + "".join([self.token_list[x] for x in best.yseq[1:-1]]) + # + "\n" + # ) + logger.info("best hypo: " + "".join( + [self.token_list[x] for x in best.yseq[1:]]) + "\n") + return nbest_hyps + + def post_process( + self, + i: int, + maxlen: int, + maxlenratio: float, + running_hyps: List[Hypothesis], + ended_hyps: List[Hypothesis], ) -> List[Hypothesis]: + """Perform post-processing of beam search iterations. + + Args: + i (int): The length of hypothesis tokens. + maxlen (int): The maximum length of tokens in beam search. + maxlenratio (int): The maximum length ratio in beam search. + running_hyps (List[Hypothesis]): The running hypotheses in beam search. + ended_hyps (List[Hypothesis]): The ended hypotheses in beam search. + + Returns: + List[Hypothesis]: The new running hypotheses. + + """ + logger.debug(f"the number of running hypotheses: {len(running_hyps)}") + if self.token_list is not None: + logger.debug("best hypo: " + "".join( + [self.token_list[x] for x in running_hyps[0].yseq[1:]])) + # add eos in the final loop to avoid that there are no ended hyps + if i == maxlen - 1: + logger.info("adding in the last position in the loop") + running_hyps = [ + h._replace(yseq=self.append_token(h.yseq, self.eos)) + for h in running_hyps + ] + + # add ended hypotheses to a final list, and removed them from current hypotheses + # (this will be a problem, number of hyps < beam) + remained_hyps = [] + for hyp in running_hyps: + if hyp.yseq[-1] == self.eos: + # e.g., Word LM needs to add final score + for k, d in chain(self.full_scorers.items(), + self.part_scorers.items()): + s = d.final_score(hyp.states[k]) + hyp.scores[k] += s + hyp = hyp._replace(score=hyp.score + self.weights[k] * s) + ended_hyps.append(hyp) + else: + remained_hyps.append(hyp) + return remained_hyps + + +def beam_search( + x: paddle.Tensor, + sos: int, + eos: int, + beam_size: int, + vocab_size: int, + scorers: Dict[str, ScorerInterface], + weights: Dict[str, float], + token_list: List[str]=None, + maxlenratio: float=0.0, + minlenratio: float=0.0, + pre_beam_ratio: float=1.5, + pre_beam_score_key: str="full", ) -> list: + """Perform beam search with scorers. + + Args: + x (paddle.Tensor): Encoded speech feature (T, D) + sos (int): Start of sequence id + eos (int): End of sequence id + beam_size (int): The number of hypotheses kept during search + vocab_size (int): The number of vocabulary + scorers (dict[str, ScorerInterface]): Dict of decoder modules + e.g., Decoder, CTCPrefixScorer, LM + The scorer will be ignored if it is `None` + weights (dict[str, float]): Dict of weights for each scorers + The scorer will be ignored if its weight is 0 + token_list (list[str]): List of tokens for debug log + maxlenratio (float): Input length ratio to obtain max output length. + If maxlenratio=0.0 (default), it uses a end-detect function + to automatically find maximum hypothesis lengths + minlenratio (float): Input length ratio to obtain min output length. + pre_beam_score_key (str): key of scores to perform pre-beam search + pre_beam_ratio (float): beam size in the pre-beam search + will be `int(pre_beam_ratio * beam_size)` + + Returns: + List[Dict]: N-best decoding results + + """ + ret = BeamSearch( + scorers, + weights, + beam_size=beam_size, + vocab_size=vocab_size, + pre_beam_ratio=pre_beam_ratio, + pre_beam_score_key=pre_beam_score_key, + sos=sos, + eos=eos, + token_list=token_list, ).forward( + x=x, maxlenratio=maxlenratio, minlenratio=minlenratio) + return [h.asdict() for h in ret] diff --git a/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/__init__.py b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/__init__.py new file mode 100644 index 0000000..37ceae6 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/__init__.py @@ -0,0 +1,18 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .swig_wrapper import ctc_beam_search_decoding +from .swig_wrapper import ctc_beam_search_decoding_batch +from .swig_wrapper import ctc_greedy_decoding +from .swig_wrapper import CTCBeamSearchDecoder +from .swig_wrapper import Scorer diff --git a/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/decoders_deprecated.py b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/decoders_deprecated.py new file mode 100644 index 0000000..fef0880 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/decoders_deprecated.py @@ -0,0 +1,248 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains various CTC decoders.""" +import multiprocessing +from itertools import groupby +from math import log + +import numpy as np + + +def ctc_greedy_decoder(probs_seq, vocabulary): + """CTC greedy (best path) decoder. + + Path consisting of the most probable tokens are further post-processed to + remove consecutive repetitions and all blanks. + + :param probs_seq: 2-D list of probabilities over the vocabulary for each + character. Each element is a list of float probabilities + for one character. + :type probs_seq: list + :param vocabulary: Vocabulary list. + :type vocabulary: list + :return: Decoding result string. + :rtype: baseline + """ + # dimension verification + for probs in probs_seq: + if not len(probs) == len(vocabulary) + 1: + raise ValueError("probs_seq dimension mismatchedd with vocabulary") + # argmax to get the best index for each time step + max_index_list = list(np.array(probs_seq).argmax(axis=1)) + # remove consecutive duplicate indexes + index_list = [index_group[0] for index_group in groupby(max_index_list)] + # remove blank indexes + blank_index = len(vocabulary) + index_list = [index for index in index_list if index != blank_index] + # convert index list to string + return ''.join([vocabulary[index] for index in index_list]) + + +def ctc_beam_search_decoder(probs_seq, + beam_size, + vocabulary, + cutoff_prob=1.0, + cutoff_top_n=40, + ext_scoring_func=None, + nproc=False): + """CTC Beam search decoder. + + It utilizes beam search to approximately select top best decoding + labels and returning results in the descending order. + The implementation is based on Prefix Beam Search + (https://arxiv.org/abs/1408.2873), and the unclear part is + redesigned. Two important modifications: 1) in the iterative computation + of probabilities, the assignment operation is changed to accumulation for + one prefix may comes from different paths; 2) the if condition "if l^+ not + in A_prev then" after probabilities' computation is deprecated for it is + hard to understand and seems unnecessary. + + :param probs_seq: 2-D list of probability distributions over each time + step, with each element being a list of normalized + probabilities over vocabulary and blank. + :type probs_seq: 2-D list + :param beam_size: Width for beam search. + :type beam_size: int + :param vocabulary: Vocabulary list. + :type vocabulary: list + :param cutoff_prob: Cutoff probability in pruning, + default 1.0, no pruning. + :type cutoff_prob: float + :param ext_scoring_func: External scoring function for + partially decoded sentence, e.g. word count + or language model. + :type external_scoring_func: callable + :param nproc: Whether the decoder used in multiprocesses. + :type nproc: bool + :return: List of tuples of log probability and sentence as decoding + results, in descending order of the probability. + :rtype: list + """ + # dimension check + for prob_list in probs_seq: + if not len(prob_list) == len(vocabulary) + 1: + raise ValueError("The shape of prob_seq does not match with the " + "shape of the vocabulary.") + + # blank_id assign + blank_id = len(vocabulary) + + # If the decoder called in the multiprocesses, then use the global scorer + # instantiated in ctc_beam_search_decoder_batch(). + if nproc is True: + global ext_nproc_scorer + ext_scoring_func = ext_nproc_scorer + + # initialize + # prefix_set_prev: the set containing selected prefixes + # probs_b_prev: prefixes' probability ending with blank in previous step + # probs_nb_prev: prefixes' probability ending with non-blank in previous step + prefix_set_prev = {'\t': 1.0} + probs_b_prev, probs_nb_prev = {'\t': 1.0}, {'\t': 0.0} + + # extend prefix in loop + for time_step in range(len(probs_seq)): + # prefix_set_next: the set containing candidate prefixes + # probs_b_cur: prefixes' probability ending with blank in current step + # probs_nb_cur: prefixes' probability ending with non-blank in current step + prefix_set_next, probs_b_cur, probs_nb_cur = {}, {}, {} + + prob_idx = list(enumerate(probs_seq[time_step])) + cutoff_len = len(prob_idx) + # If pruning is enabled + if cutoff_prob < 1.0 or cutoff_top_n < cutoff_len: + prob_idx = sorted(prob_idx, key=lambda asd: asd[1], reverse=True) + cutoff_len, cum_prob = 0, 0.0 + for i in range(len(prob_idx)): + cum_prob += prob_idx[i][1] + cutoff_len += 1 + if cum_prob >= cutoff_prob: + break + cutoff_len = min(cutoff_len, cutoff_top_n) + prob_idx = prob_idx[0:cutoff_len] + + for l in prefix_set_prev: + if l not in prefix_set_next: + probs_b_cur[l], probs_nb_cur[l] = 0.0, 0.0 + + # extend prefix by travering prob_idx + for index in range(cutoff_len): + c, prob_c = prob_idx[index][0], prob_idx[index][1] + + if c == blank_id: + probs_b_cur[l] += prob_c * ( + probs_b_prev[l] + probs_nb_prev[l]) + else: + last_char = l[-1] + new_char = vocabulary[c] + l_plus = l + new_char + if l_plus not in prefix_set_next: + probs_b_cur[l_plus], probs_nb_cur[l_plus] = 0.0, 0.0 + + if new_char == last_char: + probs_nb_cur[l_plus] += prob_c * probs_b_prev[l] + probs_nb_cur[l] += prob_c * probs_nb_prev[l] + elif new_char == ' ': + if (ext_scoring_func is None) or (len(l) == 1): + score = 1.0 + else: + prefix = l[1:] + score = ext_scoring_func(prefix) + probs_nb_cur[l_plus] += score * prob_c * ( + probs_b_prev[l] + probs_nb_prev[l]) + else: + probs_nb_cur[l_plus] += prob_c * ( + probs_b_prev[l] + probs_nb_prev[l]) + # add l_plus into prefix_set_next + prefix_set_next[l_plus] = probs_nb_cur[ + l_plus] + probs_b_cur[l_plus] + # add l into prefix_set_next + prefix_set_next[l] = probs_b_cur[l] + probs_nb_cur[l] + # update probs + probs_b_prev, probs_nb_prev = probs_b_cur, probs_nb_cur + + # store top beam_size prefixes + prefix_set_prev = sorted( + prefix_set_next.items(), key=lambda asd: asd[1], reverse=True) + if beam_size < len(prefix_set_prev): + prefix_set_prev = prefix_set_prev[:beam_size] + prefix_set_prev = dict(prefix_set_prev) + + beam_result = [] + for seq, prob in prefix_set_prev.items(): + if prob > 0.0 and len(seq) > 1: + result = seq[1:] + # score last word by external scorer + if (ext_scoring_func is not None) and (result[-1] != ' '): + prob = prob * ext_scoring_func(result) + log_prob = log(prob) + beam_result.append((log_prob, result)) + else: + beam_result.append((float('-inf'), '')) + + # output top beam_size decoding results + beam_result = sorted(beam_result, key=lambda asd: asd[0], reverse=True) + return beam_result + + +def ctc_beam_search_decoder_batch(probs_split, + beam_size, + vocabulary, + num_processes, + cutoff_prob=1.0, + cutoff_top_n=40, + ext_scoring_func=None): + """CTC beam search decoder using multiple processes. + + :param probs_seq: 3-D list with each element as an instance of 2-D list + of probabilities used by ctc_beam_search_decoder(). + :type probs_seq: 3-D list + :param beam_size: Width for beam search. + :type beam_size: int + :param vocabulary: Vocabulary list. + :type vocabulary: list + :param num_processes: Number of parallel processes. + :type num_processes: int + :param cutoff_prob: Cutoff probability in pruning, + default 1.0, no pruning. + :type cutoff_prob: float + :param num_processes: Number of parallel processes. + :type num_processes: int + :param ext_scoring_func: External scoring function for + partially decoded sentence, e.g. word count + or language model. + :type external_scoring_function: callable + :return: List of tuples of log probability and sentence as decoding + results, in descending order of the probability. + :rtype: list + """ + if not num_processes > 0: + raise ValueError("Number of processes must be positive!") + + # use global variable to pass the externnal scorer to beam search decoder + global ext_nproc_scorer + ext_nproc_scorer = ext_scoring_func + nproc = True + + pool = multiprocessing.Pool(processes=num_processes) + results = [] + for i, probs_list in enumerate(probs_split): + args = (probs_list, beam_size, vocabulary, cutoff_prob, cutoff_top_n, + None, nproc) + results.append(pool.apply_async(ctc_beam_search_decoder, args)) + + pool.close() + pool.join() + beam_search_results = [result.get() for result in results] + return beam_search_results diff --git a/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/scorer_deprecated.py b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/scorer_deprecated.py new file mode 100644 index 0000000..362098f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/scorer_deprecated.py @@ -0,0 +1,78 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""External Scorer for Beam Search Decoder.""" +import os + +import kenlm +import numpy as np + + +class Scorer(object): + """External scorer to evaluate a prefix or whole sentence in + beam search decoding, including the score from n-gram language + model and word count. + + :param alpha: Parameter associated with language model. Don't use + language model when alpha = 0. + :type alpha: float + :param beta: Parameter associated with word count. Don't use word + count when beta = 0. + :type beta: float + :model_path: Path to load language model. + :type model_path: str + """ + + def __init__(self, alpha, beta, model_path): + self._alpha = alpha + self._beta = beta + if not os.path.isfile(model_path): + raise IOError("Invaid language model path: %s" % model_path) + self._language_model = kenlm.LanguageModel(model_path) + + # n-gram language model scoring + def _language_model_score(self, sentence): + #log10 prob of last word + log_cond_prob = list( + self._language_model.full_scores(sentence, eos=False))[-1][0] + return np.power(10, log_cond_prob) + + # word insertion term + def _word_count(self, sentence): + words = sentence.strip().split(' ') + return len(words) + + # reset alpha and beta + def reset_params(self, alpha, beta): + self._alpha = alpha + self._beta = beta + + # execute evaluation + def __call__(self, sentence, log=False): + """Evaluation function, gathering all the different scores + and return the final one. + + :param sentence: The input sentence for evaluation + :type sentence: str + :param log: Whether return the score in log representation. + :type log: bool + :return: Evaluation score, in the decimal or log. + :rtype: float + """ + lm = self._language_model_score(sentence) + word_cnt = self._word_count(sentence) + if log is False: + score = np.power(lm, self._alpha) * np.power(word_cnt, self._beta) + else: + score = self._alpha * np.log(lm) + self._beta * np.log(word_cnt) + return score diff --git a/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/swig_wrapper.py b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/swig_wrapper.py new file mode 100644 index 0000000..9e2a850 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/swig_wrapper.py @@ -0,0 +1,159 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Wrapper for various CTC decoders in SWIG.""" +import paddlespeech_ctcdecoders + + +class Scorer(paddlespeech_ctcdecoders.Scorer): + """Wrapper for Scorer. + + :param alpha: Parameter associated with language model. Don't use + language model when alpha = 0. + :type alpha: float + :param beta: Parameter associated with word count. Don't use word + count when beta = 0. + :type beta: float + :model_path: Path to load language model. + :type model_path: str + :param vocabulary: Vocabulary list. + :type vocabulary: list + """ + + def __init__(self, alpha, beta, model_path, vocabulary): + paddlespeech_ctcdecoders.Scorer.__init__(self, alpha, beta, model_path, + vocabulary) + + +def ctc_greedy_decoding(probs_seq, vocabulary, blank_id): + """Wrapper for ctc best path decodeing function in swig. + + :param probs_seq: 2-D list of probability distributions over each time + step, with each element being a list of normalized + probabilities over vocabulary and blank. + :type probs_seq: 2-D list + :param vocabulary: Vocabulary list. + :type vocabulary: list + :return: Decoding result string. + :rtype: str + """ + result = paddlespeech_ctcdecoders.ctc_greedy_decoding(probs_seq.tolist(), + vocabulary, blank_id) + return result + + +def ctc_beam_search_decoding(probs_seq, + vocabulary, + beam_size, + cutoff_prob=1.0, + cutoff_top_n=40, + ext_scoring_func=None, + blank_id=0): + """Wrapper for the CTC Beam Search Decoding function. + + :param probs_seq: 2-D list of probability distributions over each time + step, with each element being a list of normalized + probabilities over vocabulary and blank. + :type probs_seq: 2-D list + :param vocabulary: Vocabulary list. + :type vocabulary: list + :param beam_size: Width for beam search. + :type beam_size: int + :param cutoff_prob: Cutoff probability in pruning, + default 1.0, no pruning. + :type cutoff_prob: float + :param cutoff_top_n: Cutoff number in pruning, only top cutoff_top_n + characters with highest probs in vocabulary will be + used in beam search, default 40. + :type cutoff_top_n: int + :param ext_scoring_func: External scoring function for + partially decoded sentence, e.g. word count + or language model. + :type external_scoring_func: callable + :return: List of tuples of log probability and sentence as decoding + results, in descending order of the probability. + :rtype: list + """ + beam_results = paddlespeech_ctcdecoders.ctc_beam_search_decoding( + probs_seq.tolist(), vocabulary, beam_size, cutoff_prob, cutoff_top_n, + ext_scoring_func, blank_id) + beam_results = [(res[0], res[1].decode('utf-8')) for res in beam_results] + return beam_results + + +def ctc_beam_search_decoding_batch(probs_split, + vocabulary, + beam_size, + num_processes, + cutoff_prob=1.0, + cutoff_top_n=40, + ext_scoring_func=None, + blank_id=0): + """Wrapper for the batched CTC beam search decodeing batch function. + + :param probs_seq: 3-D list with each element as an instance of 2-D list + of probabilities used by ctc_beam_search_decoder(). + :type probs_seq: 3-D list + :param vocabulary: Vocabulary list. + :type vocabulary: list + :param beam_size: Width for beam search. + :type beam_size: int + :param num_processes: Number of parallel processes. + :type num_processes: int + :param cutoff_prob: Cutoff probability in vocabulary pruning, + default 1.0, no pruning. + :type cutoff_prob: float + :param cutoff_top_n: Cutoff number in pruning, only top cutoff_top_n + characters with highest probs in vocabulary will be + used in beam search, default 40. + :type cutoff_top_n: int + :param num_processes: Number of parallel processes. + :type num_processes: int + :param ext_scoring_func: External scoring function for + partially decoded sentence, e.g. word count + or language model. + :type external_scoring_function: callable + :return: List of tuples of log probability and sentence as decoding + results, in descending order of the probability. + :rtype: list + """ + probs_split = [probs_seq.tolist() for probs_seq in probs_split] + + batch_beam_results = paddlespeech_ctcdecoders.ctc_beam_search_decoding_batch( + probs_split, vocabulary, beam_size, num_processes, cutoff_prob, + cutoff_top_n, ext_scoring_func, blank_id) + batch_beam_results = [[(res[0], res[1]) for res in beam_results] + for beam_results in batch_beam_results] + return batch_beam_results + + +class CTCBeamSearchDecoder(paddlespeech_ctcdecoders.CtcBeamSearchDecoderBatch): + """Wrapper for CtcBeamSearchDecoderBatch. + Args: + vocab_list (list): Vocabulary list. + beam_size (int): Width for beam search. + num_processes (int): Number of parallel processes. + param cutoff_prob (float): Cutoff probability in vocabulary pruning, + default 1.0, no pruning. + cutoff_top_n (int): Cutoff number in pruning, only top cutoff_top_n + characters with highest probs in vocabulary will be + used in beam search, default 40. + param ext_scorer (Scorer): External scorer for partially decoded sentence, e.g. word count + or language model. + """ + + def __init__(self, vocab_list, batch_size, beam_size, num_processes, + cutoff_prob, cutoff_top_n, _ext_scorer, blank_id): + paddlespeech_ctcdecoders.CtcBeamSearchDecoderBatch.__init__( + self, vocab_list, batch_size, beam_size, num_processes, cutoff_prob, + cutoff_top_n, _ext_scorer, blank_id) diff --git a/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/tests/test_decoders.py b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/tests/test_decoders.py new file mode 100644 index 0000000..a284890 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/ctcdecoder/tests/test_decoders.py @@ -0,0 +1,100 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test decoders.""" +import unittest + +from paddlespeech.s2t.decoders import decoders_deprecated as decoder + + +class TestDecoders(unittest.TestCase): + def setUp(self): + self.vocab_list = ["\'", ' ', 'a', 'b', 'c', 'd'] + self.beam_size = 20 + self.probs_seq1 = [[ + 0.06390443, 0.21124858, 0.27323887, 0.06870235, 0.0361254, + 0.18184413, 0.16493624 + ], [ + 0.03309247, 0.22866108, 0.24390638, 0.09699597, 0.31895462, + 0.0094893, 0.06890021 + ], [ + 0.218104, 0.19992557, 0.18245131, 0.08503348, 0.14903535, + 0.08424043, 0.08120984 + ], [ + 0.12094152, 0.19162472, 0.01473646, 0.28045061, 0.24246305, + 0.05206269, 0.09772094 + ], [ + 0.1333387, 0.00550838, 0.00301669, 0.21745861, 0.20803985, + 0.41317442, 0.01946335 + ], [ + 0.16468227, 0.1980699, 0.1906545, 0.18963251, 0.19860937, + 0.04377724, 0.01457421 + ]] + self.probs_seq2 = [[ + 0.08034842, 0.22671944, 0.05799633, 0.36814645, 0.11307441, + 0.04468023, 0.10903471 + ], [ + 0.09742457, 0.12959763, 0.09435383, 0.21889204, 0.15113123, + 0.10219457, 0.20640612 + ], [ + 0.45033529, 0.09091417, 0.15333208, 0.07939558, 0.08649316, + 0.12298585, 0.01654384 + ], [ + 0.02512238, 0.22079203, 0.19664364, 0.11906379, 0.07816055, + 0.22538587, 0.13483174 + ], [ + 0.17928453, 0.06065261, 0.41153005, 0.1172041, 0.11880313, + 0.07113197, 0.04139363 + ], [ + 0.15882358, 0.1235788, 0.23376776, 0.20510435, 0.00279306, + 0.05294827, 0.22298418 + ]] + self.greedy_result = ["ac'bdc", "b'da"] + self.beam_search_result = ['acdc', "b'a"] + + def test_greedy_decoder_1(self): + bst_result = decoder.ctc_greedy_decoder(self.probs_seq1, + self.vocab_list) + self.assertEqual(bst_result, self.greedy_result[0]) + + def test_greedy_decoder_2(self): + bst_result = decoder.ctc_greedy_decoder(self.probs_seq2, + self.vocab_list) + self.assertEqual(bst_result, self.greedy_result[1]) + + def test_beam_search_decoder_1(self): + beam_result = decoder.ctc_beam_search_decoder( + probs_seq=self.probs_seq1, + beam_size=self.beam_size, + vocabulary=self.vocab_list) + self.assertEqual(beam_result[0][1], self.beam_search_result[0]) + + def test_beam_search_decoder_2(self): + beam_result = decoder.ctc_beam_search_decoder( + probs_seq=self.probs_seq2, + beam_size=self.beam_size, + vocabulary=self.vocab_list) + self.assertEqual(beam_result[0][1], self.beam_search_result[1]) + + def test_beam_search_decoder_batch(self): + beam_results = decoder.ctc_beam_search_decoder_batch( + probs_split=[self.probs_seq1, self.probs_seq2], + beam_size=self.beam_size, + vocabulary=self.vocab_list, + num_processes=24) + self.assertEqual(beam_results[0][0][1], self.beam_search_result[0]) + self.assertEqual(beam_results[1][0][1], self.beam_search_result[1]) + + +if __name__ == '__main__': + unittest.main() diff --git a/ernie-sat/paddlespeech/s2t/decoders/recog.py b/ernie-sat/paddlespeech/s2t/decoders/recog.py new file mode 100644 index 0000000..2d2aa21 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/recog.py @@ -0,0 +1,196 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference espnet Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +# Modified from espnet(https://github.com/espnet/espnet) +"""V2 backend for `asr_recog.py` using py:class:`decoders.beam_search.BeamSearch`.""" +import jsonlines +import paddle +from yacs.config import CfgNode + +from .beam_search import BatchBeamSearch +from .beam_search import BeamSearch +from .scorers.length_bonus import LengthBonus +from .scorers.scorer_interface import BatchScorerInterface +from .utils import add_results_to_json +from paddlespeech.s2t.exps import dynamic_import_tester +from paddlespeech.s2t.io.reader import LoadInputsAndTargets +from paddlespeech.s2t.models.asr_interface import ASRInterface +from paddlespeech.s2t.models.lm_interface import dynamic_import_lm +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +# NOTE: you need this func to generate our sphinx doc + + +def get_config(config_path): + confs = CfgNode(new_allowed=True) + confs.merge_from_file(config_path) + return confs + + +def load_trained_model(args): + confs = get_config(args.model_conf) + class_obj = dynamic_import_tester(args.model_name) + exp = class_obj(confs, args) + with exp.eval(): + exp.setup() + exp.restore() + char_list = exp.args.char_list + model = exp.model + return model, char_list, exp, confs + + +def load_trained_lm(args): + lm_args = get_config(args.rnnlm_conf) + lm_model_module = lm_args.model_module + lm_class = dynamic_import_lm(lm_model_module) + lm = lm_class(**lm_args.model) + model_dict = paddle.load(args.rnnlm) + lm.set_state_dict(model_dict) + return lm + + +def recog_v2(args): + """Decode with custom models that implements ScorerInterface. + + Args: + args (namespace): The program arguments. + See py:func:`bin.asr_recog.get_parser` for details + + """ + logger.warning("experimental API for custom LMs is selected by --api v2") + if args.batchsize > 1: + raise NotImplementedError("multi-utt batch decoding is not implemented") + if args.streaming_mode is not None: + raise NotImplementedError("streaming mode is not implemented") + if args.word_rnnlm: + raise NotImplementedError("word LM is not implemented") + + # set_deterministic(args) + model, char_list, exp, confs = load_trained_model(args) + assert isinstance(model, ASRInterface) + + load_inputs_and_targets = LoadInputsAndTargets( + mode="asr", + load_output=False, + sort_in_input_length=False, + preprocess_conf=confs.preprocess_config + if args.preprocess_conf is None else args.preprocess_conf, + preprocess_args={"train": False}, ) + + if args.rnnlm: + lm = load_trained_lm(args) + lm.eval() + else: + lm = None + + if args.ngram_model: + from .scorers.ngram import NgramFullScorer + from .scorers.ngram import NgramPartScorer + + if args.ngram_scorer == "full": + ngram = NgramFullScorer(args.ngram_model, char_list) + else: + ngram = NgramPartScorer(args.ngram_model, char_list) + else: + ngram = None + + scorers = model.scorers() # decoder + scorers["lm"] = lm + scorers["ngram"] = ngram + scorers["length_bonus"] = LengthBonus(len(char_list)) + weights = dict( + decoder=1.0 - args.ctc_weight, + ctc=args.ctc_weight, + lm=args.lm_weight, + ngram=args.ngram_weight, + length_bonus=args.penalty, ) + beam_search = BeamSearch( + beam_size=args.beam_size, + vocab_size=len(char_list), + weights=weights, + scorers=scorers, + sos=model.sos, + eos=model.eos, + token_list=char_list, + pre_beam_score_key=None if args.ctc_weight == 1.0 else "full", ) + + # TODO(karita): make all scorers batchfied + if args.batchsize == 1: + non_batch = [ + k for k, v in beam_search.full_scorers.items() + if not isinstance(v, BatchScorerInterface) + ] + if len(non_batch) == 0: + beam_search.__class__ = BatchBeamSearch + logger.info("BatchBeamSearch implementation is selected.") + else: + logger.warning(f"As non-batch scorers {non_batch} are found, " + f"fall back to non-batch implementation.") + + if args.ngpu > 1: + raise NotImplementedError("only single GPU decoding is supported") + if args.ngpu == 1: + device = "gpu:0" + else: + device = "cpu" + paddle.set_device(device) + dtype = getattr(paddle, args.dtype) + logger.info(f"Decoding device={device}, dtype={dtype}") + model.to(device=device, dtype=dtype) + model.eval() + beam_search.to(device=device, dtype=dtype) + beam_search.eval() + + # read json data + js = [] + with jsonlines.open(args.recog_json, "r") as reader: + for item in reader: + js.append(item) + # jsonlines to dict, key by 'utt', value by jsonline + js = {item['utt']: item for item in js} + + new_js = {} + with paddle.no_grad(): + with jsonlines.open(args.result_label, "w") as f: + for idx, name in enumerate(js.keys(), 1): + logger.info(f"({idx}/{len(js.keys())}) decoding " + name) + batch = [(name, js[name])] + feat = load_inputs_and_targets(batch)[0][0] + logger.info(f'feat: {feat.shape}') + enc = model.encode(paddle.to_tensor(feat).to(dtype)) + logger.info(f'eout: {enc.shape}') + nbest_hyps = beam_search( + x=enc, + maxlenratio=args.maxlenratio, + minlenratio=args.minlenratio) + nbest_hyps = [ + h.asdict() + for h in nbest_hyps[:min(len(nbest_hyps), args.nbest)] + ] + new_js[name] = add_results_to_json(js[name], nbest_hyps, + char_list) + + item = new_js[name]['output'][0] # 1-best + ref = item['text'] + rec_text = item['rec_text'].replace('▁', ' ').replace( + '', '').strip() + rec_tokenid = list(map(int, item['rec_tokenid'].split())) + f.write({ + "utt": name, + "refs": [ref], + "hyps": [rec_text], + "hyps_tokenid": [rec_tokenid], + }) diff --git a/ernie-sat/paddlespeech/s2t/decoders/recog_bin.py b/ernie-sat/paddlespeech/s2t/decoders/recog_bin.py new file mode 100644 index 0000000..37b49f3 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/recog_bin.py @@ -0,0 +1,376 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference espnet Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +# Modified from espnet(https://github.com/espnet/espnet) +"""End-to-end speech recognition model decoding script.""" +import logging +import os +import random +import sys + +import configargparse +import numpy as np +from distutils.util import strtobool + + +def get_parser(): + """Get default arguments.""" + parser = configargparse.ArgumentParser( + description="Transcribe text from speech using " + "a speech recognition model on one CPU or GPU", + config_file_parser_class=configargparse.YAMLConfigFileParser, + formatter_class=configargparse.ArgumentDefaultsHelpFormatter, ) + parser.add( + '--model-name', + type=str, + default='u2_kaldi', + help='model name, e.g: deepspeech2, u2, u2_kaldi, u2_st') + # general configuration + parser.add("--config", is_config_file=True, help="Config file path") + parser.add( + "--config2", + is_config_file=True, + help="Second config file path that overwrites the settings in `--config`", + ) + parser.add( + "--config3", + is_config_file=True, + help="Third config file path that overwrites the settings " + "in `--config` and `--config2`", ) + + parser.add_argument("--ngpu", type=int, default=0, help="Number of GPUs") + parser.add_argument( + "--dtype", + choices=("float16", "float32", "float64"), + default="float32", + help="Float precision (only available in --api v2)", ) + parser.add_argument("--debugmode", type=int, default=1, help="Debugmode") + parser.add_argument("--seed", type=int, default=1, help="Random seed") + parser.add_argument( + "--verbose", "-V", type=int, default=2, help="Verbose option") + parser.add_argument( + "--batchsize", + type=int, + default=1, + help="Batch size for beam search (0: means no batch processing)", ) + parser.add_argument( + "--preprocess-conf", + type=str, + default=None, + help="The configuration file for the pre-processing", ) + parser.add_argument( + "--api", + default="v2", + choices=["v2"], + help="Beam search APIs " + "v2: Experimental API. It supports any models that implements ScorerInterface.", + ) + # task related + parser.add_argument( + "--recog-json", type=str, help="Filename of recognition data (json)") + parser.add_argument( + "--result-label", + type=str, + required=True, + help="Filename of result label data (json)", ) + # model (parameter) related + parser.add_argument( + "--model", + type=str, + required=True, + help="Model file parameters to read") + parser.add_argument( + "--model-conf", type=str, default=None, help="Model config file") + parser.add_argument( + "--num-spkrs", + type=int, + default=1, + choices=[1, 2], + help="Number of speakers in the speech", ) + parser.add_argument( + "--num-encs", + default=1, + type=int, + help="Number of encoders in the model.") + # search related + parser.add_argument( + "--nbest", type=int, default=1, help="Output N-best hypotheses") + parser.add_argument("--beam-size", type=int, default=1, help="Beam size") + parser.add_argument( + "--penalty", type=float, default=0.0, help="Incertion penalty") + parser.add_argument( + "--maxlenratio", + type=float, + default=0.0, + help="""Input length ratio to obtain max output length. + If maxlenratio=0.0 (default), it uses a end-detect function + to automatically find maximum hypothesis lengths. + If maxlenratio<0.0, its absolute value is interpreted + as a constant max output length""", ) + parser.add_argument( + "--minlenratio", + type=float, + default=0.0, + help="Input length ratio to obtain min output length", ) + parser.add_argument( + "--ctc-weight", + type=float, + default=0.0, + help="CTC weight in joint decoding") + parser.add_argument( + "--weights-ctc-dec", + type=float, + action="append", + help="ctc weight assigned to each encoder during decoding." + "[in multi-encoder mode only]", ) + parser.add_argument( + "--ctc-window-margin", + type=int, + default=0, + help="""Use CTC window with margin parameter to accelerate + CTC/attention decoding especially on GPU. Smaller magin + makes decoding faster, but may increase search errors. + If margin=0 (default), this function is disabled""", ) + # transducer related + parser.add_argument( + "--search-type", + type=str, + default="default", + choices=["default", "nsc", "tsd", "alsd", "maes"], + help="""Type of beam search implementation to use during inference. + Can be either: default beam search ("default"), + N-Step Constrained beam search ("nsc"), Time-Synchronous Decoding ("tsd"), + Alignment-Length Synchronous Decoding ("alsd") or + modified Adaptive Expansion Search ("maes").""", ) + parser.add_argument( + "--nstep", + type=int, + default=1, + help="""Number of expansion steps allowed in NSC beam search or mAES + (nstep > 0 for NSC and nstep > 1 for mAES).""", ) + parser.add_argument( + "--prefix-alpha", + type=int, + default=2, + help="Length prefix difference allowed in NSC beam search or mAES.", ) + parser.add_argument( + "--max-sym-exp", + type=int, + default=2, + help="Number of symbol expansions allowed in TSD.", ) + parser.add_argument( + "--u-max", + type=int, + default=400, + help="Length prefix difference allowed in ALSD.", ) + parser.add_argument( + "--expansion-gamma", + type=float, + default=2.3, + help="Allowed logp difference for prune-by-value method in mAES.", ) + parser.add_argument( + "--expansion-beta", + type=int, + default=2, + help="""Number of additional candidates for expanded hypotheses + selection in mAES.""", ) + parser.add_argument( + "--score-norm", + type=strtobool, + nargs="?", + default=True, + help="Normalize final hypotheses' score by length", ) + parser.add_argument( + "--softmax-temperature", + type=float, + default=1.0, + help="Penalization term for softmax function.", ) + # rnnlm related + parser.add_argument( + "--rnnlm", type=str, default=None, help="RNNLM model file to read") + parser.add_argument( + "--rnnlm-conf", + type=str, + default=None, + help="RNNLM model config file to read") + parser.add_argument( + "--word-rnnlm", + type=str, + default=None, + help="Word RNNLM model file to read") + parser.add_argument( + "--word-rnnlm-conf", + type=str, + default=None, + help="Word RNNLM model config file to read", ) + parser.add_argument( + "--word-dict", type=str, default=None, help="Word list to read") + parser.add_argument( + "--lm-weight", type=float, default=0.1, help="RNNLM weight") + # ngram related + parser.add_argument( + "--ngram-model", + type=str, + default=None, + help="ngram model file to read") + parser.add_argument( + "--ngram-weight", type=float, default=0.1, help="ngram weight") + parser.add_argument( + "--ngram-scorer", + type=str, + default="part", + choices=("full", "part"), + help="""if the ngram is set as a part scorer, similar with CTC scorer, + ngram scorer only scores topK hypethesis. + if the ngram is set as full scorer, ngram scorer scores all hypthesis + the decoding speed of part scorer is musch faster than full one""", + ) + # streaming related + parser.add_argument( + "--streaming-mode", + type=str, + default=None, + choices=["window", "segment"], + help="""Use streaming recognizer for inference. + `--batchsize` must be set to 0 to enable this mode""", ) + parser.add_argument( + "--streaming-window", type=int, default=10, help="Window size") + parser.add_argument( + "--streaming-min-blank-dur", + type=int, + default=10, + help="Minimum blank duration threshold", ) + parser.add_argument( + "--streaming-onset-margin", type=int, default=1, help="Onset margin") + parser.add_argument( + "--streaming-offset-margin", type=int, default=1, help="Offset margin") + # non-autoregressive related + # Mask CTC related. See https://arxiv.org/abs/2005.08700 for the detail. + parser.add_argument( + "--maskctc-n-iterations", + type=int, + default=10, + help="Number of decoding iterations." + "For Mask CTC, set 0 to predict 1 mask/iter.", ) + parser.add_argument( + "--maskctc-probability-threshold", + type=float, + default=0.999, + help="Threshold probability for CTC output", ) + # quantize model related + parser.add_argument( + "--quantize-config", + nargs="*", + help="Quantize config list. E.g.: --quantize-config=[Linear,LSTM,GRU]", + ) + parser.add_argument( + "--quantize-dtype", + type=str, + default="qint8", + help="Dtype dynamic quantize") + parser.add_argument( + "--quantize-asr-model", + type=bool, + default=False, + help="Quantize asr model", ) + parser.add_argument( + "--quantize-lm-model", + type=bool, + default=False, + help="Quantize lm model", ) + return parser + + +def main(args): + """Run the main decoding function.""" + parser = get_parser() + parser.add_argument( + "--output", metavar="CKPT_DIR", help="path to save checkpoint.") + parser.add_argument( + "--checkpoint_path", type=str, help="path to load checkpoint") + parser.add_argument("--dict-path", type=str, help="path to load checkpoint") + args = parser.parse_args(args) + + if args.ngpu == 0 and args.dtype == "float16": + raise ValueError( + f"--dtype {args.dtype} does not support the CPU backend.") + + # logging info + if args.verbose == 1: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s", + ) + elif args.verbose == 2: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s", + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s", + ) + logging.warning("Skip DEBUG/INFO messages") + logging.info(args) + + # check CUDA_VISIBLE_DEVICES + if args.ngpu > 0: + cvd = os.environ.get("CUDA_VISIBLE_DEVICES") + if cvd is None: + logging.warning("CUDA_VISIBLE_DEVICES is not set.") + elif args.ngpu != len(cvd.split(",")): + logging.error("#gpus is not matched with CUDA_VISIBLE_DEVICES.") + sys.exit(1) + + # TODO(mn5k): support of multiple GPUs + if args.ngpu > 1: + logging.error("The program only supports ngpu=1.") + sys.exit(1) + + # display PYTHONPATH + logging.info("python path = " + os.environ.get("PYTHONPATH", "(None)")) + + # seed setting + random.seed(args.seed) + np.random.seed(args.seed) + logging.info("set random seed = %d" % args.seed) + + # validate rnn options + if args.rnnlm is not None and args.word_rnnlm is not None: + logging.error( + "It seems that both --rnnlm and --word-rnnlm are specified. " + "Please use either option.") + sys.exit(1) + + # recog + if args.num_spkrs == 1: + if args.num_encs == 1: + # Experimental API that supports custom LMs + if args.api == "v2": + from paddlespeech.s2t.decoders.recog import recog_v2 + recog_v2(args) + else: + raise ValueError("Only support --api v2") + else: + if args.api == "v2": + raise NotImplementedError( + f"--num-encs {args.num_encs} > 1 is not supported in --api v2" + ) + elif args.num_spkrs == 2: + raise ValueError("asr_mix not supported.") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/__init__.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc.py new file mode 100644 index 0000000..81d8b07 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc.py @@ -0,0 +1,164 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""ScorerInterface implementation for CTC.""" +import numpy as np +import paddle + +from .ctc_prefix_score import CTCPrefixScore +from .ctc_prefix_score import CTCPrefixScorePD +from .scorer_interface import BatchPartialScorerInterface + + +class CTCPrefixScorer(BatchPartialScorerInterface): + """Decoder interface wrapper for CTCPrefixScore.""" + + def __init__(self, ctc: paddle.nn.Layer, eos: int): + """Initialize class. + + Args: + ctc (paddle.nn.Layer): The CTC implementation. + For example, :class:`paddlespeech.s2t.modules.ctc.CTC` + eos (int): The end-of-sequence id. + + """ + self.ctc = ctc + self.eos = eos + self.impl = None + + def init_state(self, x: paddle.Tensor): + """Get an initial state for decoding. + + Args: + x (paddle.Tensor): The encoded feature tensor + + Returns: initial state + + """ + logp = self.ctc.log_softmax(x.unsqueeze(0)).squeeze(0).numpy() + # TODO(karita): use CTCPrefixScorePD + self.impl = CTCPrefixScore(logp, 0, self.eos, np) + return 0, self.impl.initial_state() + + def select_state(self, state, i, new_id=None): + """Select state with relative ids in the main beam search. + + Args: + state: Decoder state for prefix tokens + i (int): Index to select a state in the main beam search + new_id (int): New label id to select a state if necessary + + Returns: + state: pruned state + + """ + if type(state) == tuple: + if len(state) == 2: # for CTCPrefixScore + sc, st = state + return sc[i], st[i] + else: # for CTCPrefixScorePD (need new_id > 0) + r, log_psi, f_min, f_max, scoring_idmap = state + s = log_psi[i, new_id].expand(log_psi.size(1)) + if scoring_idmap is not None: + return r[:, :, i, scoring_idmap[i, new_id]], s, f_min, f_max + else: + return r[:, :, i, new_id], s, f_min, f_max + return None if state is None else state[i] + + def score_partial(self, y, ids, state, x): + """Score new token. + + Args: + y (paddle.Tensor): 1D prefix token + next_tokens (paddle.Tensor): paddle.int64 next token to score + state: decoder state for prefix tokens + x (paddle.Tensor): 2D encoder feature that generates ys + + Returns: + tuple[paddle.Tensor, Any]: + Tuple of a score tensor for y that has a shape `(len(next_tokens),)` + and next state for ys + + """ + prev_score, state = state + presub_score, new_st = self.impl(y.cpu(), ids.cpu(), state) + tscore = paddle.to_tensor( + presub_score - prev_score, place=x.place, dtype=x.dtype) + return tscore, (presub_score, new_st) + + def batch_init_state(self, x: paddle.Tensor): + """Get an initial state for decoding. + + Args: + x (paddle.Tensor): The encoded feature tensor + + Returns: initial state + + """ + logp = self.ctc.log_softmax(x.unsqueeze(0)) # assuming batch_size = 1 + xlen = paddle.to_tensor([logp.size(1)]) + self.impl = CTCPrefixScorePD(logp, xlen, 0, self.eos) + return None + + def batch_score_partial(self, y, ids, state, x): + """Score new token. + + Args: + y (paddle.Tensor): 1D prefix token + ids (paddle.Tensor): paddle.int64 next token to score + state: decoder state for prefix tokens + x (paddle.Tensor): 2D encoder feature that generates ys + + Returns: + tuple[paddle.Tensor, Any]: + Tuple of a score tensor for y that has a shape `(len(next_tokens),)` + and next state for ys + + """ + batch_state = ( + (paddle.stack([s[0] for s in state], axis=2), + paddle.stack([s[1] for s in state]), state[0][2], state[0][3], ) + if state[0] is not None else None) + return self.impl(y, batch_state, ids) + + def extend_prob(self, x: paddle.Tensor): + """Extend probs for decoding. + + This extension is for streaming decoding + as in Eq (14) in https://arxiv.org/abs/2006.14941 + + Args: + x (paddle.Tensor): The encoded feature tensor + + """ + logp = self.ctc.log_softmax(x.unsqueeze(0)) + self.impl.extend_prob(logp) + + def extend_state(self, state): + """Extend state for decoding. + + This extension is for streaming decoding + as in Eq (14) in https://arxiv.org/abs/2006.14941 + + Args: + state: The states of hyps + + Returns: extended state + + """ + new_state = [] + for s in state: + new_state.append(self.impl.extend_state(s)) + + return new_state diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc_prefix_score.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc_prefix_score.py new file mode 100644 index 0000000..78b8fe3 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/ctc_prefix_score.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +# Copyright 2018 Mitsubishi Electric Research Labs (Takaaki Hori) +# Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +import numpy as np +import paddle +import six + + +class CTCPrefixScorePD(): + """Batch processing of CTCPrefixScore + + which is based on Algorithm 2 in WATANABE et al. + "HYBRID CTC/ATTENTION ARCHITECTURE FOR END-TO-END SPEECH RECOGNITION," + but extended to efficiently compute the label probabilities for multiple + hypotheses simultaneously + See also Seki et al. "Vectorized Beam Search for CTC-Attention-Based + Speech Recognition," In INTERSPEECH (pp. 3825-3829), 2019. + """ + + def __init__(self, x, xlens, blank, eos, margin=0): + """Construct CTC prefix scorer + + `margin` is M in eq.(22,23) + + :param paddle.Tensor x: input label posterior sequences (B, T, O) + :param paddle.Tensor xlens: input lengths (B,) + :param int blank: blank label id + :param int eos: end-of-sequence id + :param int margin: margin parameter for windowing (0 means no windowing) + """ + # In the comment lines, + # we assume T: input_length, B: batch size, W: beam width, O: output dim. + self.logzero = -10000000000.0 + self.blank = blank + self.eos = eos + self.batch = x.size(0) + self.input_length = x.size(1) + self.odim = x.size(2) + self.dtype = x.dtype + + # Pad the rest of posteriors in the batch + # TODO(takaaki-hori): need a better way without for-loops + for i, l in enumerate(xlens): + if l < self.input_length: + x[i, l:, :] = self.logzero + x[i, l:, blank] = 0 + # Reshape input x + xn = x.transpose([1, 0, 2]) # (B, T, O) -> (T, B, O) + xb = xn[:, :, self.blank].unsqueeze(2).expand(-1, -1, + self.odim) # (T,B,O) + self.x = paddle.stack([xn, xb]) # (2, T, B, O) + self.end_frames = paddle.to_tensor(xlens) - 1 # (B,) + + # Setup CTC windowing + self.margin = margin + if margin > 0: + self.frame_ids = paddle.arange(self.input_length, dtype=self.dtype) + # Base indices for index conversion + # B idx, hyp idx. shape (B*W, 1) + self.idx_bh = None + # B idx. shape (B,) + self.idx_b = paddle.arange(self.batch) + # B idx, O idx. shape (B, 1) + self.idx_bo = (self.idx_b * self.odim).unsqueeze(1) + + def __call__(self, y, state, scoring_ids=None, att_w=None): + """Compute CTC prefix scores for next labels + + :param list y: prefix label sequences + :param tuple state: previous CTC state + :param paddle.Tensor scoring_ids: selected next ids to score (BW, O'), O' <= O + :param paddle.Tensor att_w: attention weights to decide CTC window + :return new_state, ctc_local_scores (BW, O) + """ + output_length = len(y[0]) - 1 # ignore sos + last_ids = [yi[-1] for yi in y] # last output label ids + n_bh = len(last_ids) # batch * hyps + n_hyps = n_bh // self.batch # assuming each utterance has the same # of hyps + self.scoring_num = scoring_ids.size( + -1) if scoring_ids is not None else 0 + # prepare state info + if state is None: + r_prev = paddle.full( + (self.input_length, 2, self.batch, n_hyps), + self.logzero, + dtype=self.dtype, ) # (T, 2, B, W) + r_prev[:, 1] = paddle.cumsum(self.x[0, :, :, self.blank], + 0).unsqueeze(2) + r_prev = r_prev.view(-1, 2, n_bh) # (T, 2, BW) + s_prev = 0.0 # score + f_min_prev = 0 # eq. 22-23 + f_max_prev = 1 # eq. 22-23 + else: + r_prev, s_prev, f_min_prev, f_max_prev = state + + # select input dimensions for scoring + if self.scoring_num > 0: + # (BW, O) + scoring_idmap = paddle.full( + (n_bh, self.odim), -1, dtype=paddle.long) + snum = self.scoring_num + if self.idx_bh is None or n_bh > len(self.idx_bh): + self.idx_bh = paddle.arange(n_bh).view(-1, 1) # (BW, 1) + scoring_idmap[self.idx_bh[:n_bh], scoring_ids] = paddle.arange(snum) + scoring_idx = ( + scoring_ids + self.idx_bo.repeat(1, n_hyps).view(-1, + 1) # (BW,1) + ).view(-1) # (BWO) + # x_ shape (2, T, B*W, O) + x_ = paddle.index_select( + self.x.view(2, -1, self.batch * self.odim), scoring_idx, + 2).view(2, -1, n_bh, snum) + else: + scoring_ids = None + scoring_idmap = None + snum = self.odim + # x_ shape (2, T, B*W, O) + x_ = self.x.unsqueeze(3).repeat(1, 1, 1, n_hyps, 1).view(2, -1, + n_bh, snum) + + # new CTC forward probs are prepared as a (T x 2 x BW x S) tensor + # that corresponds to r_t^n(h) and r_t^b(h) in a batch. + r = paddle.full( + (self.input_length, 2, n_bh, snum), + self.logzero, + dtype=self.dtype, ) + if output_length == 0: + r[0, 0] = x_[0, 0] + + r_sum = paddle.logsumexp(r_prev, 1) #(T,BW) + log_phi = r_sum.unsqueeze(2).repeat(1, 1, snum) # (T, BW, O) + if scoring_ids is not None: + for idx in range(n_bh): + pos = scoring_idmap[idx, last_ids[idx]] + if pos >= 0: + log_phi[:, idx, pos] = r_prev[:, 1, idx] + else: + for idx in range(n_bh): + log_phi[:, idx, last_ids[idx]] = r_prev[:, 1, idx] + + # decide start and end frames based on attention weights + if att_w is not None and self.margin > 0: + f_arg = paddle.matmul(att_w, self.frame_ids) + f_min = max(int(f_arg.min().cpu()), f_min_prev) + f_max = max(int(f_arg.max().cpu()), f_max_prev) + start = min(f_max_prev, max(f_min - self.margin, output_length, 1)) + end = min(f_max + self.margin, self.input_length) + else: + f_min = f_max = 0 + # if one frame one out, the output_length is the eating frame num now. + start = max(output_length, 1) + end = self.input_length + + # compute forward probabilities log(r_t^n(h)) and log(r_t^b(h)) + for t in range(start, end): + rp = r[t - 1] # (2 x BW x O') + rr = paddle.stack([rp[0], log_phi[t - 1], rp[0], rp[1]]).view( + 2, 2, n_bh, snum) # (2,2,BW,O') + r[t] = paddle.logsumexp(rr, 1) + x_[:, t] + + # compute log prefix probabilities log(psi) + log_phi_x = paddle.concat( + (log_phi[0].unsqueeze(0), log_phi[:-1]), axis=0) + x_[0] + if scoring_ids is not None: + log_psi = paddle.full( + (n_bh, self.odim), self.logzero, dtype=self.dtype) + log_psi_ = paddle.logsumexp( + paddle.concat( + (log_phi_x[start:end], r[start - 1, 0].unsqueeze(0)), + axis=0), + axis=0, ) + for si in range(n_bh): + log_psi[si, scoring_ids[si]] = log_psi_[si] + else: + log_psi = paddle.logsumexp( + paddle.concat( + (log_phi_x[start:end], r[start - 1, 0].unsqueeze(0)), + axis=0), + axis=0, ) + + for si in range(n_bh): + log_psi[si, self.eos] = r_sum[self.end_frames[si // n_hyps], si] + + # exclude blank probs + log_psi[:, self.blank] = self.logzero + + return (log_psi - s_prev), (r, log_psi, f_min, f_max, scoring_idmap) + + def index_select_state(self, state, best_ids): + """Select CTC states according to best ids + + :param state : CTC state + :param best_ids : index numbers selected by beam pruning (B, W) + :return selected_state + """ + r, s, f_min, f_max, scoring_idmap = state + # convert ids to BHO space + n_bh = len(s) + n_hyps = n_bh // self.batch + vidx = (best_ids + (self.idx_b * + (n_hyps * self.odim)).view(-1, 1)).view(-1) + # select hypothesis scores + s_new = paddle.index_select(s.view(-1), vidx, 0) + s_new = s_new.view(-1, 1).repeat(1, self.odim).view(n_bh, self.odim) + # convert ids to BHS space (S: scoring_num) + if scoring_idmap is not None: + snum = self.scoring_num + hyp_idx = (best_ids // self.odim + + (self.idx_b * n_hyps).view(-1, 1)).view(-1) + label_ids = paddle.fmod(best_ids, self.odim).view(-1) + score_idx = scoring_idmap[hyp_idx, label_ids] + score_idx[score_idx == -1] = 0 + vidx = score_idx + hyp_idx * snum + else: + snum = self.odim + # select forward probabilities + r_new = paddle.index_select(r.view(-1, 2, n_bh * snum), vidx, 2).view( + -1, 2, n_bh) + return r_new, s_new, f_min, f_max + + def extend_prob(self, x): + """Extend CTC prob. + + :param paddle.Tensor x: input label posterior sequences (B, T, O) + """ + + if self.x.shape[1] < x.shape[1]: # self.x (2,T,B,O); x (B,T,O) + # Pad the rest of posteriors in the batch + # TODO(takaaki-hori): need a better way without for-loops + xlens = [x.size(1)] + for i, l in enumerate(xlens): + if l < self.input_length: + x[i, l:, :] = self.logzero + x[i, l:, self.blank] = 0 + tmp_x = self.x + xn = x.transpose([1, 0, 2]) # (B, T, O) -> (T, B, O) + xb = xn[:, :, self.blank].unsqueeze(2).expand(-1, -1, self.odim) + self.x = paddle.stack([xn, xb]) # (2, T, B, O) + self.x[:, :tmp_x.shape[1], :, :] = tmp_x + self.input_length = x.size(1) + self.end_frames = paddle.to_tensor(xlens) - 1 + + def extend_state(self, state): + """Compute CTC prefix state. + + + :param state : CTC state + :return ctc_state + """ + + if state is None: + # nothing to do + return state + else: + r_prev, s_prev, f_min_prev, f_max_prev = state + + r_prev_new = paddle.full( + (self.input_length, 2), + self.logzero, + dtype=self.dtype, ) + start = max(r_prev.shape[0], 1) + r_prev_new[0:start] = r_prev + for t in range(start, self.input_length): + r_prev_new[t, 1] = r_prev_new[t - 1, 1] + self.x[0, t, :, + self.blank] + + return (r_prev_new, s_prev, f_min_prev, f_max_prev) + + +class CTCPrefixScore(): + """Compute CTC label sequence scores + + which is based on Algorithm 2 in WATANABE et al. + "HYBRID CTC/ATTENTION ARCHITECTURE FOR END-TO-END SPEECH RECOGNITION," + but extended to efficiently compute the probabilities of multiple labels + simultaneously + """ + + def __init__(self, x, blank, eos, xp): + self.xp = xp + self.logzero = -10000000000.0 + self.blank = blank + self.eos = eos + self.input_length = len(x) + self.x = x # (T, O) + + def initial_state(self): + """Obtain an initial CTC state + + :return: CTC state + """ + # initial CTC state is made of a frame x 2 tensor that corresponds to + # r_t^n() and r_t^b(), where 0 and 1 of axis=1 represent + # superscripts n and b (non-blank and blank), respectively. + # r shape (T, 2) + r = self.xp.full((self.input_length, 2), self.logzero, dtype=np.float32) + r[0, 1] = self.x[0, self.blank] + for i in six.moves.range(1, self.input_length): + r[i, 1] = r[i - 1, 1] + self.x[i, self.blank] + return r + + def __call__(self, y, cs, r_prev): + """Compute CTC prefix scores for next labels + + :param y : prefix label sequence + :param cs : array of next labels + :param r_prev: previous CTC state + :return ctc_scores, ctc_states + """ + # initialize CTC states + output_length = len(y) - 1 # ignore sos + # new CTC states are prepared as a frame x (n or b) x n_labels tensor + # that corresponds to r_t^n(h) and r_t^b(h). + # r shape (T, 2, n_labels) + r = self.xp.ndarray((self.input_length, 2, len(cs)), dtype=np.float32) + xs = self.x[:, cs] + if output_length == 0: + r[0, 0] = xs[0] + r[0, 1] = self.logzero + else: + # Although the code does not exactly follow Algorithm 2, + # we don't have to change it because we can assume + # r_t(h)=0 for t < |h| in CTC forward computation + # (Note: we assume here that index t starts with 0). + # The purpose of this difference is to reduce the number of for-loops. + # https://github.com/espnet/espnet/pull/3655 + # where we start to accumulate r_t(h) from t=|h| + # and iterate r_t(h) = (r_{t-1}(h) + ...) to T-1, + # avoiding accumulating zeros for t=1~|h|-1. + # Thus, we need to set r_{|h|-1}(h) = 0, + # i.e., r[output_length-1] = logzero, for initialization. + # This is just for reducing the computation. + r[output_length - 1] = self.logzero + + # prepare forward probabilities for the last label + r_sum = self.xp.logaddexp(r_prev[:, 0], + r_prev[:, 1]) # log(r_t^n(g) + r_t^b(g)) + last = y[-1] + if output_length > 0 and last in cs: + log_phi = self.xp.ndarray( + (self.input_length, len(cs)), dtype=np.float32) + for i in six.moves.range(len(cs)): + log_phi[:, i] = r_sum if cs[i] != last else r_prev[:, 1] + else: + log_phi = r_sum + + # compute forward probabilities log(r_t^n(h)), log(r_t^b(h)), + # and log prefix probabilities log(psi) + start = max(output_length, 1) + log_psi = r[start - 1, 0] + for t in six.moves.range(start, self.input_length): + r[t, 0] = self.xp.logaddexp(r[t - 1, 0], log_phi[t - 1]) + xs[t] + r[t, 1] = (self.xp.logaddexp(r[t - 1, 0], r[t - 1, 1]) + + self.x[t, self.blank]) + log_psi = self.xp.logaddexp(log_psi, log_phi[t - 1] + xs[t]) + + # get P(...eos|X) that ends with the prefix itself + eos_pos = self.xp.where(cs == self.eos)[0] + if len(eos_pos) > 0: + log_psi[eos_pos] = r_sum[-1] # log(r_T^n(g) + r_T^b(g)) + + # exclude blank probs + blank_pos = self.xp.where(cs == self.blank)[0] + if len(blank_pos) > 0: + log_psi[blank_pos] = self.logzero + + # return the log prefix probability and CTC states, where the label axis + # of the CTC states is moved to the first axis to slice it easily + # log_psi shape (n_labels,), state shape (n_labels, T, 2) + return log_psi, self.xp.rollaxis(r, 2) diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/length_bonus.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/length_bonus.py new file mode 100644 index 0000000..c5a76db --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/length_bonus.py @@ -0,0 +1,73 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Length bonus module.""" +from typing import Any +from typing import List +from typing import Tuple + +import paddle + +from .scorer_interface import BatchScorerInterface + + +class LengthBonus(BatchScorerInterface): + """Length bonus in beam search.""" + + def __init__(self, n_vocab: int): + """Initialize class. + + Args: + n_vocab (int): The number of tokens in vocabulary for beam search + + """ + self.n = n_vocab + + def score(self, y, state, x): + """Score new token. + + Args: + y (paddle.Tensor): 1D paddle.int64 prefix tokens. + state: Scorer state for prefix tokens + x (paddle.Tensor): 2D encoder feature that generates ys. + + Returns: + tuple[paddle.Tensor, Any]: Tuple of + paddle.float32 scores for next token (n_vocab) + and None + + """ + return paddle.to_tensor( + [1.0], place=x.place, dtype=x.dtype).expand(self.n), None + + def batch_score(self, + ys: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor) -> Tuple[paddle.Tensor, List[Any]]: + """Score new token batch. + + Args: + ys (paddle.Tensor): paddle.int64 prefix tokens (n_batch, ylen). + states (List[Any]): Scorer states for prefix tokens. + xs (paddle.Tensor): + The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + return (paddle.to_tensor([1.0], place=xs.place, dtype=xs.dtype).expand( + ys.shape[0], self.n), None, ) diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/ngram.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/ngram.py new file mode 100644 index 0000000..f260082 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/ngram.py @@ -0,0 +1,116 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Ngram lm implement.""" +from abc import ABC + +import kenlm +import paddle + +from .scorer_interface import BatchScorerInterface +from .scorer_interface import PartialScorerInterface + + +class Ngrambase(ABC): + """Ngram base implemented through ScorerInterface.""" + + def __init__(self, ngram_model, token_list): + """Initialize Ngrambase. + + Args: + ngram_model: ngram model path + token_list: token list from dict or model.json + + """ + self.chardict = [x if x != "" else "" for x in token_list] + self.charlen = len(self.chardict) + self.lm = kenlm.LanguageModel(ngram_model) + self.tmpkenlmstate = kenlm.State() + + def init_state(self, x): + """Initialize tmp state.""" + state = kenlm.State() + self.lm.NullContextWrite(state) + return state + + def score_partial_(self, y, next_token, state, x): + """Score interface for both full and partial scorer. + + Args: + y: previous char + next_token: next token need to be score + state: previous state + x: encoded feature + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + out_state = kenlm.State() + ys = self.chardict[y[-1]] if y.shape[0] > 1 else "" + self.lm.BaseScore(state, ys, out_state) + scores = paddle.empty_like(next_token, dtype=x.dtype) + for i, j in enumerate(next_token): + scores[i] = self.lm.BaseScore(out_state, self.chardict[j], + self.tmpkenlmstate) + return scores, out_state + + +class NgramFullScorer(Ngrambase, BatchScorerInterface): + """Fullscorer for ngram.""" + + def score(self, y, state, x): + """Score interface for both full and partial scorer. + + Args: + y: previous char + state: previous state + x: encoded feature + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + return self.score_partial_(y, + paddle.to_tensor(range(self.charlen)), state, + x) + + +class NgramPartScorer(Ngrambase, PartialScorerInterface): + """Partialscorer for ngram.""" + + def score_partial(self, y, next_token, state, x): + """Score interface for both full and partial scorer. + + Args: + y: previous char + next_token: next token need to be score + state: previous state + x: encoded feature + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + return self.score_partial_(y, next_token, state, x) + + def select_state(self, state, i): + """Empty select state for scorer interface.""" + return state diff --git a/ernie-sat/paddlespeech/s2t/decoders/scorers/scorer_interface.py b/ernie-sat/paddlespeech/s2t/decoders/scorers/scorer_interface.py new file mode 100644 index 0000000..3272e6b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/scorers/scorer_interface.py @@ -0,0 +1,202 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Scorer interface module.""" +import warnings +from typing import Any +from typing import List +from typing import Tuple + +import paddle + + +class ScorerInterface: + """Scorer interface for beam search. + + The scorer performs scoring of the all tokens in vocabulary. + + Examples: + * Search heuristics + * :class:`scorers.length_bonus.LengthBonus` + * Decoder networks of the sequence-to-sequence models + * :class:`transformer.decoder.Decoder` + * :class:`rnn.decoders.Decoder` + * Neural language models + * :class:`lm.transformer.TransformerLM` + * :class:`lm.default.DefaultRNNLM` + * :class:`lm.seq_rnn.SequentialRNNLM` + + """ + + def init_state(self, x: paddle.Tensor) -> Any: + """Get an initial state for decoding (optional). + + Args: + x (paddle.Tensor): The encoded feature tensor + + Returns: initial state + + """ + return None + + def select_state(self, state: Any, i: int, new_id: int=None) -> Any: + """Select state with relative ids in the main beam search. + + Args: + state: Decoder state for prefix tokens + i (int): Index to select a state in the main beam search + new_id (int): New label index to select a state if necessary + + Returns: + state: pruned state + + """ + return None if state is None else state[i] + + def score(self, y: paddle.Tensor, state: Any, + x: paddle.Tensor) -> Tuple[paddle.Tensor, Any]: + """Score new token (required). + + Args: + y (paddle.Tensor): 1D paddle.int64 prefix tokens. + state: Scorer state for prefix tokens + x (paddle.Tensor): The encoder feature that generates ys. + + Returns: + tuple[paddle.Tensor, Any]: Tuple of + scores for next token that has a shape of `(n_vocab)` + and next state for ys + + """ + raise NotImplementedError + + def final_score(self, state: Any) -> float: + """Score eos (optional). + + Args: + state: Scorer state for prefix tokens + + Returns: + float: final score + + """ + return 0.0 + + +class BatchScorerInterface(ScorerInterface): + """Batch scorer interface.""" + + def batch_init_state(self, x: paddle.Tensor) -> Any: + """Get an initial state for decoding (optional). + + Args: + x (paddle.Tensor): The encoded feature tensor + + Returns: initial state + + """ + return self.init_state(x) + + def batch_score(self, + ys: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor) -> Tuple[paddle.Tensor, List[Any]]: + """Score new token batch (required). + + Args: + ys (paddle.Tensor): paddle.int64 prefix tokens (n_batch, ylen). + states (List[Any]): Scorer states for prefix tokens. + xs (paddle.Tensor): + The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + warnings.warn( + "{} batch score is implemented through for loop not parallelized". + format(self.__class__.__name__)) + scores = list() + outstates = list() + for i, (y, state, x) in enumerate(zip(ys, states, xs)): + score, outstate = self.score(y, state, x) + outstates.append(outstate) + scores.append(score) + scores = paddle.cat(scores, 0).view(ys.shape[0], -1) + return scores, outstates + + +class PartialScorerInterface(ScorerInterface): + """Partial scorer interface for beam search. + + The partial scorer performs scoring when non-partial scorer finished scoring, + and receives pre-pruned next tokens to score because it is too heavy to score + all the tokens. + + Score sub-set of tokens, not all. + + Examples: + * Prefix search for connectionist-temporal-classification models + * :class:`decoders.scorers.ctc.CTCPrefixScorer` + + """ + + def score_partial(self, + y: paddle.Tensor, + next_tokens: paddle.Tensor, + state: Any, + x: paddle.Tensor) -> Tuple[paddle.Tensor, Any]: + """Score new token (required). + + Args: + y (paddle.Tensor): 1D prefix token + next_tokens (paddle.Tensor): paddle.int64 next token to score + state: decoder state for prefix tokens + x (paddle.Tensor): The encoder feature that generates ys + + Returns: + tuple[paddle.Tensor, Any]: + Tuple of a score tensor for y that has a shape `(len(next_tokens),)` + and next state for ys + + """ + raise NotImplementedError + + +class BatchPartialScorerInterface(BatchScorerInterface, PartialScorerInterface): + """Batch partial scorer interface for beam search.""" + + def batch_score_partial( + self, + ys: paddle.Tensor, + next_tokens: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor, ) -> Tuple[paddle.Tensor, Any]: + """Score new token (required). + + Args: + ys (paddle.Tensor): paddle.int64 prefix tokens (n_batch, ylen). + next_tokens (paddle.Tensor): paddle.int64 tokens to score (n_batch, n_token). + states (List[Any]): Scorer states for prefix tokens. + xs (paddle.Tensor): + The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[paddle.Tensor, Any]: + Tuple of a score tensor for ys that has a shape `(n_batch, n_vocab)` + and next states for ys + """ + raise NotImplementedError diff --git a/ernie-sat/paddlespeech/s2t/decoders/utils.py b/ernie-sat/paddlespeech/s2t/decoders/utils.py new file mode 100644 index 0000000..a609f1c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/decoders/utils.py @@ -0,0 +1,130 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import numpy as np + +from paddlespeech.s2t.utils.log import Log +logger = Log(__name__).getlog() + +__all__ = ["end_detect", "parse_hypothesis", "add_results_to_json"] + + +def end_detect(ended_hyps, i, M=3, D_end=np.log(1 * np.exp(-10))): + """End detection. + + described in Eq. (50) of S. Watanabe et al + "Hybrid CTC/Attention Architecture for End-to-End Speech Recognition" + + :param ended_hyps: dict + :param i: int + :param M: int + :param D_end: float + :return: bool + """ + if len(ended_hyps) == 0: + return False + count = 0 + best_hyp = sorted(ended_hyps, key=lambda x: x["score"], reverse=True)[0] + for m in range(M): + # get ended_hyps with their length is i - m + hyp_length = i - m + hyps_same_length = [ + x for x in ended_hyps if len(x["yseq"]) == hyp_length + ] + if len(hyps_same_length) > 0: + best_hyp_same_length = sorted( + hyps_same_length, key=lambda x: x["score"], reverse=True)[0] + if best_hyp_same_length["score"] - best_hyp["score"] < D_end: + count += 1 + + if count == M: + return True + else: + return False + + +# * ------------------ recognition related ------------------ * +def parse_hypothesis(hyp, char_list): + """Parse hypothesis. + + Args: + hyp (list[dict[str, Any]]): Recognition hypothesis. + char_list (list[str]): List of characters. + + Returns: + tuple(str, str, str, float) + + """ + # remove sos and get results + tokenid_as_list = list(map(int, hyp["yseq"][1:])) + token_as_list = [char_list[idx] for idx in tokenid_as_list] + score = float(hyp["score"]) + + # convert to string + tokenid = " ".join([str(idx) for idx in tokenid_as_list]) + token = " ".join(token_as_list) + text = "".join(token_as_list).replace("", " ") + + return text, token, tokenid, score + + +def add_results_to_json(js, nbest_hyps, char_list): + """Add N-best results to json. + + Args: + js (dict[str, Any]): Groundtruth utterance dict. + nbest_hyps_sd (list[dict[str, Any]]): + List of hypothesis for multi_speakers: nutts x nspkrs. + char_list (list[str]): List of characters. + + Returns: + dict[str, Any]: N-best results added utterance dict. + + """ + # copy old json info + new_js = dict() + new_js["utt2spk"] = js["utt2spk"] + new_js["output"] = [] + + for n, hyp in enumerate(nbest_hyps, 1): + # parse hypothesis + rec_text, rec_token, rec_tokenid, score = parse_hypothesis(hyp, + char_list) + + # copy ground-truth + if len(js["output"]) > 0: + out_dic = dict(js["output"][0].items()) + else: + # for no reference case (e.g., speech translation) + out_dic = {"name": ""} + + # update name + out_dic["name"] += "[%d]" % n + + # add recognition results + out_dic["rec_text"] = rec_text + out_dic["rec_token"] = rec_token + out_dic["rec_tokenid"] = rec_tokenid + out_dic["score"] = score + + # add to list of N-best result dicts + new_js["output"].append(out_dic) + + # show 1-best result + if n == 1: + if "text" in out_dic.keys(): + logger.info("groundtruth: %s" % out_dic["text"]) + logger.info("prediction : %s" % out_dic["rec_text"]) + + return new_js diff --git a/ernie-sat/paddlespeech/s2t/exps/__init__.py b/ernie-sat/paddlespeech/s2t/exps/__init__.py new file mode 100644 index 0000000..b4d0306 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/__init__.py @@ -0,0 +1,62 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +model_trainer_alias = { + "ds2": "paddlespeech.s2t.exp.deepspeech2.model:DeepSpeech2Trainer", + "u2": "paddlespeech.s2t.exps.u2.model:U2Trainer", + "u2_kaldi": "paddlespeech.s2t.exps.u2_kaldi.model:U2Trainer", + "u2_st": "paddlespeech.s2t.exps.u2_st.model:U2STTrainer", +} + + +def dynamic_import_trainer(module): + """Import Trainer dynamically. + + Args: + module (str): trainer name. e.g., ds2, u2, u2_kaldi + + Returns: + type: Trainer class + + """ + model_class = dynamic_import(module, model_trainer_alias) + assert issubclass(model_class, + Trainer), f"{module} does not implement Trainer" + return model_class + + +model_tester_alias = { + "ds2": "paddlespeech.s2t.exp.deepspeech2.model:DeepSpeech2Tester", + "u2": "paddlespeech.s2t.exps.u2.model:U2Tester", + "u2_kaldi": "paddlespeech.s2t.exps.u2_kaldi.model:U2Tester", + "u2_st": "paddlespeech.s2t.exps.u2_st.model:U2STTester", +} + + +def dynamic_import_tester(module): + """Import Tester dynamically. + + Args: + module (str): tester name. e.g., ds2, u2, u2_kaldi + + Returns: + type: Tester class + + """ + model_class = dynamic_import(module, model_tester_alias) + assert issubclass(model_class, + Trainer), f"{module} does not implement Tester" + return model_class diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/__init__.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/__init__.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/__init__.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/client.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/client.py new file mode 100644 index 0000000..f7ed842 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/client.py @@ -0,0 +1,95 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Client-end for the ASR demo.""" +import argparse +import sys + +import keyboard +import pyaudio + +from paddlespeech.s2t.utils.socket_server import socket_send + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + "--host_ip", + default="localhost", + type=str, + help="Server IP address. (default: %(default)s)") +parser.add_argument( + "--host_port", + default=8086, + type=int, + help="Server Port. (default: %(default)s)") +args = parser.parse_args() + +is_recording = False +enable_trigger_record = True + + +def on_press_release(x): + """Keyboard callback function.""" + global is_recording, enable_trigger_record + press = keyboard.KeyboardEvent('down', 28, 'space') + release = keyboard.KeyboardEvent('up', 28, 'space') + if x.event_type == 'down' and x.name == press.name: + if (not is_recording) and enable_trigger_record: + sys.stdout.write("Start Recording ... ") + sys.stdout.flush() + is_recording = True + if x.event_type == 'up' and x.name == release.name: + if is_recording: + is_recording = False + + +data_list = [] + + +def callback(in_data, frame_count, time_info, status): + """Audio recorder's stream callback function.""" + global data_list, is_recording, enable_trigger_record + if is_recording: + data_list.append(in_data) + enable_trigger_record = False + elif len(data_list) > 0: + socket_send(args.host_ip, args.host_port, ''.join(data_list)) + data_list = [] + enable_trigger_record = True + return (in_data, pyaudio.paContinue) + + +def main(): + # prepare audio recorder + p = pyaudio.PyAudio() + stream = p.open( + format=pyaudio.paInt16, + channels=1, + rate=16000, + input=True, + stream_callback=callback) + stream.start_stream() + + # prepare keyboard listener + while (1): + keyboard.hook(on_press_release) + if keyboard.record('esc'): + break + + # close up + stream.stop_stream() + stream.close() + p.terminate() + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/record.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/record.py new file mode 100644 index 0000000..94ad0f1 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/record.py @@ -0,0 +1,55 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Record wav from Microphone""" +# http://people.csail.mit.edu/hubert/pyaudio/ +import wave + +import pyaudio + +CHUNK = 1024 +FORMAT = pyaudio.paInt16 +CHANNELS = 1 +RATE = 16000 +RECORD_SECONDS = 5 +WAVE_OUTPUT_FILENAME = "output.wav" + +p = pyaudio.PyAudio() + +stream = p.open( + format=FORMAT, + channels=CHANNELS, + rate=RATE, + input=True, + frames_per_buffer=CHUNK) + +print("* recording") + +frames = [] + +for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)): + data = stream.read(CHUNK) + frames.append(data) + +print("* done recording") + +stream.stop_stream() +stream.close() +p.terminate() + +wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb') +wf.setnchannels(CHANNELS) +wf.setsampwidth(p.get_sample_size(FORMAT)) +wf.setframerate(RATE) +wf.writeframes(b''.join(frames)) +wf.close() diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/runtime.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/runtime.py new file mode 100644 index 0000000..5755a5f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/runtime.py @@ -0,0 +1,198 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Server-end for the ASR demo.""" +import functools + +import numpy as np +import paddle +from paddle.inference import Config +from paddle.inference import create_predictor +from paddle.io import DataLoader +from yacs.config import CfgNode + +from paddlespeech.s2t.io.collator import SpeechCollator +from paddlespeech.s2t.io.dataset import ManifestDataset +from paddlespeech.s2t.models.ds2 import DeepSpeech2Model +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.socket_server import AsrRequestHandler +from paddlespeech.s2t.utils.socket_server import AsrTCPServer +from paddlespeech.s2t.utils.socket_server import warm_up_test +from paddlespeech.s2t.utils.utility import add_arguments +from paddlespeech.s2t.utils.utility import print_arguments + + +def init_predictor(args): + if args.model_dir is not None: + config = Config(args.model_dir) + else: + config = Config(args.model_file, args.params_file) + + config.enable_memory_optim() + if args.use_gpu: + config.enable_use_gpu(memory_pool_init_size_mb=1000, device_id=0) + else: + # If not specific mkldnn, you can set the blas thread. + # The thread num should not be greater than the number of cores in the CPU. + config.set_cpu_math_library_num_threads(4) + config.enable_mkldnn() + + predictor = create_predictor(config) + return predictor + + +def run(predictor, img): + # copy img data to input tensor + input_names = predictor.get_input_names() + for i, name in enumerate(input_names): + input_tensor = predictor.get_input_handle(name) + #input_tensor.reshape(img[i].shape) + #input_tensor.copy_from_cpu(img[i].copy()) + + # do the inference + predictor.run() + + results = [] + # get out data from output tensor + output_names = predictor.get_output_names() + for i, name in enumerate(output_names): + output_tensor = predictor.get_output_handle(name) + output_data = output_tensor.copy_to_cpu() + results.append(output_data) + + return results + + +def inference(config, args): + predictor = init_predictor(args) + + +def start_server(config, args): + """Start the ASR server""" + config.defrost() + config.manifest = config.test_manifest + dataset = ManifestDataset.from_config(config) + + config.augmentation_config = "" + config.keep_transcription_text = True + config.batch_size = 1 + config.num_workers = 0 + collate_fn = SpeechCollator.from_config(config) + test_loader = DataLoader(dataset, collate_fn=collate_fn, num_workers=0) + + model = DeepSpeech2Model.from_pretrained(test_loader, config, + args.checkpoint_path) + model.eval() + + # prepare ASR inference handler + def file_to_transcript(filename): + feature = test_loader.collate_fn.process_utterance(filename, "") + audio = np.array([feature[0]]).astype('float32') #[1, T, D] + audio_len = feature[0].shape[0] + audio_len = np.array([audio_len]).astype('int64') # [1] + + result_transcript = model.decode( + paddle.to_tensor(audio), + paddle.to_tensor(audio_len), + vocab_list=test_loader.collate_fn.vocab_list, + decoding_method=config.decode.decoding_method, + lang_model_path=config.decode.lang_model_path, + beam_alpha=config.decode.alpha, + beam_beta=config.decode.beta, + beam_size=config.decode.beam_size, + cutoff_prob=config.decode.cutoff_prob, + cutoff_top_n=config.decode.cutoff_top_n, + num_processes=config.decode.num_proc_bsearch) + return result_transcript[0] + + # warming up with utterrances sampled from Librispeech + print('-----------------------------------------------------------') + print('Warming up ...') + warm_up_test( + audio_process_handler=file_to_transcript, + manifest_path=args.warmup_manifest, + num_test_cases=3) + print('-----------------------------------------------------------') + + # start the server + server = AsrTCPServer( + server_address=(args.host_ip, args.host_port), + RequestHandlerClass=AsrRequestHandler, + speech_save_dir=args.speech_save_dir, + audio_process_handler=file_to_transcript) + print("ASR Server Started.") + server.serve_forever() + + +def main(config, args): + start_server(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + add_arg = functools.partial(add_arguments, argparser=parser) + # yapf: disable + add_arg('host_ip', str, + 'localhost', + "Server's IP address.") + add_arg('host_port', int, 8089, "Server's IP port.") + add_arg('speech_save_dir', str, + 'demo_cache', + "Directory to save demo audios.") + add_arg('warmup_manifest', str, None, "Filepath of manifest to warm up.") + add_arg( + "--model_file", + type=str, + default="", + help="Model filename, Specify this when your model is a combined model." + ) + add_arg( + "--params_file", + type=str, + default="", + help="Parameter filename, Specify this when your model is a combined model." + ) + add_arg( + "--model_dir", + type=str, + default=None, + help="Model dir, If you load a non-combined model, specify the directory of the model." + ) + add_arg("--use_gpu", + type=bool, + default=False, + help="Whether use gpu.") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + + args.warmup_manifest = config.test_manifest + print_arguments(args, globals()) + + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/send.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/send.py new file mode 100644 index 0000000..596e701 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/send.py @@ -0,0 +1,50 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Socket client to send wav to ASR server.""" +import argparse +import wave + +from paddlespeech.s2t.utils.socket_server import socket_send + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + "--host_ip", + default="localhost", + type=str, + help="Server IP address. (default: %(default)s)") +parser.add_argument( + "--host_port", + default=8086, + type=int, + help="Server Port. (default: %(default)s)") +args = parser.parse_args() + +WAVE_OUTPUT_FILENAME = "output.wav" + + +def main(): + wf = wave.open(WAVE_OUTPUT_FILENAME, 'rb') + nframe = wf.getnframes() + data = wf.readframes(nframe) + print(f"Wave: {WAVE_OUTPUT_FILENAME}") + print(f"Wave samples: {nframe}") + print(f"Wave channels: {wf.getnchannels()}") + print(f"Wave sample rate: {wf.getframerate()}") + print(f"Wave sample width: {wf.getsampwidth()}") + assert isinstance(data, bytes) + socket_send(args.host_ip, args.host_port, data) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/server.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/server.py new file mode 100644 index 0000000..0d0b4f2 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/deploy/server.py @@ -0,0 +1,133 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Server-end for the ASR demo.""" +import functools + +import numpy as np +import paddle +from paddle.io import DataLoader +from yacs.config import CfgNode + +from paddlespeech.s2t.io.collator import SpeechCollator +from paddlespeech.s2t.io.dataset import ManifestDataset +from paddlespeech.s2t.models.ds2 import DeepSpeech2Model +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.socket_server import AsrRequestHandler +from paddlespeech.s2t.utils.socket_server import AsrTCPServer +from paddlespeech.s2t.utils.socket_server import warm_up_test +from paddlespeech.s2t.utils.utility import add_arguments +from paddlespeech.s2t.utils.utility import print_arguments + + +def start_server(config, args): + """Start the ASR server""" + config.defrost() + config.manifest = config.test_manifest + dataset = ManifestDataset.from_config(config) + + config.augmentation_config = "" + config.keep_transcription_text = True + config.batch_size = 1 + config.num_workers = 0 + collate_fn = SpeechCollator.from_config(config) + test_loader = DataLoader(dataset, collate_fn=collate_fn, num_workers=0) + + model = DeepSpeech2Model.from_pretrained(test_loader, config, + args.checkpoint_path) + model.eval() + + # prepare ASR inference handler + def file_to_transcript(filename): + feature = test_loader.collate_fn.process_utterance(filename, "") + audio = np.array([feature[0]]).astype('float32') #[1, T, D] + # audio = audio.swapaxes(1,2) + print('---file_to_transcript feature----') + print(audio.shape) + audio_len = feature[0].shape[0] + print(audio_len) + audio_len = np.array([audio_len]).astype('int64') # [1] + + result_transcript = model.decode( + paddle.to_tensor(audio), + paddle.to_tensor(audio_len), + vocab_list=test_loader.collate_fn.vocab_list, + decoding_method=config.decode.decoding_method, + lang_model_path=config.decode.lang_model_path, + beam_alpha=config.decode.alpha, + beam_beta=config.decode.beta, + beam_size=config.decode.beam_size, + cutoff_prob=config.decode.cutoff_prob, + cutoff_top_n=config.decode.cutoff_top_n, + num_processes=config.decode.num_proc_bsearch) + return result_transcript[0] + + # warming up with utterrances sampled from Librispeech + print('-----------------------------------------------------------') + print('Warming up ...') + warm_up_test( + audio_process_handler=file_to_transcript, + manifest_path=args.warmup_manifest, + num_test_cases=3) + print('-----------------------------------------------------------') + + # start the server + server = AsrTCPServer( + server_address=(args.host_ip, args.host_port), + RequestHandlerClass=AsrRequestHandler, + speech_save_dir=args.speech_save_dir, + audio_process_handler=file_to_transcript) + print("ASR Server Started.") + server.serve_forever() + + +def main(config, args): + start_server(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + add_arg = functools.partial(add_arguments, argparser=parser) + # yapf: disable + add_arg('host_ip', str, + 'localhost', + "Server's IP address.") + add_arg('host_port', int, 8088, "Server's IP port.") + add_arg('speech_save_dir', str, + 'demo_cache', + "Directory to save demo audios.") + add_arg('warmup_manifest', str, None, "Filepath of manifest to warm up.") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + + args.warmup_manifest = config.test_manifest + print_arguments(args, globals()) + + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/export.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/export.py new file mode 100644 index 0000000..ee013d7 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/export.py @@ -0,0 +1,56 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Export for DeepSpeech2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.deepspeech2.model import DeepSpeech2Tester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_export() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save jit model to + parser.add_argument( + "--export_path", type=str, help="path of the jit model to save") + parser.add_argument( + "--model_type", type=str, default='offline', help="offline/online") + args = parser.parse_args() + print("model_type:{}".format(args.model_type)) + print_arguments(args) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test.py new file mode 100644 index 0000000..388b380 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test.py @@ -0,0 +1,60 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for DeepSpeech2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.deepspeech2.model import DeepSpeech2Tester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_test() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + parser.add_argument( + "--model_type", type=str, default='offline', help='offline/online') + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + args = parser.parse_args() + print_arguments(args, globals()) + print("model_type:{}".format(args.model_type)) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_export.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_export.py new file mode 100644 index 0000000..707eb9e --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_export.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for DeepSpeech2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.deepspeech2.model import DeepSpeech2ExportTester as ExportTester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = ExportTester(config, args) + with exp.eval(): + exp.setup() + exp.run_test() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + #load jit model from + parser.add_argument( + "--export_path", type=str, help="path of the jit model to save") + parser.add_argument( + "--model_type", type=str, default='offline', help='offline/online') + parser.add_argument( + "--enable-auto-log", action="store_true", help="use auto log") + args = parser.parse_args() + print_arguments(args, globals()) + print("model_type:{}".format(args.model_type)) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_wav.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_wav.py new file mode 100644 index 0000000..a909dd4 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/test_wav.py @@ -0,0 +1,205 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for DeepSpeech2 model.""" +import os +import sys +from pathlib import Path + +import paddle +import soundfile +from yacs.config import CfgNode + +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.io.collator import SpeechCollator +from paddlespeech.s2t.models.ds2 import DeepSpeech2Model +from paddlespeech.s2t.models.ds2_online import DeepSpeech2ModelOnline +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.checkpoint import Checkpoint +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import print_arguments +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class DeepSpeech2Tester_hub(): + def __init__(self, config, args): + self.args = args + self.config = config + self.audio_file = args.audio_file + self.collate_fn_test = SpeechCollator.from_config(config) + self._text_featurizer = TextFeaturizer( + unit_type=config.unit_type, vocab=None) + + def compute_result_transcripts(self, audio, audio_len, vocab_list, cfg): + result_transcripts = self.model.decode( + audio, + audio_len, + vocab_list, + decoding_method=cfg.decoding_method, + lang_model_path=cfg.lang_model_path, + beam_alpha=cfg.alpha, + beam_beta=cfg.beta, + beam_size=cfg.beam_size, + cutoff_prob=cfg.cutoff_prob, + cutoff_top_n=cfg.cutoff_top_n, + num_processes=cfg.num_proc_bsearch) + + return result_transcripts + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + self.model.eval() + cfg = self.config + audio_file = self.audio_file + collate_fn_test = self.collate_fn_test + audio, _ = collate_fn_test.process_utterance( + audio_file=audio_file, transcript=" ") + audio_len = audio.shape[0] + audio = paddle.to_tensor(audio, dtype='float32') + audio_len = paddle.to_tensor(audio_len) + audio = paddle.unsqueeze(audio, axis=0) + vocab_list = collate_fn_test.vocab_list + result_transcripts = self.compute_result_transcripts( + audio, audio_len, vocab_list, cfg.decode) + logger.info("result_transcripts: " + result_transcripts[0]) + + def run_test(self): + self.resume() + try: + self.test() + except KeyboardInterrupt: + exit(-1) + + def setup(self): + """Setup the experiment. + """ + paddle.set_device('gpu' if self.args.ngpu > 0 else 'cpu') + + self.setup_output_dir() + self.setup_checkpointer() + + self.setup_model() + + def setup_output_dir(self): + """Create a directory used for output. + """ + # output dir + if self.args.output: + output_dir = Path(self.args.output).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + else: + output_dir = Path( + self.args.checkpoint_path).expanduser().parent.parent + output_dir.mkdir(parents=True, exist_ok=True) + self.output_dir = output_dir + + def setup_model(self): + config = self.config.clone() + with UpdateConfig(config): + config.input_dim = self.collate_fn_test.feature_size + config.output_dim = self.collate_fn_test.vocab_size + + if self.args.model_type == 'offline': + model = DeepSpeech2Model.from_config(config) + elif self.args.model_type == 'online': + model = DeepSpeech2ModelOnline.from_config(config) + else: + raise Exception("wrong model type") + + self.model = model + + def setup_checkpointer(self): + """Create a directory used to save checkpoints into. + + It is "checkpoints" inside the output directory. + """ + # checkpoint dir + checkpoint_dir = self.output_dir / "checkpoints" + checkpoint_dir.mkdir(exist_ok=True) + + self.checkpoint_dir = checkpoint_dir + + self.checkpoint = Checkpoint( + kbest_n=self.config.checkpoint.kbest_n, + latest_n=self.config.checkpoint.latest_n) + + def resume(self): + """Resume from the checkpoint at checkpoints in the output + directory or load a specified checkpoint. + """ + params_path = self.args.checkpoint_path + ".pdparams" + model_dict = paddle.load(params_path) + self.model.set_state_dict(model_dict) + + +def check(audio_file): + logger.info("checking the audio file format......") + try: + sig, sample_rate = soundfile.read(audio_file) + except Exception as e: + logger.error(str(e)) + logger.error( + "can not open the wav file, please check the audio file format") + sys.exit(-1) + logger.info("The sample rate is %d" % sample_rate) + assert (sample_rate == 16000) + logger.info("The audio file format is right") + + +def main_sp(config, args): + exp = DeepSpeech2Tester_hub(config, args) + exp.setup() + exp.run_test() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + parser.add_argument( + "--model_type", type=str, default='offline', help='offline/online') + parser.add_argument("--audio_file", type=str, help='audio file path') + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + args = parser.parse_args() + print_arguments(args, globals()) + if not os.path.isfile(args.audio_file): + print("Please input the audio file path") + sys.exit(-1) + check(args.audio_file) + print("model_type:{}".format(args.model_type)) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/train.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/train.py new file mode 100644 index 0000000..09e8662 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/bin/train.py @@ -0,0 +1,56 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Trainer for DeepSpeech2 model.""" +from paddle import distributed as dist +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.deepspeech2.model import DeepSpeech2Trainer as Trainer +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Trainer(config, args) + exp.setup() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + parser.add_argument( + "--model_type", type=str, default='offline', help='offline/online') + args = parser.parse_args() + print("model_type:{}".format(args.model_type)) + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/deepspeech2/model.py b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/model.py new file mode 100644 index 0000000..3e9ede7 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/deepspeech2/model.py @@ -0,0 +1,649 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains DeepSpeech2 and DeepSpeech2Online model.""" +import os +import time +from collections import defaultdict +from contextlib import nullcontext + +import jsonlines +import numpy as np +import paddle +from paddle import distributed as dist +from paddle import inference +from paddle.io import DataLoader + +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.io.collator import SpeechCollator +from paddlespeech.s2t.io.dataset import ManifestDataset +from paddlespeech.s2t.io.sampler import SortagradBatchSampler +from paddlespeech.s2t.io.sampler import SortagradDistributedBatchSampler +from paddlespeech.s2t.models.ds2 import DeepSpeech2InferModel +from paddlespeech.s2t.models.ds2 import DeepSpeech2Model +from paddlespeech.s2t.models.ds2_online import DeepSpeech2InferModelOnline +from paddlespeech.s2t.models.ds2_online import DeepSpeech2ModelOnline +from paddlespeech.s2t.training.gradclip import ClipGradByGlobalNormWithLog +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.utils import error_rate +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class DeepSpeech2Trainer(Trainer): + def __init__(self, config, args): + super().__init__(config, args) + + def train_batch(self, batch_index, batch_data, msg): + batch_size = self.config.batch_size + accum_grad = self.config.accum_grad + + start = time.time() + + # forward + utt, audio, audio_len, text, text_len = batch_data + loss = self.model(audio, audio_len, text, text_len) + losses_np = { + 'train_loss': float(loss), + } + + # loss backward + if (batch_index + 1) % accum_grad != 0: + # Disable gradient synchronizations across DDP processes. + # Within this context, gradients will be accumulated on module + # variables, which will later be synchronized. + context = self.model.no_sync if (hasattr(self.model, "no_sync") and + self.parallel) else nullcontext + else: + # Used for single gpu training and DDP gradient synchronization + # processes. + context = nullcontext + + with context(): + loss.backward() + layer_tools.print_grads(self.model, print_func=None) + + # optimizer step + if (batch_index + 1) % accum_grad == 0: + self.optimizer.step() + self.optimizer.clear_grad() + self.iteration += 1 + + iteration_time = time.time() - start + + for k, v in losses_np.items(): + report(k, v) + report("batch_size", batch_size) + report("accum", accum_grad) + report("step_cost", iteration_time) + + if dist.get_rank() == 0 and self.visualizer: + for k, v in losses_np.items(): + # `step -1` since we update `step` after optimizer.step(). + self.visualizer.add_scalar("train/{}".format(k), v, + self.iteration - 1) + + @paddle.no_grad() + def valid(self): + logger.info(f"Valid Total Examples: {len(self.valid_loader.dataset)}") + self.model.eval() + valid_losses = defaultdict(list) + num_seen_utts = 1 + total_loss = 0.0 + for i, batch in enumerate(self.valid_loader): + utt, audio, audio_len, text, text_len = batch + loss = self.model(audio, audio_len, text, text_len) + if paddle.isfinite(loss): + num_utts = batch[1].shape[0] + num_seen_utts += num_utts + total_loss += float(loss) * num_utts + valid_losses['val_loss'].append(float(loss)) + + if (i + 1) % self.config.log_interval == 0: + valid_dump = {k: np.mean(v) for k, v in valid_losses.items()} + valid_dump['val_history_loss'] = total_loss / num_seen_utts + + # logging + msg = f"Valid: Rank: {dist.get_rank()}, " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "batch : {}/{}, ".format(i + 1, len(self.valid_loader)) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in valid_dump.items()) + logger.info(msg) + + logger.info('Rank {} Val info val_loss {}'.format( + dist.get_rank(), total_loss / num_seen_utts)) + return total_loss, num_seen_utts + + def setup_model(self): + config = self.config.clone() + with UpdateConfig(config): + if self.train: + config.input_dim = self.train_loader.collate_fn.feature_size + config.output_dim = self.train_loader.collate_fn.vocab_size + else: + config.input_dim = self.test_loader.collate_fn.feature_size + config.output_dim = self.test_loader.collate_fn.vocab_size + + if self.args.model_type == 'offline': + model = DeepSpeech2Model.from_config(config) + elif self.args.model_type == 'online': + model = DeepSpeech2ModelOnline.from_config(config) + else: + raise Exception("wrong model type") + if self.parallel: + model = paddle.DataParallel(model) + + logger.info(f"{model}") + layer_tools.print_params(model, logger.info) + self.model = model + logger.info("Setup model!") + + if not self.train: + return + + grad_clip = ClipGradByGlobalNormWithLog(config.global_grad_clip) + lr_scheduler = paddle.optimizer.lr.ExponentialDecay( + learning_rate=config.lr, gamma=config.lr_decay, verbose=True) + optimizer = paddle.optimizer.Adam( + learning_rate=lr_scheduler, + parameters=model.parameters(), + weight_decay=paddle.regularizer.L2Decay(config.weight_decay), + grad_clip=grad_clip) + self.optimizer = optimizer + self.lr_scheduler = lr_scheduler + logger.info("Setup optimizer/lr_scheduler!") + + def setup_dataloader(self): + config = self.config.clone() + config.defrost() + if self.train: + # train + config.manifest = config.train_manifest + train_dataset = ManifestDataset.from_config(config) + if self.parallel: + batch_sampler = SortagradDistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + num_replicas=None, + rank=None, + shuffle=True, + drop_last=True, + sortagrad=config.sortagrad, + shuffle_method=config.shuffle_method) + else: + batch_sampler = SortagradBatchSampler( + train_dataset, + shuffle=True, + batch_size=config.batch_size, + drop_last=True, + sortagrad=config.sortagrad, + shuffle_method=config.shuffle_method) + + config.keep_transcription_text = False + collate_fn_train = SpeechCollator.from_config(config) + self.train_loader = DataLoader( + train_dataset, + batch_sampler=batch_sampler, + collate_fn=collate_fn_train, + num_workers=config.num_workers) + + # dev + config.manifest = config.dev_manifest + dev_dataset = ManifestDataset.from_config(config) + + config.augmentation_config = "" + config.keep_transcription_text = False + collate_fn_dev = SpeechCollator.from_config(config) + self.valid_loader = DataLoader( + dev_dataset, + batch_size=int(config.batch_size), + shuffle=False, + drop_last=False, + collate_fn=collate_fn_dev, + num_workers=config.num_workers) + logger.info("Setup train/valid Dataloader!") + else: + # test + config.manifest = config.test_manifest + test_dataset = ManifestDataset.from_config(config) + + config.augmentation_config = "" + config.keep_transcription_text = True + collate_fn_test = SpeechCollator.from_config(config) + decode_batch_size = config.get('decode', dict()).get( + 'decode_batch_size', 1) + self.test_loader = DataLoader( + test_dataset, + batch_size=decode_batch_size, + shuffle=False, + drop_last=False, + collate_fn=collate_fn_test, + num_workers=config.num_workers) + logger.info("Setup test Dataloader!") + + +class DeepSpeech2Tester(DeepSpeech2Trainer): + def __init__(self, config, args): + super().__init__(config, args) + self._text_featurizer = TextFeaturizer( + unit_type=config.unit_type, vocab=None) + + def ordid2token(self, texts, texts_len): + """ ord() id to chr() chr """ + trans = [] + for text, n in zip(texts, texts_len): + n = n.numpy().item() + ids = text[:n] + trans.append(''.join([chr(i) for i in ids])) + return trans + + def compute_metrics(self, + utts, + audio, + audio_len, + texts, + texts_len, + fout=None): + decode_cfg = self.config.decode + errors_sum, len_refs, num_ins = 0.0, 0, 0 + errors_func = error_rate.char_errors if decode_cfg.error_rate_type == 'cer' else error_rate.word_errors + error_rate_func = error_rate.cer if decode_cfg.error_rate_type == 'cer' else error_rate.wer + + target_transcripts = self.ordid2token(texts, texts_len) + + result_transcripts = self.compute_result_transcripts(audio, audio_len) + + for utt, target, result in zip(utts, target_transcripts, + result_transcripts): + errors, len_ref = errors_func(target, result) + errors_sum += errors + len_refs += len_ref + num_ins += 1 + if fout: + fout.write({"utt": utt, "ref": target, "hyp": result}) + logger.info(f"Utt: {utt}") + logger.info(f"Ref: {target}") + logger.info(f"Hyp: {result}") + logger.info( + "Current error rate [%s] = %f" % + (decode_cfg.error_rate_type, error_rate_func(target, result))) + + return dict( + errors_sum=errors_sum, + len_refs=len_refs, + num_ins=num_ins, + error_rate=errors_sum / len_refs, + error_rate_type=decode_cfg.error_rate_type) + + def compute_result_transcripts(self, audio, audio_len): + result_transcripts = self.model.decode(audio, audio_len) + return result_transcripts + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + logger.info(f"Test Total Examples: {len(self.test_loader.dataset)}") + self.model.eval() + error_rate_type = None + errors_sum, len_refs, num_ins = 0.0, 0, 0 + + # Initialized the decoder in model + decode_cfg = self.config.decode + vocab_list = self.test_loader.collate_fn.vocab_list + decode_batch_size = self.test_loader.batch_size + self.model.decoder.init_decoder( + decode_batch_size, vocab_list, decode_cfg.decoding_method, + decode_cfg.lang_model_path, decode_cfg.alpha, decode_cfg.beta, + decode_cfg.beam_size, decode_cfg.cutoff_prob, + decode_cfg.cutoff_top_n, decode_cfg.num_proc_bsearch) + + with jsonlines.open(self.args.result_file, 'w') as fout: + for i, batch in enumerate(self.test_loader): + utts, audio, audio_len, texts, texts_len = batch + metrics = self.compute_metrics(utts, audio, audio_len, texts, + texts_len, fout) + errors_sum += metrics['errors_sum'] + len_refs += metrics['len_refs'] + num_ins += metrics['num_ins'] + error_rate_type = metrics['error_rate_type'] + logger.info("Error rate [%s] (%d/?) = %f" % + (error_rate_type, num_ins, errors_sum / len_refs)) + + # logging + msg = "Test: " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "Final error rate [%s] (%d/%d) = %f" % ( + error_rate_type, num_ins, num_ins, errors_sum / len_refs) + logger.info(msg) + self.model.decoder.del_decoder() + + @paddle.no_grad() + def export(self): + if self.args.model_type == 'offline': + infer_model = DeepSpeech2InferModel.from_pretrained( + self.test_loader, self.config, self.args.checkpoint_path) + elif self.args.model_type == 'online': + infer_model = DeepSpeech2InferModelOnline.from_pretrained( + self.test_loader, self.config, self.args.checkpoint_path) + else: + raise Exception("wrong model type") + + infer_model.eval() + feat_dim = self.test_loader.collate_fn.feature_size + static_model = infer_model.export() + logger.info(f"Export code: {static_model.forward.code}") + paddle.jit.save(static_model, self.args.export_path) + + +class DeepSpeech2ExportTester(DeepSpeech2Tester): + def __init__(self, config, args): + super().__init__(config, args) + self.apply_static = True + self.args = args + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + logger.info(f"Test Total Examples: {len(self.test_loader.dataset)}") + if self.args.enable_auto_log is True: + from paddlespeech.s2t.utils.log import Autolog + self.autolog = Autolog( + batch_size=self.config.decode.decode_batch_size, + model_name="deepspeech2", + model_precision="fp32").getlog() + self.model.eval() + error_rate_type = None + errors_sum, len_refs, num_ins = 0.0, 0, 0 + + # Initialized the decoder in model + decode_cfg = self.config.decode + vocab_list = self.test_loader.collate_fn.vocab_list + if self.args.model_type == "online": + decode_batch_size = 1 + elif self.args.model_type == "offline": + decode_batch_size = self.test_loader.batch_size + else: + raise Exception("wrong model type") + self.model.decoder.init_decoder( + decode_batch_size, vocab_list, decode_cfg.decoding_method, + decode_cfg.lang_model_path, decode_cfg.alpha, decode_cfg.beta, + decode_cfg.beam_size, decode_cfg.cutoff_prob, + decode_cfg.cutoff_top_n, decode_cfg.num_proc_bsearch) + + with jsonlines.open(self.args.result_file, 'w') as fout: + for i, batch in enumerate(self.test_loader): + utts, audio, audio_len, texts, texts_len = batch + metrics = self.compute_metrics(utts, audio, audio_len, texts, + texts_len, fout) + errors_sum += metrics['errors_sum'] + len_refs += metrics['len_refs'] + num_ins += metrics['num_ins'] + error_rate_type = metrics['error_rate_type'] + logger.info("Error rate [%s] (%d/?) = %f" % + (error_rate_type, num_ins, errors_sum / len_refs)) + # logging + msg = "Test: " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "Final error rate [%s] (%d/%d) = %f" % ( + error_rate_type, num_ins, num_ins, errors_sum / len_refs) + logger.info(msg) + if self.args.enable_auto_log is True: + self.autolog.report() + self.model.decoder.del_decoder() + + def compute_result_transcripts(self, audio, audio_len): + if self.args.model_type == "online": + output_probs, output_lens, trans_batch = self.static_forward_online( + audio, audio_len, decoder_chunk_size=1) + result_transcripts = [trans[-1] for trans in trans_batch] + elif self.args.model_type == "offline": + output_probs, output_lens = self.static_forward_offline(audio, + audio_len) + batch_size = output_probs.shape[0] + self.model.decoder.reset_decoder(batch_size=batch_size) + + self.model.decoder.next(output_probs, output_lens) + + trans_best, trans_beam = self.model.decoder.decode() + + result_transcripts = trans_best + + else: + raise Exception("wrong model type") + + self.predictor.clear_intermediate_tensor() + self.predictor.try_shrink_memory() + + #replace the with ' ' + result_transcripts = [ + self._text_featurizer.detokenize(sentence) + for sentence in result_transcripts + ] + + return result_transcripts + + def run_test(self): + """Do Test/Decode""" + try: + with Timer("Test/Decode Done: {}"): + with self.eval(): + self.test() + except KeyboardInterrupt: + exit(-1) + + def static_forward_online(self, audio, audio_len, + decoder_chunk_size: int=1): + """ + Parameters + ---------- + audio (Tensor): shape[B, T, D] + audio_len (Tensor): shape[B] + decoder_chunk_size(int) + Returns + ------- + output_probs(numpy.array): shape[B, T, vocab_size] + output_lens(numpy.array): shape[B] + trans(list(list(str))): shape[B, T] + """ + output_probs_list = [] + output_lens_list = [] + subsampling_rate = self.model.encoder.conv.subsampling_rate + receptive_field_length = self.model.encoder.conv.receptive_field_length + chunk_stride = subsampling_rate * decoder_chunk_size + chunk_size = (decoder_chunk_size - 1 + ) * subsampling_rate + receptive_field_length + + x_batch = audio.numpy() + batch_size, Tmax, x_dim = x_batch.shape + x_len_batch = audio_len.numpy().astype(np.int64) + if (Tmax - chunk_size) % chunk_stride != 0: + # The length of padding for the batch + padding_len_batch = chunk_stride - (Tmax - chunk_size + ) % chunk_stride + else: + padding_len_batch = 0 + x_list = np.split(x_batch, batch_size, axis=0) + x_len_list = np.split(x_len_batch, batch_size, axis=0) + + trans_batch = [] + for x, x_len in zip(x_list, x_len_list): + if self.args.enable_auto_log is True: + self.autolog.times.start() + x_len = x_len[0] + assert (chunk_size <= x_len) + + if (x_len - chunk_size) % chunk_stride != 0: + padding_len_x = chunk_stride - (x_len - chunk_size + ) % chunk_stride + else: + padding_len_x = 0 + + padding = np.zeros( + (x.shape[0], padding_len_x, x.shape[2]), dtype=x.dtype) + padded_x = np.concatenate([x, padding], axis=1) + + num_chunk = (x_len + padding_len_x - chunk_size) / chunk_stride + 1 + num_chunk = int(num_chunk) + + chunk_state_h_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=x.dtype) + chunk_state_c_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=x.dtype) + + input_names = self.predictor.get_input_names() + audio_handle = self.predictor.get_input_handle(input_names[0]) + audio_len_handle = self.predictor.get_input_handle(input_names[1]) + h_box_handle = self.predictor.get_input_handle(input_names[2]) + c_box_handle = self.predictor.get_input_handle(input_names[3]) + + trans = [] + probs_chunk_list = [] + probs_chunk_lens_list = [] + if self.args.enable_auto_log is True: + # record the model preprocessing time + self.autolog.times.stamp() + + self.model.decoder.reset_decoder(batch_size=1) + for i in range(0, num_chunk): + start = i * chunk_stride + end = start + chunk_size + x_chunk = padded_x[:, start:end, :] + if x_len < i * chunk_stride: + x_chunk_lens = 0 + else: + x_chunk_lens = min(x_len - i * chunk_stride, chunk_size) + #means the number of input frames in the chunk is not enough for predicting one prob + if (x_chunk_lens < receptive_field_length): + break + x_chunk_lens = np.array([x_chunk_lens]) + audio_handle.reshape(x_chunk.shape) + audio_handle.copy_from_cpu(x_chunk) + + audio_len_handle.reshape(x_chunk_lens.shape) + audio_len_handle.copy_from_cpu(x_chunk_lens) + + h_box_handle.reshape(chunk_state_h_box.shape) + h_box_handle.copy_from_cpu(chunk_state_h_box) + + c_box_handle.reshape(chunk_state_c_box.shape) + c_box_handle.copy_from_cpu(chunk_state_c_box) + + output_names = self.predictor.get_output_names() + output_handle = self.predictor.get_output_handle( + output_names[0]) + output_lens_handle = self.predictor.get_output_handle( + output_names[1]) + output_state_h_handle = self.predictor.get_output_handle( + output_names[2]) + output_state_c_handle = self.predictor.get_output_handle( + output_names[3]) + self.predictor.run() + output_chunk_probs = output_handle.copy_to_cpu() + output_chunk_lens = output_lens_handle.copy_to_cpu() + chunk_state_h_box = output_state_h_handle.copy_to_cpu() + chunk_state_c_box = output_state_c_handle.copy_to_cpu() + self.model.decoder.next(output_chunk_probs, output_chunk_lens) + probs_chunk_list.append(output_chunk_probs) + probs_chunk_lens_list.append(output_chunk_lens) + trans_best, trans_beam = self.model.decoder.decode() + trans.append(trans_best[0]) + trans_batch.append(trans) + output_probs = np.concatenate(probs_chunk_list, axis=1) + output_lens = np.sum(probs_chunk_lens_list, axis=0) + vocab_size = output_probs.shape[2] + output_probs_padding_len = Tmax + padding_len_batch - output_probs.shape[ + 1] + output_probs_padding = np.zeros( + (1, output_probs_padding_len, vocab_size), + dtype=output_probs. + dtype) # The prob padding for a piece of utterance + output_probs = np.concatenate( + [output_probs, output_probs_padding], axis=1) + output_probs_list.append(output_probs) + output_lens_list.append(output_lens) + if self.args.enable_auto_log is True: + # record the model inference time + self.autolog.times.stamp() + # record the post processing time + self.autolog.times.stamp() + self.autolog.times.end() + output_probs = np.concatenate(output_probs_list, axis=0) + output_lens = np.concatenate(output_lens_list, axis=0) + return output_probs, output_lens, trans_batch + + def static_forward_offline(self, audio, audio_len): + """ + Parameters + ---------- + audio (Tensor): shape[B, T, D] + audio_len (Tensor): shape[B] + + Returns + ------- + output_probs(numpy.array): shape[B, T, vocab_size] + output_lens(numpy.array): shape[B] + """ + x = audio.numpy() + x_len = audio_len.numpy().astype(np.int64) + + input_names = self.predictor.get_input_names() + audio_handle = self.predictor.get_input_handle(input_names[0]) + audio_len_handle = self.predictor.get_input_handle(input_names[1]) + + audio_handle.reshape(x.shape) + audio_handle.copy_from_cpu(x) + + audio_len_handle.reshape(x_len.shape) + audio_len_handle.copy_from_cpu(x_len) + + if self.args.enable_auto_log is True: + self.autolog.times.start() + # record the prefix processing time + self.autolog.times.stamp() + self.predictor.run() + if self.args.enable_auto_log is True: + # record the model inference time + self.autolog.times.stamp() + # record the post processing time + self.autolog.times.stamp() + self.autolog.times.end() + + output_names = self.predictor.get_output_names() + output_handle = self.predictor.get_output_handle(output_names[0]) + output_lens_handle = self.predictor.get_output_handle(output_names[1]) + output_probs = output_handle.copy_to_cpu() + output_lens = output_lens_handle.copy_to_cpu() + return output_probs, output_lens + + def setup_model(self): + super().setup_model() + deepspeech_config = inference.Config( + self.args.export_path + ".pdmodel", + self.args.export_path + ".pdiparams") + if (os.environ['CUDA_VISIBLE_DEVICES'].strip() != ''): + deepspeech_config.enable_use_gpu(100, 0) + deepspeech_config.enable_memory_optim() + deepspeech_predictor = inference.create_predictor(deepspeech_config) + self.predictor = deepspeech_predictor diff --git a/ernie-sat/paddlespeech/s2t/exps/lm/transformer/__init__.py b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/__init__.py b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/cacu_perplexity.py b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/cacu_perplexity.py new file mode 100644 index 0000000..f3e4d20 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/bin/cacu_perplexity.py @@ -0,0 +1,82 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +import configargparse + + +def get_parser(): + """Get default arguments.""" + parser = configargparse.ArgumentParser( + description="The parser for caculating the perplexity of transformer language model ", + config_file_parser_class=configargparse.YAMLConfigFileParser, + formatter_class=configargparse.ArgumentDefaultsHelpFormatter, ) + + parser.add_argument( + "--rnnlm", type=str, default=None, help="RNNLM model file to read") + + parser.add_argument( + "--rnnlm-conf", + type=str, + default=None, + help="RNNLM model config file to read") + + parser.add_argument( + "--vocab_path", + type=str, + default=None, + help="vocab path to for token2id") + + parser.add_argument( + "--bpeprefix", + type=str, + default=None, + help="The path of bpeprefix for loading") + + parser.add_argument( + "--text_path", + type=str, + default=None, + help="The path of text file for testing ") + + parser.add_argument( + "--ngpu", + type=int, + default=0, + help="The number of gpu to use, 0 for using cpu instead") + + parser.add_argument( + "--dtype", + choices=("float16", "float32", "float64"), + default="float32", + help="Float precision (only available in --api v2)", ) + + parser.add_argument( + "--output_dir", + type=str, + default=".", + help="The output directory to store the sentence PPL") + + return parser + + +def main(args): + parser = get_parser() + args = parser.parse_args(args) + from paddlespeech.s2t.exps.lm.transformer.lm_cacu_perplexity import run_get_perplexity + run_get_perplexity(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/ernie-sat/paddlespeech/s2t/exps/lm/transformer/lm_cacu_perplexity.py b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/lm_cacu_perplexity.py new file mode 100644 index 0000000..e628f32 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/lm/transformer/lm_cacu_perplexity.py @@ -0,0 +1,132 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Caculating the PPL of LM model +import os + +import numpy as np +import paddle +from paddle.io import DataLoader +from yacs.config import CfgNode + +from paddlespeech.s2t.models.lm.dataset import TextCollatorSpm +from paddlespeech.s2t.models.lm.dataset import TextDataset +from paddlespeech.s2t.models.lm_interface import dynamic_import_lm +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + + +def get_config(config_path): + confs = CfgNode(new_allowed=True) + confs.merge_from_file(config_path) + return confs + + +def load_trained_lm(args): + lm_config = get_config(args.rnnlm_conf) + lm_model_module = lm_config.model_module + lm_class = dynamic_import_lm(lm_model_module) + lm = lm_class(**lm_config.model) + model_dict = paddle.load(args.rnnlm) + lm.set_state_dict(model_dict) + return lm, lm_config + + +def write_dict_into_file(ppl_dict, name): + with open(name, "w") as f: + for key in ppl_dict.keys(): + f.write(key + " " + ppl_dict[key] + "\n") + return + + +def cacu_perplexity( + lm_model, + lm_config, + args, + log_base=None, ): + unit_type = lm_config.data.unit_type + batch_size = lm_config.decoding.batch_size + num_workers = lm_config.decoding.num_workers + text_file_path = args.text_path + + total_nll = 0.0 + total_ntokens = 0 + ppl_dict = {} + len_dict = {} + text_dataset = TextDataset.from_file(text_file_path) + collate_fn_text = TextCollatorSpm( + unit_type=unit_type, + vocab_filepath=args.vocab_path, + spm_model_prefix=args.bpeprefix) + train_loader = DataLoader( + text_dataset, + batch_size=batch_size, + collate_fn=collate_fn_text, + num_workers=num_workers) + + logger.info("start caculating PPL......") + for i, (keys, ys_input_pad, ys_output_pad, + y_lens) in enumerate(train_loader()): + + ys_input_pad = paddle.to_tensor(ys_input_pad) + ys_output_pad = paddle.to_tensor(ys_output_pad) + _, unused_logp, unused_count, nll, nll_count = lm_model.forward( + ys_input_pad, ys_output_pad) + nll = nll.numpy() + nll_count = nll_count.numpy() + for key, _nll, ntoken in zip(keys, nll, nll_count): + if log_base is None: + utt_ppl = np.exp(_nll / ntoken) + else: + utt_ppl = log_base**(_nll / ntoken / np.log(log_base)) + + # Write PPL of each utts for debugging or analysis + ppl_dict[key] = str(utt_ppl) + len_dict[key] = str(ntoken) + + total_nll += nll.sum() + total_ntokens += nll_count.sum() + logger.info("Current total nll: " + str(total_nll)) + logger.info("Current total tokens: " + str(total_ntokens)) + write_dict_into_file(ppl_dict, os.path.join(args.output_dir, "uttPPL")) + write_dict_into_file(len_dict, os.path.join(args.output_dir, "uttLEN")) + if log_base is None: + ppl = np.exp(total_nll / total_ntokens) + else: + ppl = log_base**(total_nll / total_ntokens / np.log(log_base)) + + if log_base is None: + log_base = np.e + else: + log_base = log_base + + return ppl, log_base + + +def run_get_perplexity(args): + if args.ngpu > 1: + raise NotImplementedError("only single GPU decoding is supported") + if args.ngpu == 1: + device = "gpu:0" + else: + device = "cpu" + paddle.set_device(device) + dtype = getattr(paddle, args.dtype) + logger.info(f"Decoding device={device}, dtype={dtype}") + lm_model, lm_config = load_trained_lm(args) + lm_model.to(device=device, dtype=dtype) + lm_model.eval() + PPL, log_base = cacu_perplexity(lm_model, lm_config, args, None) + logger.info("Final PPL: " + str(PPL)) + logger.info("The log base is:" + str("%.2f" % log_base)) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/alignment.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/alignment.py new file mode 100644 index 0000000..e3390fe --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/alignment.py @@ -0,0 +1,57 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Alignment for U2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2.model import U2Tester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_align() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/export.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/export.py new file mode 100644 index 0000000..592b123 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/export.py @@ -0,0 +1,53 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Export for U2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2.model import U2Tester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_export() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save jit model to + parser.add_argument( + "--export_path", type=str, help="path of the jit model to save") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/test.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/test.py new file mode 100644 index 0000000..f14d804 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/test.py @@ -0,0 +1,64 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for U2 model.""" +import cProfile + +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2.model import U2Tester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + +# TODO(hui zhang): dynamic load + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_test() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats('test.profile') diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/test_wav.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/test_wav.py new file mode 100644 index 0000000..9904813 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/test_wav.py @@ -0,0 +1,141 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for U2 model.""" +import os +import sys +from pathlib import Path + +import paddle +import soundfile +from yacs.config import CfgNode + +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.models.u2 import U2Model +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.transform.transformation import Transformation +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig +logger = Log(__name__).getlog() + +# TODO(hui zhang): dynamic load + + +class U2Infer(): + def __init__(self, config, args): + self.args = args + self.config = config + self.audio_file = args.audio_file + + self.preprocess_conf = config.preprocess_config + self.preprocess_args = {"train": False} + self.preprocessing = Transformation(self.preprocess_conf) + + self.text_feature = TextFeaturizer( + unit_type=config.unit_type, + vocab=config.vocab_filepath, + spm_model_prefix=config.spm_model_prefix) + + paddle.set_device('gpu' if self.args.ngpu > 0 else 'cpu') + + # model + model_conf = config + with UpdateConfig(model_conf): + model_conf.input_dim = config.feat_dim + model_conf.output_dim = self.text_feature.vocab_size + model = U2Model.from_config(model_conf) + self.model = model + self.model.eval() + + # load model + params_path = self.args.checkpoint_path + ".pdparams" + model_dict = paddle.load(params_path) + self.model.set_state_dict(model_dict) + + def run(self): + check(args.audio_file) + + with paddle.no_grad(): + # read + audio, sample_rate = soundfile.read( + self.audio_file, dtype="int16", always_2d=True) + + audio = audio[:, 0] + logger.info(f"audio shape: {audio.shape}") + + # fbank + feat = self.preprocessing(audio, **self.preprocess_args) + logger.info(f"feat shape: {feat.shape}") + + ilen = paddle.to_tensor(feat.shape[0]) + xs = paddle.to_tensor(feat, dtype='float32').unsqueeze(axis=0) + + decode_config = self.config.decode + result_transcripts = self.model.decode( + xs, + ilen, + text_feature=self.text_feature, + decoding_method=decode_config.decoding_method, + beam_size=decode_config.beam_size, + ctc_weight=decode_config.ctc_weight, + decoding_chunk_size=decode_config.decoding_chunk_size, + num_decoding_left_chunks=decode_config.num_decoding_left_chunks, + simulate_streaming=decode_config.simulate_streaming) + rsl = result_transcripts[0][0] + utt = Path(self.audio_file).name + logger.info(f"hyp: {utt} {result_transcripts[0][0]}") + return rsl + + +def check(audio_file): + if not os.path.isfile(audio_file): + print("Please input the right audio file path") + sys.exit(-1) + + logger.info("checking the audio file format......") + try: + sig, sample_rate = soundfile.read(audio_file) + except Exception as e: + logger.error(str(e)) + logger.error( + "can not open the wav file, please check the audio file format") + sys.exit(-1) + logger.info("The sample rate is %d" % sample_rate) + assert (sample_rate == 16000) + logger.info("The audio file format is right") + + +def main(config, args): + U2Infer(config, args).run() + + +if __name__ == "__main__": + parser = default_argument_parser() + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + parser.add_argument( + "--audio_file", type=str, help="path of the input audio file") + args = parser.parse_args() + + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/bin/train.py b/ernie-sat/paddlespeech/s2t/exps/u2/bin/train.py new file mode 100644 index 0000000..53c2232 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/bin/train.py @@ -0,0 +1,61 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Trainer for U2 model.""" +import cProfile +import os + +from paddle import distributed as dist +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2.model import U2Trainer as Trainer +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + +# from paddlespeech.s2t.exps.u2.trainer import U2Trainer as Trainer + + +def main_sp(config, args): + exp = Trainer(config, args) + exp.setup() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats(os.path.join(args.output, 'train.profile')) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/model.py b/ernie-sat/paddlespeech/s2t/exps/u2/model.py new file mode 100644 index 0000000..efcc962 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/model.py @@ -0,0 +1,546 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains U2 model.""" +import json +import os +import time +from collections import defaultdict +from collections import OrderedDict +from contextlib import nullcontext + +import jsonlines +import numpy as np +import paddle +from paddle import distributed as dist + +from paddlespeech.s2t.frontend.featurizer import TextFeaturizer +from paddlespeech.s2t.io.dataloader import BatchDataLoader +from paddlespeech.s2t.models.u2 import U2Model +from paddlespeech.s2t.training.optimizer import OptimizerFactory +from paddlespeech.s2t.training.reporter import ObsScope +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.scheduler import LRSchedulerFactory +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.utils import ctc_utils +from paddlespeech.s2t.utils import error_rate +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class U2Trainer(Trainer): + def __init__(self, config, args): + super().__init__(config, args) + + def train_batch(self, batch_index, batch_data, msg): + train_conf = self.config + start = time.time() + + # forward + utt, audio, audio_len, text, text_len = batch_data + loss, attention_loss, ctc_loss = self.model(audio, audio_len, text, + text_len) + + # loss div by `batch_size * accum_grad` + loss /= train_conf.accum_grad + losses_np = {'loss': float(loss) * train_conf.accum_grad} + if attention_loss: + losses_np['att_loss'] = float(attention_loss) + if ctc_loss: + losses_np['ctc_loss'] = float(ctc_loss) + + # loss backward + if (batch_index + 1) % train_conf.accum_grad != 0: + # Disable gradient synchronizations across DDP processes. + # Within this context, gradients will be accumulated on module + # variables, which will later be synchronized. + # When using cpu w/o DDP, model does not have `no_sync` + context = self.model.no_sync if (hasattr(self.model, "no_sync") and + self.parallel) else nullcontext + else: + # Used for single gpu training and DDP gradient synchronization + # processes. + context = nullcontext + with context(): + loss.backward() + layer_tools.print_grads(self.model, print_func=None) + + # optimizer step + if (batch_index + 1) % train_conf.accum_grad == 0: + self.optimizer.step() + self.optimizer.clear_grad() + self.lr_scheduler.step() + self.iteration += 1 + + iteration_time = time.time() - start + + for k, v in losses_np.items(): + report(k, v) + report("batch_size", self.config.batch_size) + report("accum", train_conf.accum_grad) + report("step_cost", iteration_time) + + if (batch_index + 1) % train_conf.accum_grad == 0: + if dist.get_rank() == 0 and self.visualizer: + losses_np_v = losses_np.copy() + losses_np_v.update({"lr": self.lr_scheduler()}) + for key, val in losses_np_v.items(): + self.visualizer.add_scalar( + tag='train/' + key, value=val, step=self.iteration - 1) + + @paddle.no_grad() + def valid(self): + self.model.eval() + logger.info(f"Valid Total Examples: {len(self.valid_loader.dataset)}") + valid_losses = defaultdict(list) + num_seen_utts = 1 + total_loss = 0.0 + for i, batch in enumerate(self.valid_loader): + utt, audio, audio_len, text, text_len = batch + loss, attention_loss, ctc_loss = self.model(audio, audio_len, text, + text_len) + if paddle.isfinite(loss): + num_utts = batch[1].shape[0] + num_seen_utts += num_utts + total_loss += float(loss) * num_utts + valid_losses['val_loss'].append(float(loss)) + if attention_loss: + valid_losses['val_att_loss'].append(float(attention_loss)) + if ctc_loss: + valid_losses['val_ctc_loss'].append(float(ctc_loss)) + + if (i + 1) % self.config.log_interval == 0: + valid_dump = {k: np.mean(v) for k, v in valid_losses.items()} + valid_dump['val_history_loss'] = total_loss / num_seen_utts + + # logging + msg = f"Valid: Rank: {dist.get_rank()}, " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "batch: {}/{}, ".format(i + 1, len(self.valid_loader)) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in valid_dump.items()) + logger.info(msg) + + logger.info('Rank {} Val info val_loss {}'.format( + dist.get_rank(), total_loss / num_seen_utts)) + return total_loss, num_seen_utts + + def do_train(self): + """The training process control by step.""" + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + # script_model = paddle.jit.to_static(self.model) + # script_model_path = str(self.checkpoint_dir / 'init') + # paddle.jit.save(script_model, script_model_path) + + self.before_train() + + logger.info(f"Train Total Examples: {len(self.train_loader.dataset)}") + while self.epoch < self.config.n_epoch: + with Timer("Epoch-Train Time Cost: {}"): + self.model.train() + try: + data_start_time = time.time() + for batch_index, batch in enumerate(self.train_loader): + dataload_time = time.time() - data_start_time + msg = "Train:" + observation = OrderedDict() + with ObsScope(observation): + report("Rank", dist.get_rank()) + report("epoch", self.epoch) + report('step', self.iteration) + report("lr", self.lr_scheduler()) + self.train_batch(batch_index, batch, msg) + self.after_train_batch() + report('iter', batch_index + 1) + report('total', len(self.train_loader)) + report('reader_cost', dataload_time) + observation['batch_cost'] = observation[ + 'reader_cost'] + observation['step_cost'] + observation['samples'] = observation['batch_size'] + observation['ips,samples/s'] = observation[ + 'batch_size'] / observation['batch_cost'] + for k, v in observation.items(): + msg += f" {k.split(',')[0]}: " + msg += f"{v:>.8f}" if isinstance(v, + float) else f"{v}" + msg += f" {k.split(',')[1]}" if len( + k.split(',')) == 2 else "" + msg += "," + msg = msg[:-1] # remove the last "," + if (batch_index + 1) % self.config.log_interval == 0: + logger.info(msg) + data_start_time = time.time() + except Exception as e: + logger.error(e) + raise e + + with Timer("Eval Time Cost: {}"): + total_loss, num_seen_utts = self.valid() + if dist.get_world_size() > 1: + num_seen_utts = paddle.to_tensor(num_seen_utts) + # the default operator in all_reduce function is sum. + dist.all_reduce(num_seen_utts) + total_loss = paddle.to_tensor(total_loss) + dist.all_reduce(total_loss) + cv_loss = total_loss / num_seen_utts + cv_loss = float(cv_loss) + else: + cv_loss = total_loss / num_seen_utts + + logger.info( + 'Epoch {} Val info val_loss {}'.format(self.epoch, cv_loss)) + if self.visualizer: + self.visualizer.add_scalar( + tag='eval/cv_loss', value=cv_loss, step=self.epoch) + self.visualizer.add_scalar( + tag='eval/lr', value=self.lr_scheduler(), step=self.epoch) + + self.save(tag=self.epoch, infos={'val_loss': cv_loss}) + self.new_epoch() + + def setup_dataloader(self): + config = self.config.clone() + + if self.train: + # train/valid dataset, return token ids + self.train_loader = BatchDataLoader( + json_file=config.train_manifest, + train_mode=True, + sortagrad=config.sortagrad, + batch_size=config.batch_size, + maxlen_in=config.maxlen_in, + maxlen_out=config.maxlen_out, + minibatches=config.minibatches, + mini_batch_size=self.args.ngpu, + batch_count=config.batch_count, + batch_bins=config.batch_bins, + batch_frames_in=config.batch_frames_in, + batch_frames_out=config.batch_frames_out, + batch_frames_inout=config.batch_frames_inout, + preprocess_conf=config.preprocess_config, + n_iter_processes=config.num_workers, + subsampling_factor=1, + num_encs=1, + dist_sampler=config.get('dist_sampler', False), + shortest_first=False) + + self.valid_loader = BatchDataLoader( + json_file=config.dev_manifest, + train_mode=False, + sortagrad=False, + batch_size=config.batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=self.args.ngpu, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config.preprocess_config, + n_iter_processes=config.num_workers, + subsampling_factor=1, + num_encs=1, + dist_sampler=config.get('dist_sampler', False), + shortest_first=False) + logger.info("Setup train/valid Dataloader!") + else: + decode_batch_size = config.get('decode', dict()).get( + 'decode_batch_size', 1) + # test dataset, return raw text + self.test_loader = BatchDataLoader( + json_file=config.test_manifest, + train_mode=False, + sortagrad=False, + batch_size=decode_batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config.preprocess_config, + n_iter_processes=1, + subsampling_factor=1, + num_encs=1) + + self.align_loader = BatchDataLoader( + json_file=config.test_manifest, + train_mode=False, + sortagrad=False, + batch_size=decode_batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config.preprocess_config, + n_iter_processes=1, + subsampling_factor=1, + num_encs=1) + logger.info("Setup test/align Dataloader!") + + def setup_model(self): + config = self.config + model_conf = config + + with UpdateConfig(model_conf): + if self.train: + model_conf.input_dim = self.train_loader.feat_dim + model_conf.output_dim = self.train_loader.vocab_size + else: + model_conf.input_dim = self.test_loader.feat_dim + model_conf.output_dim = self.test_loader.vocab_size + + model = U2Model.from_config(model_conf) + + if self.parallel: + model = paddle.DataParallel(model) + + logger.info(f"{model}") + layer_tools.print_params(model, logger.info) + self.model = model + logger.info("Setup model!") + + if not self.train: + return + + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + + scheduler_args = { + "learning_rate": optim_conf.lr, + "verbose": False, + "warmup_steps": scheduler_conf.warmup_steps, + "gamma": scheduler_conf.lr_decay, + "d_model": model_conf.encoder_conf.output_size, + } + lr_scheduler = LRSchedulerFactory.from_args(scheduler_type, + scheduler_args) + + def optimizer_args( + config, + parameters, + lr_scheduler=None, ): + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + return { + "grad_clip": train_config.global_grad_clip, + "weight_decay": optim_conf.weight_decay, + "learning_rate": lr_scheduler + if lr_scheduler else optim_conf.lr, + "parameters": parameters, + "epsilon": 1e-9 if optim_type == 'noam' else None, + "beta1": 0.9 if optim_type == 'noam' else None, + "beat2": 0.98 if optim_type == 'noam' else None, + } + + optimzer_args = optimizer_args(config, model.parameters(), lr_scheduler) + optimizer = OptimizerFactory.from_args(optim_type, optimzer_args) + + self.optimizer = optimizer + self.lr_scheduler = lr_scheduler + logger.info("Setup optimizer/lr_scheduler!") + + +class U2Tester(U2Trainer): + def __init__(self, config, args): + super().__init__(config, args) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, + vocab=self.config.vocab_filepath, + spm_model_prefix=self.config.spm_model_prefix) + self.vocab_list = self.text_feature.vocab_list + + def id2token(self, texts, texts_len, text_feature): + """ ord() id to chr() chr """ + trans = [] + for text, n in zip(texts, texts_len): + n = n.numpy().item() + ids = text[:n] + trans.append(text_feature.defeaturize(ids.numpy().tolist())) + return trans + + def compute_metrics(self, + utts, + audio, + audio_len, + texts, + texts_len, + fout=None): + decode_config = self.config.decode + errors_sum, len_refs, num_ins = 0.0, 0, 0 + errors_func = error_rate.char_errors if decode_config.error_rate_type == 'cer' else error_rate.word_errors + error_rate_func = error_rate.cer if decode_config.error_rate_type == 'cer' else error_rate.wer + + start_time = time.time() + target_transcripts = self.id2token(texts, texts_len, self.text_feature) + result_transcripts, result_tokenids = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=decode_config.decoding_method, + beam_size=decode_config.beam_size, + ctc_weight=decode_config.ctc_weight, + decoding_chunk_size=decode_config.decoding_chunk_size, + num_decoding_left_chunks=decode_config.num_decoding_left_chunks, + simulate_streaming=decode_config.simulate_streaming) + decode_time = time.time() - start_time + + for utt, target, result, rec_tids in zip( + utts, target_transcripts, result_transcripts, result_tokenids): + errors, len_ref = errors_func(target, result) + errors_sum += errors + len_refs += len_ref + num_ins += 1 + if fout: + fout.write({ + "utt": utt, + "refs": [target], + "hyps": [result], + "hyps_tokenid": [rec_tids], + }) + logger.info(f"Utt: {utt}") + logger.info(f"Ref: {target}") + logger.info(f"Hyp: {result}") + logger.info("One example error rate [%s] = %f" % ( + decode_config.error_rate_type, error_rate_func(target, result))) + + return dict( + errors_sum=errors_sum, + len_refs=len_refs, + num_ins=num_ins, # num examples + error_rate=errors_sum / len_refs, + error_rate_type=decode_config.error_rate_type, + num_frames=audio_len.sum().numpy().item(), + decode_time=decode_time) + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + assert self.args.result_file + self.model.eval() + logger.info(f"Test Total Examples: {len(self.test_loader.dataset)}") + + stride_ms = self.config.stride_ms + error_rate_type = None + errors_sum, len_refs, num_ins = 0.0, 0, 0 + num_frames = 0.0 + num_time = 0.0 + with jsonlines.open(self.args.result_file, 'w') as fout: + for i, batch in enumerate(self.test_loader): + metrics = self.compute_metrics(*batch, fout=fout) + num_frames += metrics['num_frames'] + num_time += metrics["decode_time"] + errors_sum += metrics['errors_sum'] + len_refs += metrics['len_refs'] + num_ins += metrics['num_ins'] + error_rate_type = metrics['error_rate_type'] + rtf = num_time / (num_frames * stride_ms) + logger.info( + "RTF: %f, Error rate [%s] (%d/?) = %f" % + (rtf, error_rate_type, num_ins, errors_sum / len_refs)) + + rtf = num_time / (num_frames * stride_ms) + msg = "Test: " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "RTF: {}, ".format(rtf) + msg += "Final error rate [%s] (%d/%d) = %f" % ( + error_rate_type, num_ins, num_ins, errors_sum / len_refs) + logger.info(msg) + + # test meta results + err_meta_path = os.path.splitext(self.args.result_file)[0] + '.err' + err_type_str = "{}".format(error_rate_type) + with open(err_meta_path, 'w') as f: + data = json.dumps({ + "epoch": + self.epoch, + "step": + self.iteration, + "rtf": + rtf, + error_rate_type: + errors_sum / len_refs, + "dataset_hour": (num_frames * stride_ms) / 1000.0 / 3600.0, + "process_hour": + num_time / 1000.0 / 3600.0, + "num_examples": + num_ins, + "err_sum": + errors_sum, + "ref_len": + len_refs, + "decode_method": + self.config.decode.decoding_method, + }) + f.write(data + '\n') + + @paddle.no_grad() + def align(self): + ctc_utils.ctc_align(self.config, self.model, self.align_loader, + self.config.decode.decode_batch_size, + self.config.stride_ms, self.vocab_list, + self.args.result_file) + + def load_inferspec(self): + """infer model and input spec. + + Returns: + nn.Layer: inference model + List[paddle.static.InputSpec]: input spec. + """ + from paddlespeech.s2t.models.u2 import U2InferModel + infer_model = U2InferModel.from_pretrained(self.test_loader, + self.config.clone(), + self.args.checkpoint_path) + feat_dim = self.test_loader.feat_dim + input_spec = [ + paddle.static.InputSpec(shape=[1, None, feat_dim], + dtype='float32'), # audio, [B,T,D] + paddle.static.InputSpec(shape=[1], + dtype='int64'), # audio_length, [B] + ] + return infer_model, input_spec + + @paddle.no_grad() + def export(self): + infer_model, input_spec = self.load_inferspec() + assert isinstance(input_spec, list), type(input_spec) + infer_model.eval() + static_model = paddle.jit.to_static(infer_model, input_spec=input_spec) + logger.info(f"Export code: {static_model.forward.code}") + paddle.jit.save(static_model, self.args.export_path) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2/trainer.py b/ernie-sat/paddlespeech/s2t/exps/u2/trainer.py new file mode 100644 index 0000000..ab87c30 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2/trainer.py @@ -0,0 +1,219 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains U2 model.""" +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader + +from paddlespeech.s2t.io.collator import SpeechCollator +from paddlespeech.s2t.io.dataset import ManifestDataset +from paddlespeech.s2t.io.sampler import SortagradBatchSampler +from paddlespeech.s2t.io.sampler import SortagradDistributedBatchSampler +from paddlespeech.s2t.models.u2 import U2Evaluator +from paddlespeech.s2t.models.u2 import U2Model +from paddlespeech.s2t.models.u2 import U2Updater +from paddlespeech.s2t.training.extensions.snapshot import Snapshot +from paddlespeech.s2t.training.extensions.visualizer import VisualDL +from paddlespeech.s2t.training.optimizer import OptimizerFactory +from paddlespeech.s2t.training.scheduler import LRSchedulerFactory +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.training.updaters.trainer import Trainer as NewTrainer +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class U2Trainer(Trainer): + def __init__(self, config, args): + super().__init__(config, args) + + def setup_dataloader(self): + config = self.config.clone() + config.defrost() + config.keep_transcription_text = False + + # train/valid dataset, return token ids + config.manifest = config.train_manifest + train_dataset = ManifestDataset.from_config(config) + + config.manifest = config.dev_manifest + dev_dataset = ManifestDataset.from_config(config) + + collate_fn_train = SpeechCollator.from_config(config) + + collate_fn_dev = SpeechCollator.from_config(config) + + if self.parallel: + batch_sampler = SortagradDistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + num_replicas=None, + rank=None, + shuffle=True, + drop_last=True, + sortagrad=config.sortagrad, + shuffle_method=config.shuffle_method) + else: + batch_sampler = SortagradBatchSampler( + train_dataset, + shuffle=True, + batch_size=config.batch_size, + drop_last=True, + sortagrad=config.sortagrad, + shuffle_method=config.shuffle_method) + self.train_loader = DataLoader( + train_dataset, + batch_sampler=batch_sampler, + collate_fn=collate_fn_train, + num_workers=config.num_workers, ) + self.valid_loader = DataLoader( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False, + collate_fn=collate_fn_dev, + num_workers=config.num_workers, ) + + # test dataset, return raw text + config.manifest = config.test_manifest + # filter test examples, will cause less examples, but no mismatch with training + # and can use large batch size , save training time, so filter test egs now. + config.min_input_len = 0.0 # second + config.max_input_len = float('inf') # second + config.min_output_len = 0.0 # tokens + config.max_output_len = float('inf') # tokens + config.min_output_input_ratio = 0.00 + config.max_output_input_ratio = float('inf') + + test_dataset = ManifestDataset.from_config(config) + # return text ord id + config.keep_transcription_text = True + self.test_loader = DataLoader( + test_dataset, + batch_size=config.decode.batch_size, + shuffle=False, + drop_last=False, + collate_fn=SpeechCollator.from_config(config)) + # return text token id + config.keep_transcription_text = False + self.align_loader = DataLoader( + test_dataset, + batch_size=config.decode.batch_size, + shuffle=False, + drop_last=False, + collate_fn=SpeechCollator.from_config(config)) + logger.info("Setup train/valid/test/align Dataloader!") + + def setup_model(self): + config = self.config + model_conf = config + with UpdateConfig(model_conf): + model_conf.input_dim = self.train_loader.collate_fn.feature_size + model_conf.output_dim = self.train_loader.collate_fn.vocab_size + + model = U2Model.from_config(model_conf) + + if self.parallel: + model = paddle.DataParallel(model) + + model.train() + logger.info(f"{model}") + layer_tools.print_params(model, logger.info) + + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + + scheduler_args = { + "learning_rate": optim_conf.lr, + "verbose": False, + "warmup_steps": scheduler_conf.warmup_steps, + "gamma": scheduler_conf.lr_decay, + "d_model": model_conf.encoder_conf.output_size, + } + lr_scheduler = LRSchedulerFactory.from_args(scheduler_type, + scheduler_args) + + def optimizer_args( + config, + parameters, + lr_scheduler=None, ): + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + return { + "grad_clip": train_config.global_grad_clip, + "weight_decay": optim_conf.weight_decay, + "learning_rate": lr_scheduler + if lr_scheduler else optim_conf.lr, + "parameters": parameters, + "epsilon": 1e-9 if optim_type == 'noam' else None, + "beta1": 0.9 if optim_type == 'noam' else None, + "beat2": 0.98 if optim_type == 'noam' else None, + } + + optimzer_args = optimizer_args(config, model.parameters(), lr_scheduler) + optimizer = OptimizerFactory.from_args(optim_type, optimzer_args) + + self.model = model + self.optimizer = optimizer + self.lr_scheduler = lr_scheduler + logger.info("Setup model/optimizer/lr_scheduler!") + + def setup_updater(self): + output_dir = self.output_dir + config = self.config + + updater = U2Updater( + model=self.model, + optimizer=self.optimizer, + scheduler=self.lr_scheduler, + dataloader=self.train_loader, + output_dir=output_dir, + accum_grad=config.accum_grad) + + trainer = NewTrainer(updater, (config.n_epoch, 'epoch'), output_dir) + + evaluator = U2Evaluator(self.model, self.valid_loader) + + trainer.extend(evaluator, trigger=(1, "epoch")) + + if dist.get_rank() == 0: + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + num_snapshots = config.checkpoint.kbest_n + trainer.extend( + Snapshot( + mode='kbest', + max_size=num_snapshots, + indicator='VALID/LOSS', + less_better=True), + trigger=(1, 'epoch')) + # print(trainer.extensions) + # trainer.run() + self.trainer = trainer + + def run(self): + """The routine of the experiment after setup. This method is intended + to be used by the user. + """ + self.setup_updater() + with Timer("Training Done: {}"): + self.trainer.run() diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/recog.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/recog.py new file mode 100644 index 0000000..37ddd22 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/recog.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +from paddlespeech.s2t.decoders.recog_bin import main + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/test.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/test.py new file mode 100644 index 0000000..422483b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/test.py @@ -0,0 +1,87 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for U2 model.""" +import cProfile + +from yacs.config import CfgNode + +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.utility import print_arguments + +model_test_alias = { + "u2": "paddlespeech.s2t.exps.u2.model:U2Tester", + "u2_kaldi": "paddlespeech.s2t.exps.u2_kaldi.model:U2Tester", +} + + +def main_sp(config, args): + class_obj = dynamic_import(args.model_name, model_test_alias) + exp = class_obj(config, args) + with exp.eval(): + exp.setup() + if args.run_mode == 'test': + exp.run_test() + elif args.run_mode == 'export': + exp.run_export() + elif args.run_mode == 'align': + exp.run_align() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + parser.add_argument( + '--model-name', + type=str, + default='u2_kaldi', + help='model name, e.g: deepspeech2, u2, u2_kaldi, u2_st') + parser.add_argument( + '--run-mode', + type=str, + default='test', + help='run mode, e.g. test, align, export') + parser.add_argument( + '--dict-path', type=str, default=None, help='dict path.') + # save asr result to + parser.add_argument( + "--result-file", type=str, help="path of save the asr result") + # save jit model to + parser.add_argument( + "--export-path", type=str, help="path of the jit model to save") + args = parser.parse_args() + print_arguments(args, globals()) + + config = CfgNode() + config.set_new_allowed(True) + config.merge_from_file(args.config) + if args.decode_cfg: + decode_confs = CfgNode(new_allowed=True) + decode_confs.merge_from_file(args.decode_cfg) + config.decode = decode_confs + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats('test.profile') diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/train.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/train.py new file mode 100644 index 0000000..fcfc05a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/bin/train.py @@ -0,0 +1,69 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Trainer for U2 model.""" +import cProfile +import os + +from paddle import distributed as dist +from yacs.config import CfgNode + +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.utility import print_arguments + +model_train_alias = { + "u2": "paddlespeech.s2t.exps.u2.model:U2Trainer", + "u2_kaldi": "paddlespeech.s2t.exps.u2_kaldi.model:U2Trainer", +} + + +def main_sp(config, args): + class_obj = dynamic_import(args.model_name, model_train_alias) + exp = class_obj(config, args) + exp.setup() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + parser.add_argument( + '--model-name', + type=str, + default='u2_kaldi', + help='model name, e.g: deepspeech2, u2, u2_kaldi, u2_st') + args = parser.parse_args() + print_arguments(args, globals()) + + config = CfgNode() + config.set_new_allowed(True) + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats(os.path.join(args.output, 'train.profile')) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/model.py b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/model.py new file mode 100644 index 0000000..bc99597 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_kaldi/model.py @@ -0,0 +1,509 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains U2 model.""" +import json +import os +import time +from collections import defaultdict +from contextlib import nullcontext + +import jsonlines +import numpy as np +import paddle +from paddle import distributed as dist + +from paddlespeech.s2t.frontend.featurizer import TextFeaturizer +from paddlespeech.s2t.frontend.utility import load_dict +from paddlespeech.s2t.io.dataloader import BatchDataLoader +from paddlespeech.s2t.models.u2 import U2Model +from paddlespeech.s2t.training.optimizer import OptimizerFactory +from paddlespeech.s2t.training.scheduler import LRSchedulerFactory +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.utils import ctc_utils +from paddlespeech.s2t.utils import error_rate +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class U2Trainer(Trainer): + def __init__(self, config, args): + super().__init__(config, args) + + def train_batch(self, batch_index, batch_data, msg): + train_conf = self.config + start = time.time() + + # forward + utt, audio, audio_len, text, text_len = batch_data + loss, attention_loss, ctc_loss = self.model(audio, audio_len, text, + text_len) + + # loss div by `batch_size * accum_grad` + loss /= train_conf.accum_grad + losses_np = {'loss': float(loss) * train_conf.accum_grad} + if attention_loss: + losses_np['att_loss'] = float(attention_loss) + if ctc_loss: + losses_np['ctc_loss'] = float(ctc_loss) + + # loss backward + if (batch_index + 1) % train_conf.accum_grad != 0: + # Disable gradient synchronizations across DDP processes. + # Within this context, gradients will be accumulated on module + # variables, which will later be synchronized. + context = self.model.no_sync if (hasattr(self.model, "no_sync") and + self.parallel) else nullcontext + else: + # Used for single gpu training and DDP gradient synchronization + # processes. + context = nullcontext + with context(): + loss.backward() + layer_tools.print_grads(self.model, print_func=None) + + # optimizer step + if (batch_index + 1) % train_conf.accum_grad == 0: + self.optimizer.step() + self.optimizer.clear_grad() + self.lr_scheduler.step() + self.iteration += 1 + + iteration_time = time.time() - start + + if (batch_index + 1) % train_conf.log_interval == 0: + msg += "train time: {:>.3f}s, ".format(iteration_time) + msg += "batch size: {}, ".format(self.config.batch_size) + msg += "accum: {}, ".format(train_conf.accum_grad) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_np.items()) + logger.info(msg) + + if dist.get_rank() == 0 and self.visualizer: + losses_np_v = losses_np.copy() + losses_np_v.update({"lr": self.lr_scheduler()}) + for key, val in losses_np_v.items(): + self.visualizer.add_scalar( + tag="train/" + key, value=val, step=self.iteration - 1) + + @paddle.no_grad() + def valid(self): + self.model.eval() + logger.info(f"Valid Total Examples: {len(self.valid_loader.dataset)}") + valid_losses = defaultdict(list) + num_seen_utts = 1 + total_loss = 0.0 + + for i, batch in enumerate(self.valid_loader): + utt, audio, audio_len, text, text_len = batch + loss, attention_loss, ctc_loss = self.model(audio, audio_len, text, + text_len) + if paddle.isfinite(loss): + num_utts = batch[1].shape[0] + num_seen_utts += num_utts + total_loss += float(loss) * num_utts + valid_losses['val_loss'].append(float(loss)) + if attention_loss: + valid_losses['val_att_loss'].append(float(attention_loss)) + if ctc_loss: + valid_losses['val_ctc_loss'].append(float(ctc_loss)) + + if (i + 1) % self.config.log_interval == 0: + valid_dump = {k: np.mean(v) for k, v in valid_losses.items()} + valid_dump['val_history_loss'] = total_loss / num_seen_utts + + # logging + msg = f"Valid: Rank: {dist.get_rank()}, " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "batch: {}/{}, ".format(i + 1, len(self.valid_loader)) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in valid_dump.items()) + logger.info(msg) + + logger.info('Rank {} Val info val_loss {}'.format( + dist.get_rank(), total_loss / num_seen_utts)) + return total_loss, num_seen_utts + + def do_train(self): + """The training process control by step.""" + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + # script_model = paddle.jit.to_static(self.model) + # script_model_path = str(self.checkpoint_dir / 'init') + # paddle.jit.save(script_model, script_model_path) + + self.before_train() + + logger.info(f"Train Total Examples: {len(self.train_loader.dataset)}") + while self.epoch < self.config.n_epoch: + with Timer("Epoch-Train Time Cost: {}"): + self.model.train() + try: + data_start_time = time.time() + for batch_index, batch in enumerate(self.train_loader): + dataload_time = time.time() - data_start_time + msg = "Train: Rank: {}, ".format(dist.get_rank()) + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "batch : {}/{}, ".format(batch_index + 1, + len(self.train_loader)) + msg += "lr: {:>.8f}, ".format(self.lr_scheduler()) + msg += "data time: {:>.3f}s, ".format(dataload_time) + self.train_batch(batch_index, batch, msg) + self.after_train_batch() + data_start_time = time.time() + except Exception as e: + logger.error(e) + raise e + + with Timer("Eval Time Cost: {}"): + total_loss, num_seen_utts = self.valid() + if dist.get_world_size() > 1: + num_seen_utts = paddle.to_tensor(num_seen_utts) + # the default operator in all_reduce function is sum. + dist.all_reduce(num_seen_utts) + total_loss = paddle.to_tensor(total_loss) + dist.all_reduce(total_loss) + cv_loss = total_loss / num_seen_utts + cv_loss = float(cv_loss) + else: + cv_loss = total_loss / num_seen_utts + + logger.info( + 'Epoch {} Val info val_loss {}'.format(self.epoch, cv_loss)) + if self.visualizer: + self.visualizer.add_scalar( + tag='eval/cv_loss', value=cv_loss, step=self.epoch) + self.visualizer.add_scalar( + tag='eval/lr', value=self.lr_scheduler(), step=self.epoch) + + self.save(tag=self.epoch, infos={'val_loss': cv_loss}) + self.new_epoch() + + def setup_dataloader(self): + config = self.config.clone() + # train/valid dataset, return token ids + self.train_loader = BatchDataLoader( + json_file=config.train_manifest, + train_mode=True, + sortagrad=False, + batch_size=config.batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=self.args.ngpu, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config.preprocess_config, + n_iter_processes=config.num_workers, + subsampling_factor=1, + num_encs=1) + + self.valid_loader = BatchDataLoader( + json_file=config.dev_manifest, + train_mode=False, + sortagrad=False, + batch_size=config.batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=self.args.ngpu, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=None, + n_iter_processes=config.num_workers, + subsampling_factor=1, + num_encs=1) + + decode_batch_size = config.get('decode', dict()).get( + 'decode_batch_size', 1) + # test dataset, return raw text + self.test_loader = BatchDataLoader( + json_file=config.test_manifest, + train_mode=False, + sortagrad=False, + batch_size=decode_batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=None, + n_iter_processes=1, + subsampling_factor=1, + num_encs=1) + + self.align_loader = BatchDataLoader( + json_file=config.test_manifest, + train_mode=False, + sortagrad=False, + batch_size=decode_batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=None, + n_iter_processes=1, + subsampling_factor=1, + num_encs=1) + logger.info("Setup train/valid/test/align Dataloader!") + + def setup_model(self): + config = self.config + + # model + model_conf = config + with UpdateConfig(model_conf): + model_conf.input_dim = self.train_loader.feat_dim + model_conf.output_dim = self.train_loader.vocab_size + model = U2Model.from_config(model_conf) + if self.parallel: + model = paddle.DataParallel(model) + layer_tools.print_params(model, logger.info) + + # lr + scheduler_conf = config.scheduler_conf + scheduler_args = { + "learning_rate": scheduler_conf.lr, + "warmup_steps": scheduler_conf.warmup_steps, + "gamma": scheduler_conf.lr_decay, + "d_model": model_conf.encoder_conf.output_size, + "verbose": False, + } + lr_scheduler = LRSchedulerFactory.from_args(config.scheduler, + scheduler_args) + + # opt + def optimizer_args( + config, + parameters, + lr_scheduler=None, ): + optim_conf = config.optim_conf + return { + "grad_clip": optim_conf.global_grad_clip, + "weight_decay": optim_conf.weight_decay, + "learning_rate": lr_scheduler, + "parameters": parameters, + } + + optimzer_args = optimizer_args(config, model.parameters(), lr_scheduler) + optimizer = OptimizerFactory.from_args(config.optim, optimzer_args) + + self.model = model + self.lr_scheduler = lr_scheduler + self.optimizer = optimizer + logger.info("Setup model/optimizer/lr_scheduler!") + + +class U2Tester(U2Trainer): + def __init__(self, config, args): + super().__init__(config, args) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, + vocab=self.config.vocab_filepath, + spm_model_prefix=self.config.spm_model_prefix) + self.vocab_list = self.text_feature.vocab_list + + def id2token(self, texts, texts_len, text_feature): + """ ord() id to chr() chr """ + trans = [] + for text, n in zip(texts, texts_len): + n = n.numpy().item() + ids = text[:n] + trans.append(text_feature.defeaturize(ids.numpy().tolist())) + return trans + + def compute_metrics(self, + utts, + audio, + audio_len, + texts, + texts_len, + fout=None): + decode_cfg = self.config.decode + errors_sum, len_refs, num_ins = 0.0, 0, 0 + errors_func = error_rate.char_errors if decode_cfg.error_rate_type == 'cer' else error_rate.word_errors + error_rate_func = error_rate.cer if decode_cfg.error_rate_type == 'cer' else error_rate.wer + + start_time = time.time() + target_transcripts = self.id2token(texts, texts_len, self.text_feature) + result_transcripts, result_tokenids = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=decode_cfg.decoding_method, + beam_size=decode_cfg.beam_size, + ctc_weight=decode_cfg.ctc_weight, + decoding_chunk_size=decode_cfg.decoding_chunk_size, + num_decoding_left_chunks=decode_cfg.num_decoding_left_chunks, + simulate_streaming=decode_cfg.simulate_streaming) + decode_time = time.time() - start_time + + for i, (utt, target, result, rec_tids) in enumerate( + zip(utts, target_transcripts, result_transcripts, + result_tokenids)): + errors, len_ref = errors_func(target, result) + errors_sum += errors + len_refs += len_ref + num_ins += 1 + if fout: + fout.write({ + "utt": utt, + "refs": [target], + "hyps": [result], + "hyps_tokenid": [rec_tids], + }) + logger.info(f"Utt: {utt}") + logger.info(f"Ref: {target}") + logger.info(f"Hyp: {result}") + logger.info( + "One example error rate [%s] = %f" % + (decode_cfg.error_rate_type, error_rate_func(target, result))) + + return dict( + errors_sum=errors_sum, + len_refs=len_refs, + num_ins=num_ins, # num examples + error_rate=errors_sum / len_refs, + error_rate_type=decode_cfg.error_rate_type, + num_frames=audio_len.sum().numpy().item(), + decode_time=decode_time) + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + assert self.args.result_file + self.model.eval() + logger.info(f"Test Total Examples: {len(self.test_loader.dataset)}") + + stride_ms = self.config.stride_ms + error_rate_type = None + errors_sum, len_refs, num_ins = 0.0, 0, 0 + num_frames = 0.0 + num_time = 0.0 + with jsonlines.open(self.args.result_file, 'w') as fout: + for i, batch in enumerate(self.test_loader): + metrics = self.compute_metrics(*batch, fout=fout) + num_frames += metrics['num_frames'] + num_time += metrics["decode_time"] + errors_sum += metrics['errors_sum'] + len_refs += metrics['len_refs'] + num_ins += metrics['num_ins'] + error_rate_type = metrics['error_rate_type'] + rtf = num_time / (num_frames * stride_ms) + logger.info( + "RTF: %f, Error rate [%s] (%d/?) = %f" % + (rtf, error_rate_type, num_ins, errors_sum / len_refs)) + + rtf = num_time / (num_frames * stride_ms) + msg = "Test: " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "RTF: {}, ".format(rtf) + msg += "Final error rate [%s] (%d/%d) = %f" % ( + error_rate_type, num_ins, num_ins, errors_sum / len_refs) + logger.info(msg) + + # test meta results + err_meta_path = os.path.splitext(self.args.result_file)[0] + '.err' + err_type_str = "{}".format(error_rate_type) + with open(err_meta_path, 'w') as f: + data = json.dumps({ + "epoch": + self.epoch, + "step": + self.iteration, + "rtf": + rtf, + error_rate_type: + errors_sum / len_refs, + "dataset_hour": (num_frames * stride_ms) / 1000.0 / 3600.0, + "process_hour": + num_time / 1000.0 / 3600.0, + "num_examples": + num_ins, + "err_sum": + errors_sum, + "ref_len": + len_refs, + "decode_method": + self.config.decode.decoding_method, + }) + f.write(data + '\n') + + @paddle.no_grad() + def align(self): + ctc_utils.ctc_align(self.config, self.model, self.align_loader, + self.config.decode.decode_batch_size, + self.config.stride_ms, self.vocab_list, + self.args.result_file) + + def load_inferspec(self): + """infer model and input spec. + + Returns: + nn.Layer: inference model + List[paddle.static.InputSpec]: input spec. + """ + from paddlespeech.s2t.models.u2 import U2InferModel + infer_model = U2InferModel.from_pretrained(self.test_loader, + self.config.clone(), + self.args.checkpoint_path) + feat_dim = self.test_loader.feat_dim + input_spec = [ + paddle.static.InputSpec(shape=[1, None, feat_dim], + dtype='float32'), # audio, [B,T,D] + paddle.static.InputSpec(shape=[1], + dtype='int64'), # audio_length, [B] + ] + return infer_model, input_spec + + @paddle.no_grad() + def export(self): + infer_model, input_spec = self.load_inferspec() + assert isinstance(input_spec, list), type(input_spec) + infer_model.eval() + static_model = paddle.jit.to_static(infer_model, input_spec=input_spec) + logger.info(f"Export code: {static_model.forward.code}") + paddle.jit.save(static_model, self.args.export_path) + + def setup_dict(self): + # load dictionary for debug log + self.args.char_list = load_dict(self.args.dict_path, + "maskctc" in self.args.model_name) + + def setup(self): + super().setup() + self.setup_dict() diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/__init__.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/export.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/export.py new file mode 100644 index 0000000..c641152 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/export.py @@ -0,0 +1,53 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Export for U2 model.""" +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2_st.model import U2STTester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_export() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save jit model to + parser.add_argument( + "--export_path", type=str, help="path of the jit model to save") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + main(config, args) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/test.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/test.py new file mode 100644 index 0000000..1d70a31 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/test.py @@ -0,0 +1,64 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Evaluation for U2 model.""" +import cProfile + +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2_st.model import U2STTester as Tester +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + +# TODO(hui zhang): dynamic load + + +def main_sp(config, args): + exp = Tester(config, args) + with exp.eval(): + exp.setup() + exp.run_test() + + +def main(config, args): + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + # save asr result to + parser.add_argument( + "--result_file", type=str, help="path of save the asr result") + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.decode_cfg: + decode_conf = CfgNode(new_allowed=True) + decode_conf.merge_from_file(args.decode_cfg) + config.decode = decode_conf + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats('test.profile') diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/train.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/train.py new file mode 100644 index 0000000..4dec9ec --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/bin/train.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Trainer for U2 model.""" +import cProfile +import os + +from paddle import distributed as dist +from yacs.config import CfgNode + +from paddlespeech.s2t.exps.u2_st.model import U2STTrainer as Trainer +from paddlespeech.s2t.training.cli import default_argument_parser +from paddlespeech.s2t.utils.utility import print_arguments + + +def main_sp(config, args): + exp = Trainer(config, args) + exp.setup() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + parser = default_argument_parser() + args = parser.parse_args() + print_arguments(args, globals()) + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + if args.dump_config: + with open(args.dump_config, 'w') as f: + print(config, file=f) + + # Setting for profiling + pr = cProfile.Profile() + pr.runcall(main, config, args) + pr.dump_stats(os.path.join(args.output, 'train.profile')) diff --git a/ernie-sat/paddlespeech/s2t/exps/u2_st/model.py b/ernie-sat/paddlespeech/s2t/exps/u2_st/model.py new file mode 100644 index 0000000..6a32eda --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/exps/u2_st/model.py @@ -0,0 +1,552 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains U2 model.""" +import json +import os +import time +from collections import defaultdict +from collections import OrderedDict +from contextlib import nullcontext + +import jsonlines +import numpy as np +import paddle +from paddle import distributed as dist + +from paddlespeech.s2t.frontend.featurizer import TextFeaturizer +from paddlespeech.s2t.io.dataloader import BatchDataLoader +from paddlespeech.s2t.models.u2_st import U2STModel +from paddlespeech.s2t.training.optimizer import OptimizerFactory +from paddlespeech.s2t.training.reporter import ObsScope +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.scheduler import LRSchedulerFactory +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.trainer import Trainer +from paddlespeech.s2t.utils import bleu_score +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import UpdateConfig + +logger = Log(__name__).getlog() + + +class U2STTrainer(Trainer): + def __init__(self, config, args): + super().__init__(config, args) + + def train_batch(self, batch_index, batch_data, msg): + train_conf = self.config + start = time.time() + # forward + utt, audio, audio_len, text, text_len = batch_data + if isinstance(text, list) and isinstance(text_len, list): + # joint training with ASR. Two decoding texts [translation, transcription] + text, text_transcript = text + text_len, text_transcript_len = text_len + loss, st_loss, attention_loss, ctc_loss = self.model( + audio, audio_len, text, text_len, text_transcript, + text_transcript_len) + else: + loss, st_loss, attention_loss, ctc_loss = self.model( + audio, audio_len, text, text_len) + + # loss div by `batch_size * accum_grad` + loss /= train_conf.accum_grad + losses_np = {'loss': float(loss) * train_conf.accum_grad} + if st_loss: + losses_np['st_loss'] = float(st_loss) + if attention_loss: + losses_np['att_loss'] = float(attention_loss) + if ctc_loss: + losses_np['ctc_loss'] = float(ctc_loss) + + # loss backward + if (batch_index + 1) % train_conf.accum_grad != 0: + # Disable gradient synchronizations across DDP processes. + # Within this context, gradients will be accumulated on module + # variables, which will later be synchronized. + context = self.model.no_sync if (hasattr(self.model, "no_sync") and + self.parallel) else nullcontext + else: + # Used for single gpu training and DDP gradient synchronization + # processes. + context = nullcontext + with context(): + loss.backward() + layer_tools.print_grads(self.model, print_func=None) + + # optimizer step + if (batch_index + 1) % train_conf.accum_grad == 0: + self.optimizer.step() + self.optimizer.clear_grad() + self.lr_scheduler.step() + self.iteration += 1 + + iteration_time = time.time() - start + + for k, v in losses_np.items(): + report(k, v) + report("batch_size", self.config.batch_size) + report("accum", train_conf.accum_grad) + report("step_cost", iteration_time) + + if (batch_index + 1) % train_conf.log_interval == 0: + msg += "train time: {:>.3f}s, ".format(iteration_time) + msg += "batch size: {}, ".format(self.config.batch_size) + msg += "accum: {}, ".format(train_conf.accum_grad) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_np.items()) + logger.info(msg) + + if dist.get_rank() == 0 and self.visualizer: + losses_np_v = losses_np.copy() + losses_np_v.update({"lr": self.lr_scheduler()}) + for key, val in losses_np_v.items(): + self.visualizer.add_scalar( + tag="train/" + key, value=val, step=self.iteration - 1) + + @paddle.no_grad() + def valid(self): + self.model.eval() + logger.info(f"Valid Total Examples: {len(self.valid_loader.dataset)}") + valid_losses = defaultdict(list) + num_seen_utts = 1 + total_loss = 0.0 + for i, batch in enumerate(self.valid_loader): + utt, audio, audio_len, text, text_len = batch + if isinstance(text, list) and isinstance(text_len, list): + text, text_transcript = text + text_len, text_transcript_len = text_len + loss, st_loss, attention_loss, ctc_loss = self.model( + audio, audio_len, text, text_len, text_transcript, + text_transcript_len) + else: + loss, st_loss, attention_loss, ctc_loss = self.model( + audio, audio_len, text, text_len) + if paddle.isfinite(loss): + num_utts = batch[1].shape[0] + num_seen_utts += num_utts + total_loss += float(st_loss) * num_utts + valid_losses['val_loss'].append(float(st_loss)) + if attention_loss: + valid_losses['val_att_loss'].append(float(attention_loss)) + if ctc_loss: + valid_losses['val_ctc_loss'].append(float(ctc_loss)) + + if (i + 1) % self.config.log_interval == 0: + valid_dump = {k: np.mean(v) for k, v in valid_losses.items()} + valid_dump['val_history_st_loss'] = total_loss / num_seen_utts + + # logging + msg = f"Valid: Rank: {dist.get_rank()}, " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "batch: {}/{}, ".format(i + 1, len(self.valid_loader)) + msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in valid_dump.items()) + logger.info(msg) + + logger.info('Rank {} Val info st_val_loss {}'.format( + dist.get_rank(), total_loss / num_seen_utts)) + return total_loss, num_seen_utts + + def do_train(self): + """The training process control by step.""" + # !!!IMPORTANT!!! + # Try to export the model by script, if fails, we should refine + # the code to satisfy the script export requirements + # script_model = paddle.jit.to_static(self.model) + # script_model_path = str(self.checkpoint_dir / 'init') + # paddle.jit.save(script_model, script_model_path) + + self.before_train() + + logger.info(f"Train Total Examples: {len(self.train_loader.dataset)}") + while self.epoch < self.config.n_epoch: + with Timer("Epoch-Train Time Cost: {}"): + self.model.train() + try: + data_start_time = time.time() + for batch_index, batch in enumerate(self.train_loader): + dataload_time = time.time() - data_start_time + msg = "Train:" + observation = OrderedDict() + with ObsScope(observation): + report("Rank", dist.get_rank()) + report("epoch", self.epoch) + report('step', self.iteration) + report("lr", self.lr_scheduler()) + self.train_batch(batch_index, batch, msg) + self.after_train_batch() + report('iter', batch_index + 1) + report('total', len(self.train_loader)) + report('reader_cost', dataload_time) + observation['batch_cost'] = observation[ + 'reader_cost'] + observation['step_cost'] + observation['samples'] = observation['batch_size'] + observation['ips,sent./sec'] = observation[ + 'batch_size'] / observation['batch_cost'] + for k, v in observation.items(): + msg += f" {k.split(',')[0]}: " + msg += f"{v:>.8f}" if isinstance(v, + float) else f"{v}" + msg += f" {k.split(',')[1]}" if len( + k.split(',')) == 2 else "" + msg += "," + msg = msg[:-1] # remove the last "," + if (batch_index + 1) % self.config.log_interval == 0: + logger.info(msg) + except Exception as e: + logger.error(e) + raise e + + with Timer("Eval Time Cost: {}"): + total_loss, num_seen_utts = self.valid() + if dist.get_world_size() > 1: + num_seen_utts = paddle.to_tensor(num_seen_utts) + # the default operator in all_reduce function is sum. + dist.all_reduce(num_seen_utts) + total_loss = paddle.to_tensor(total_loss) + dist.all_reduce(total_loss) + cv_loss = total_loss / num_seen_utts + cv_loss = float(cv_loss) + else: + cv_loss = total_loss / num_seen_utts + + logger.info( + 'Epoch {} Val info val_loss {}'.format(self.epoch, cv_loss)) + if self.visualizer: + self.visualizer.add_scalar( + tag='eval/cv_loss', value=cv_loss, step=self.epoch) + self.visualizer.add_scalar( + tag='eval/lr', value=self.lr_scheduler(), step=self.epoch) + + self.save(tag=self.epoch, infos={'val_loss': cv_loss}) + self.new_epoch() + + def setup_dataloader(self): + config = self.config.clone() + + load_transcript = True if config.model_conf.asr_weight > 0 else False + + if self.train: + # train/valid dataset, return token ids + self.train_loader = BatchDataLoader( + json_file=config.train_manifest, + train_mode=True, + sortagrad=False, + batch_size=config.batch_size, + maxlen_in=config.maxlen_in, + maxlen_out=config.maxlen_out, + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config. + preprocess_config, # aug will be off when train_mode=False + n_iter_processes=config.num_workers, + subsampling_factor=1, + load_aux_output=load_transcript, + num_encs=1, + dist_sampler=True) + + self.valid_loader = BatchDataLoader( + json_file=config.dev_manifest, + train_mode=False, + sortagrad=False, + batch_size=config.batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config. + preprocess_config, # aug will be off when train_mode=False + n_iter_processes=config.num_workers, + subsampling_factor=1, + load_aux_output=load_transcript, + num_encs=1, + dist_sampler=False) + logger.info("Setup train/valid Dataloader!") + else: + # test dataset, return raw text + decode_batch_size = config.get('decode', dict()).get( + 'decode_batch_size', 1) + self.test_loader = BatchDataLoader( + json_file=config.test_manifest, + train_mode=False, + sortagrad=False, + batch_size=decode_batch_size, + maxlen_in=float('inf'), + maxlen_out=float('inf'), + minibatches=0, + mini_batch_size=1, + batch_count='auto', + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + preprocess_conf=config. + preprocess_config, # aug will be off when train_mode=False + n_iter_processes=config.num_workers, + subsampling_factor=1, + num_encs=1, + dist_sampler=False) + + logger.info("Setup test Dataloader!") + + def setup_model(self): + config = self.config + model_conf = config + with UpdateConfig(model_conf): + if self.train: + model_conf.input_dim = self.train_loader.feat_dim + model_conf.output_dim = self.train_loader.vocab_size + else: + model_conf.input_dim = self.test_loader.feat_dim + model_conf.output_dim = self.test_loader.vocab_size + + model = U2STModel.from_config(model_conf) + + if self.parallel: + model = paddle.DataParallel(model) + + logger.info(f"{model}") + layer_tools.print_params(model, logger.info) + + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + + scheduler_args = { + "learning_rate": optim_conf.lr, + "verbose": False, + "warmup_steps": scheduler_conf.warmup_steps, + "gamma": scheduler_conf.lr_decay, + "d_model": model_conf.encoder_conf.output_size, + } + lr_scheduler = LRSchedulerFactory.from_args(scheduler_type, + scheduler_args) + + def optimizer_args( + config, + parameters, + lr_scheduler=None, ): + train_config = config + optim_type = train_config.optim + optim_conf = train_config.optim_conf + scheduler_type = train_config.scheduler + scheduler_conf = train_config.scheduler_conf + return { + "grad_clip": train_config.global_grad_clip, + "weight_decay": optim_conf.weight_decay, + "learning_rate": lr_scheduler + if lr_scheduler else optim_conf.lr, + "parameters": parameters, + "epsilon": 1e-9 if optim_type == 'noam' else None, + "beta1": 0.9 if optim_type == 'noam' else None, + "beat2": 0.98 if optim_type == 'noam' else None, + } + + optimzer_args = optimizer_args(config, model.parameters(), lr_scheduler) + optimizer = OptimizerFactory.from_args(optim_type, optimzer_args) + + self.model = model + self.optimizer = optimizer + self.lr_scheduler = lr_scheduler + logger.info("Setup model/optimizer/lr_scheduler!") + + +class U2STTester(U2STTrainer): + def __init__(self, config, args): + super().__init__(config, args) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, + vocab=self.config.vocab_filepath, + spm_model_prefix=self.config.spm_model_prefix) + self.vocab_list = self.text_feature.vocab_list + + def id2token(self, texts, texts_len, text_feature): + """ ord() id to chr() chr """ + trans = [] + for text, n in zip(texts, texts_len): + n = n.numpy().item() + ids = text[:n] + trans.append(text_feature.defeaturize(ids.numpy().tolist())) + return trans + + def translate(self, audio, audio_len): + """"E2E translation from extracted audio feature""" + decode_cfg = self.config.decode + self.model.eval() + + hyps = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=decode_cfg.decoding_method, + beam_size=decode_cfg.beam_size, + word_reward=decode_cfg.word_reward, + maxlenratio=decode_cfg.maxlenratio, + decoding_chunk_size=decode_cfg.decoding_chunk_size, + num_decoding_left_chunks=decode_cfg.num_decoding_left_chunks, + simulate_streaming=decode_cfg.simulate_streaming) + return hyps + + def compute_translation_metrics(self, + utts, + audio, + audio_len, + texts, + texts_len, + bleu_func, + fout=None): + decode_cfg = self.config.decode + len_refs, num_ins = 0, 0 + + start_time = time.time() + + refs = self.id2token(texts, texts_len, self.text_feature) + + hyps = self.model.decode( + audio, + audio_len, + text_feature=self.text_feature, + decoding_method=decode_cfg.decoding_method, + beam_size=decode_cfg.beam_size, + word_reward=decode_cfg.word_reward, + maxlenratio=decode_cfg.maxlenratio, + decoding_chunk_size=decode_cfg.decoding_chunk_size, + num_decoding_left_chunks=decode_cfg.num_decoding_left_chunks, + simulate_streaming=decode_cfg.simulate_streaming) + + decode_time = time.time() - start_time + + for utt, target, result in zip(utts, refs, hyps): + len_refs += len(target.split()) + num_ins += 1 + if fout: + fout.write({"utt": utt, "ref": target, "hyp": result}) + logger.info(f"Utt: {utt}") + logger.info(f"Ref: {target}") + logger.info(f"Hyp: {result}") + logger.info("One example BLEU = %s" % + (bleu_func([result], [[target]]).prec_str)) + + return dict( + hyps=hyps, + refs=refs, + bleu=bleu_func(hyps, [refs]).score, + len_refs=len_refs, + num_ins=num_ins, # num examples + num_frames=audio_len.sum().numpy().item(), + decode_time=decode_time) + + @mp_tools.rank_zero_only + @paddle.no_grad() + def test(self): + assert self.args.result_file + self.model.eval() + logger.info(f"Test Total Examples: {len(self.test_loader.dataset)}") + + decode_cfg = self.config.decode + bleu_func = bleu_score.char_bleu if decode_cfg.error_rate_type == 'char-bleu' else bleu_score.bleu + + stride_ms = self.config.stride_ms + hyps, refs = [], [] + len_refs, num_ins = 0, 0 + num_frames = 0.0 + num_time = 0.0 + with jsonlines.open(self.args.result_file, 'w') as fout: + for i, batch in enumerate(self.test_loader): + metrics = self.compute_translation_metrics( + *batch, bleu_func=bleu_func, fout=fout) + hyps += metrics['hyps'] + refs += metrics['refs'] + bleu = metrics['bleu'] + num_frames += metrics['num_frames'] + num_time += metrics["decode_time"] + len_refs += metrics['len_refs'] + num_ins += metrics['num_ins'] + rtf = num_time / (num_frames * stride_ms) + logger.info("RTF: %f, instance (%d), batch BELU = %f" % + (rtf, num_ins, bleu)) + + rtf = num_time / (num_frames * stride_ms) + msg = "Test: " + msg += "epoch: {}, ".format(self.epoch) + msg += "step: {}, ".format(self.iteration) + msg += "RTF: {}, ".format(rtf) + msg += "Test set [%s]: %s" % (len(hyps), str(bleu_func(hyps, [refs]))) + logger.info(msg) + bleu_meta_path = os.path.splitext(self.args.result_file)[0] + '.bleu' + err_type_str = "BLEU" + with open(bleu_meta_path, 'w') as f: + data = json.dumps({ + "epoch": + self.epoch, + "step": + self.iteration, + "rtf": + rtf, + err_type_str: + bleu_func(hyps, [refs]).score, + "dataset_hour": (num_frames * stride_ms) / 1000.0 / 3600.0, + "process_hour": + num_time / 1000.0 / 3600.0, + "num_examples": + num_ins, + "decode_method": + self.config.decode.decoding_method, + }) + f.write(data + '\n') + + def load_inferspec(self): + """infer model and input spec. + + Returns: + nn.Layer: inference model + List[paddle.static.InputSpec]: input spec. + """ + from paddlespeech.s2t.models.u2_st import U2STInferModel + infer_model = U2STInferModel.from_pretrained(self.test_loader, + self.config.clone(), + self.args.checkpoint_path) + feat_dim = self.test_loader.feat_dim + input_spec = [ + paddle.static.InputSpec(shape=[1, None, feat_dim], + dtype='float32'), # audio, [B,T,D] + paddle.static.InputSpec(shape=[1], + dtype='int64'), # audio_length, [B] + ] + return infer_model, input_spec + + @paddle.no_grad() + def export(self): + infer_model, input_spec = self.load_inferspec() + assert isinstance(input_spec, list), type(input_spec) + infer_model.eval() + static_model = paddle.jit.to_static(infer_model, input_spec=input_spec) + logger.info(f"Export code: {static_model.forward.code}") + paddle.jit.save(static_model, self.args.export_path) diff --git a/ernie-sat/paddlespeech/s2t/frontend/__init__.py b/ernie-sat/paddlespeech/s2t/frontend/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/frontend/audio.py b/ernie-sat/paddlespeech/s2t/frontend/audio.py new file mode 100644 index 0000000..7f71e5d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/audio.py @@ -0,0 +1,730 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the audio segment class.""" +import copy +import io +import random +import re +import struct + +import numpy as np +import resampy +import soundfile +from scipy import signal + +from .utility import convert_samples_from_float32 +from .utility import convert_samples_to_float32 +from .utility import subfile_from_tar + + +class AudioSegment(): + """Monaural audio segment abstraction. + + :param samples: Audio samples [num_samples x num_channels]. + :type samples: ndarray.float32 + :param sample_rate: Audio sample rate. + :type sample_rate: int + :raises TypeError: If the sample data type is not float or int. + """ + + def __init__(self, samples, sample_rate): + """Create audio segment from samples. + + Samples are convert float32 internally, with int scaled to [-1, 1]. + """ + self._samples = self._convert_samples_to_float32(samples) + self._sample_rate = sample_rate + if self._samples.ndim >= 2: + self._samples = np.mean(self._samples, 1) + + def __eq__(self, other): + """Return whether two objects are equal.""" + if type(other) is not type(self): + return False + if self._sample_rate != other._sample_rate: + return False + if self._samples.shape != other._samples.shape: + return False + if np.any(self.samples != other._samples): + return False + return True + + def __ne__(self, other): + """Return whether two objects are unequal.""" + return not self.__eq__(other) + + def __str__(self): + """Return human-readable representation of segment.""" + return ("%s: num_samples=%d, sample_rate=%d, duration=%.2fsec, " + "rms=%.2fdB" % (type(self), self.num_samples, self.sample_rate, + self.duration, self.rms_db)) + + @classmethod + def from_file(cls, file, infos=None): + """Create audio segment from audio file. + + Args: + filepath (str|file): Filepath or file object to audio file. + infos (TarLocalData, optional): tar2obj and tar2infos. Defaults to None. + + Returns: + AudioSegment: Audio segment instance. + """ + if isinstance(file, str) and re.findall(r".seqbin_\d+$", file): + return cls.from_sequence_file(file) + elif isinstance(file, str) and file.startswith('tar:'): + return cls.from_file(subfile_from_tar(file, infos)) + else: + samples, sample_rate = soundfile.read(file, dtype='float32') + return cls(samples, sample_rate) + + @classmethod + def slice_from_file(cls, file, start=None, end=None): + """Loads a small section of an audio without having to load + the entire file into the memory which can be incredibly wasteful. + + :param file: Input audio filepath or file object. + :type file: str|file + :param start: Start time in seconds. If start is negative, it wraps + around from the end. If not provided, this function + reads from the very beginning. + :type start: float + :param end: End time in seconds. If end is negative, it wraps around + from the end. If not provided, the default behvaior is + to read to the end of the file. + :type end: float + :return: AudioSegment instance of the specified slice of the input + audio file. + :rtype: AudioSegment + :raise ValueError: If start or end is incorrectly set, e.g. out of + bounds in time. + """ + sndfile = soundfile.SoundFile(file) + sample_rate = sndfile.samplerate + duration = float(len(sndfile)) / sample_rate + start = 0. if start is None else start + end = duration if end is None else end + if start < 0.0: + start += duration + if end < 0.0: + end += duration + if start < 0.0: + raise ValueError("The slice start position (%f s) is out of " + "bounds." % start) + if end < 0.0: + raise ValueError("The slice end position (%f s) is out of bounds." % + end) + if start > end: + raise ValueError("The slice start position (%f s) is later than " + "the slice end position (%f s)." % (start, end)) + if end > duration: + raise ValueError("The slice end position (%f s) is out of bounds " + "(> %f s)" % (end, duration)) + start_frame = int(start * sample_rate) + end_frame = int(end * sample_rate) + sndfile.seek(start_frame) + data = sndfile.read(frames=end_frame - start_frame, dtype='float32') + return cls(data, sample_rate) + + @classmethod + def from_sequence_file(cls, filepath): + """Create audio segment from sequence file. Sequence file is a binary + file containing a collection of multiple audio files, with several + header bytes in the head indicating the offsets of each audio byte data + chunk. + + The format is: + + 4 bytes (int, version), + 4 bytes (int, num of utterance), + 4 bytes (int, bytes per header), + [bytes_per_header*(num_utterance+1)] bytes (offsets for each audio), + audio_bytes_data_of_1st_utterance, + audio_bytes_data_of_2nd_utterance, + ...... + + Sequence file name must end with ".seqbin". And the filename of the 5th + utterance's audio file in sequence file "xxx.seqbin" must be + "xxx.seqbin_5", with "5" indicating the utterance index within this + sequence file (starting from 1). + + :param filepath: Filepath of sequence file. + :type filepath: str + :return: Audio segment instance. + :rtype: AudioSegment + """ + # parse filepath + matches = re.match(r"(.+\.seqbin)_(\d+)", filepath) + if matches is None: + raise IOError("File type of %s is not supported" % filepath) + filename = matches.group(1) + fileno = int(matches.group(2)) + + # read headers + f = io.open(filename, mode='rb', encoding='utf8') + version = f.read(4) + num_utterances = struct.unpack("i", f.read(4))[0] + bytes_per_header = struct.unpack("i", f.read(4))[0] + header_bytes = f.read(bytes_per_header * (num_utterances + 1)) + header = [ + struct.unpack("i", header_bytes[bytes_per_header * i: + bytes_per_header * (i + 1)])[0] + for i in range(num_utterances + 1) + ] + + # read audio bytes + f.seek(header[fileno - 1]) + audio_bytes = f.read(header[fileno] - header[fileno - 1]) + f.close() + + # create audio segment + try: + return cls.from_bytes(audio_bytes) + except Exception as e: + samples = np.frombuffer(audio_bytes, dtype='int16') + return cls(samples=samples, sample_rate=8000) + + @classmethod + def from_bytes(cls, bytes): + """Create audio segment from a byte string containing audio samples. + + :param bytes: Byte string containing audio samples. + :type bytes: str + :return: Audio segment instance. + :rtype: AudioSegment + """ + samples, sample_rate = soundfile.read( + io.BytesIO(bytes), dtype='float32') + return cls(samples, sample_rate) + + @classmethod + def from_pcm(cls, samples, sample_rate): + """Create audio segment from a byte string containing audio samples. + :param samples: Audio samples [num_samples x num_channels]. + :type samples: numpy.ndarray + :param sample_rate: Audio sample rate. + :type sample_rate: int + :return: Audio segment instance. + :rtype: AudioSegment + """ + return cls(samples, sample_rate) + + @classmethod + def concatenate(cls, *segments): + """Concatenate an arbitrary number of audio segments together. + + :param *segments: Input audio segments to be concatenated. + :type *segments: tuple of AudioSegment + :return: Audio segment instance as concatenating results. + :rtype: AudioSegment + :raises ValueError: If the number of segments is zero, or if the + sample_rate of any segments does not match. + :raises TypeError: If any segment is not AudioSegment instance. + """ + # Perform basic sanity-checks. + if len(segments) == 0: + raise ValueError("No audio segments are given to concatenate.") + sample_rate = segments[0]._sample_rate + for seg in segments: + if sample_rate != seg._sample_rate: + raise ValueError("Can't concatenate segments with " + "different sample rates") + if type(seg) is not cls: + raise TypeError("Only audio segments of the same type " + "can be concatenated.") + samples = np.concatenate([seg.samples for seg in segments]) + return cls(samples, sample_rate) + + @classmethod + def make_silence(cls, duration, sample_rate): + """Creates a silent audio segment of the given duration and sample rate. + + :param duration: Length of silence in seconds. + :type duration: float + :param sample_rate: Sample rate. + :type sample_rate: float + :return: Silent AudioSegment instance of the given duration. + :rtype: AudioSegment + """ + samples = np.zeros(int(duration * sample_rate)) + return cls(samples, sample_rate) + + def to_wav_file(self, filepath, dtype='float32'): + """Save audio segment to disk as wav file. + + :param filepath: WAV filepath or file object to save the + audio segment. + :type filepath: str|file + :param dtype: Subtype for audio file. Options: 'int16', 'int32', + 'float32', 'float64'. Default is 'float32'. + :type dtype: str + :raises TypeError: If dtype is not supported. + """ + samples = self._convert_samples_from_float32(self._samples, dtype) + subtype_map = { + 'int16': 'PCM_16', + 'int32': 'PCM_32', + 'float32': 'FLOAT', + 'float64': 'DOUBLE' + } + soundfile.write( + filepath, + samples, + self._sample_rate, + format='WAV', + subtype=subtype_map[dtype]) + + def superimpose(self, other): + """Add samples from another segment to those of this segment + (sample-wise addition, not segment concatenation). + + Note that this is an in-place transformation. + + :param other: Segment containing samples to be added in. + :type other: AudioSegments + :raise TypeError: If type of two segments don't match. + :raise ValueError: If the sample rates of the two segments are not + equal, or if the lengths of segments don't match. + """ + if isinstance(other, type(self)): + raise TypeError("Cannot add segments of different types: %s " + "and %s." % (type(self), type(other))) + if self._sample_rate != other._sample_rate: + raise ValueError("Sample rates must match to add segments.") + if len(self._samples) != len(other._samples): + raise ValueError("Segment lengths must match to add segments.") + self._samples += other._samples + + def to_bytes(self, dtype='float32'): + """Create a byte string containing the audio content. + + :param dtype: Data type for export samples. Options: 'int16', 'int32', + 'float32', 'float64'. Default is 'float32'. + :type dtype: str + :return: Byte string containing audio content. + :rtype: str + """ + samples = self._convert_samples_from_float32(self._samples, dtype) + return samples.tostring() + + def to(self, dtype='int16'): + """Create a `dtype` audio content. + + :param dtype: Data type for export samples. Options: 'int16', 'int32', + 'float32', 'float64'. Default is 'float32'. + :type dtype: str + :return: np.ndarray containing `dtype` audio content. + :rtype: str + """ + samples = self._convert_samples_from_float32(self._samples, dtype) + return samples + + def gain_db(self, gain): + """Apply gain in decibels to samples. + + Note that this is an in-place transformation. + + :param gain: Gain in decibels to apply to samples. + :type gain: float|1darray + """ + self._samples *= 10.**(gain / 20.) + + def change_speed(self, speed_rate): + """Change the audio speed by linear interpolation. + + Note that this is an in-place transformation. + + :param speed_rate: Rate of speed change: + speed_rate > 1.0, speed up the audio; + speed_rate = 1.0, unchanged; + speed_rate < 1.0, slow down the audio; + speed_rate <= 0.0, not allowed, raise ValueError. + :type speed_rate: float + :raises ValueError: If speed_rate <= 0.0. + """ + if speed_rate == 1.0: + return + if speed_rate <= 0: + raise ValueError("speed_rate should be greater than zero.") + + # numpy + # old_length = self._samples.shape[0] + # new_length = int(old_length / speed_rate) + # old_indices = np.arange(old_length) + # new_indices = np.linspace(start=0, stop=old_length, num=new_length) + # self._samples = np.interp(new_indices, old_indices, self._samples) + + # sox, slow + try: + import soxbindings as sox + except ImportError: + try: + from paddlespeech.s2t.utils import dynamic_pip_install + package = "sox" + dynamic_pip_install.install(package) + package = "soxbindings" + dynamic_pip_install.install(package) + import soxbindings as sox + except Exception: + raise RuntimeError( + "Can not install soxbindings on your system.") + + tfm = sox.Transformer() + tfm.set_globals(multithread=False) + tfm.speed(speed_rate) + self._samples = tfm.build_array( + input_array=self._samples, + sample_rate_in=self._sample_rate).squeeze(-1).astype( + np.float32).copy() + + def normalize(self, target_db=-20, max_gain_db=300.0): + """Normalize audio to be of the desired RMS value in decibels. + + Note that this is an in-place transformation. + + :param target_db: Target RMS value in decibels. This value should be + less than 0.0 as 0.0 is full-scale audio. + :type target_db: float + :param max_gain_db: Max amount of gain in dB that can be applied for + normalization. This is to prevent nans when + attempting to normalize a signal consisting of + all zeros. + :type max_gain_db: float + :raises ValueError: If the required gain to normalize the segment to + the target_db value exceeds max_gain_db. + """ + gain = target_db - self.rms_db + if gain > max_gain_db: + raise ValueError( + "Unable to normalize segment to %f dB because the " + "the probable gain have exceeds max_gain_db (%f dB)" % + (target_db, max_gain_db)) + self.gain_db(min(max_gain_db, target_db - self.rms_db)) + + def normalize_online_bayesian(self, + target_db, + prior_db, + prior_samples, + startup_delay=0.0): + """Normalize audio using a production-compatible online/causal + algorithm. This uses an exponential likelihood and gamma prior to + make online estimates of the RMS even when there are very few samples. + + Note that this is an in-place transformation. + + :param target_db: Target RMS value in decibels. + :type target_bd: float + :param prior_db: Prior RMS estimate in decibels. + :type prior_db: float + :param prior_samples: Prior strength in number of samples. + :type prior_samples: float + :param startup_delay: Default 0.0s. If provided, this function will + accrue statistics for the first startup_delay + seconds before applying online normalization. + :type startup_delay: float + """ + # Estimate total RMS online. + startup_sample_idx = min(self.num_samples - 1, + int(self.sample_rate * startup_delay)) + prior_mean_squared = 10.**(prior_db / 10.) + prior_sum_of_squares = prior_mean_squared * prior_samples + cumsum_of_squares = np.cumsum(self.samples**2) + sample_count = np.arange(self.num_samples) + 1 + if startup_sample_idx > 0: + cumsum_of_squares[:startup_sample_idx] = \ + cumsum_of_squares[startup_sample_idx] + sample_count[:startup_sample_idx] = \ + sample_count[startup_sample_idx] + mean_squared_estimate = ((cumsum_of_squares + prior_sum_of_squares) / + (sample_count + prior_samples)) + rms_estimate_db = 10 * np.log10(mean_squared_estimate) + # Compute required time-varying gain. + gain_db = target_db - rms_estimate_db + self.gain_db(gain_db) + + def resample(self, target_sample_rate, filter='kaiser_best'): + """Resample the audio to a target sample rate. + + Note that this is an in-place transformation. + + :param target_sample_rate: Target sample rate. + :type target_sample_rate: int + :param filter: The resampling filter to use one of {'kaiser_best', + 'kaiser_fast'}. + :type filter: str + """ + self._samples = resampy.resample( + self.samples, self.sample_rate, target_sample_rate, filter=filter) + self._sample_rate = target_sample_rate + + def pad_silence(self, duration, sides='both'): + """Pad this audio sample with a period of silence. + + Note that this is an in-place transformation. + + :param duration: Length of silence in seconds to pad. + :type duration: float + :param sides: Position for padding: + 'beginning' - adds silence in the beginning; + 'end' - adds silence in the end; + 'both' - adds silence in both the beginning and the end. + :type sides: str + :raises ValueError: If sides is not supported. + """ + if duration == 0.0: + return self + cls = type(self) + silence = self.make_silence(duration, self._sample_rate) + if sides == "beginning": + padded = cls.concatenate(silence, self) + elif sides == "end": + padded = cls.concatenate(self, silence) + elif sides == "both": + padded = cls.concatenate(silence, self, silence) + else: + raise ValueError("Unknown value for the sides %s" % sides) + self._samples = padded._samples + + def shift(self, shift_ms): + """Shift the audio in time. If `shift_ms` is positive, shift with time + advance; if negative, shift with time delay. Silence are padded to + keep the duration unchanged. + + Note that this is an in-place transformation. + + :param shift_ms: Shift time in millseconds. If positive, shift with + time advance; if negative; shift with time delay. + :type shift_ms: float + :raises ValueError: If shift_ms is longer than audio duration. + """ + if abs(shift_ms) / 1000.0 > self.duration: + raise ValueError("Absolute value of shift_ms should be smaller " + "than audio duration.") + shift_samples = int(shift_ms * self._sample_rate / 1000) + if shift_samples > 0: + # time advance + self._samples[:-shift_samples] = self._samples[shift_samples:] + self._samples[-shift_samples:] = 0 + elif shift_samples < 0: + # time delay + self._samples[-shift_samples:] = self._samples[:shift_samples] + self._samples[:-shift_samples] = 0 + + def subsegment(self, start_sec=None, end_sec=None): + """Cut the AudioSegment between given boundaries. + + Note that this is an in-place transformation. + + :param start_sec: Beginning of subsegment in seconds. + :type start_sec: float + :param end_sec: End of subsegment in seconds. + :type end_sec: float + :raise ValueError: If start_sec or end_sec is incorrectly set, e.g. out + of bounds in time. + """ + start_sec = 0.0 if start_sec is None else start_sec + end_sec = self.duration if end_sec is None else end_sec + if start_sec < 0.0: + start_sec = self.duration + start_sec + if end_sec < 0.0: + end_sec = self.duration + end_sec + if start_sec < 0.0: + raise ValueError("The slice start position (%f s) is out of " + "bounds." % start_sec) + if end_sec < 0.0: + raise ValueError("The slice end position (%f s) is out of bounds." % + end_sec) + if start_sec > end_sec: + raise ValueError("The slice start position (%f s) is later than " + "the end position (%f s)." % (start_sec, end_sec)) + if end_sec > self.duration: + raise ValueError("The slice end position (%f s) is out of bounds " + "(> %f s)" % (end_sec, self.duration)) + start_sample = int(round(start_sec * self._sample_rate)) + end_sample = int(round(end_sec * self._sample_rate)) + self._samples = self._samples[start_sample:end_sample] + + def random_subsegment(self, subsegment_length, rng=None): + """Cut the specified length of the audiosegment randomly. + + Note that this is an in-place transformation. + + :param subsegment_length: Subsegment length in seconds. + :type subsegment_length: float + :param rng: Random number generator state. + :type rng: random.Random + :raises ValueError: If the length of subsegment is greater than + the origineal segemnt. + """ + rng = random.Random() if rng is None else rng + if subsegment_length > self.duration: + raise ValueError("Length of subsegment must not be greater " + "than original segment.") + start_time = rng.uniform(0.0, self.duration - subsegment_length) + self.subsegment(start_time, start_time + subsegment_length) + + def convolve(self, impulse_segment, allow_resample=False): + """Convolve this audio segment with the given impulse segment. + + Note that this is an in-place transformation. + + :param impulse_segment: Impulse response segments. + :type impulse_segment: AudioSegment + :param allow_resample: Indicates whether resampling is allowed when + the impulse_segment has a different sample + rate from this signal. + :type allow_resample: bool + :raises ValueError: If the sample rate is not match between two + audio segments when resample is not allowed. + """ + if allow_resample and self.sample_rate != impulse_segment.sample_rate: + impulse_segment.resample(self.sample_rate) + if self.sample_rate != impulse_segment.sample_rate: + raise ValueError("Impulse segment's sample rate (%d Hz) is not " + "equal to base signal sample rate (%d Hz)." % + (impulse_segment.sample_rate, self.sample_rate)) + samples = signal.fftconvolve(self.samples, impulse_segment.samples, + "full") + self._samples = samples + + def convolve_and_normalize(self, impulse_segment, allow_resample=False): + """Convolve and normalize the resulting audio segment so that it + has the same average power as the input signal. + + Note that this is an in-place transformation. + + :param impulse_segment: Impulse response segments. + :type impulse_segment: AudioSegment + :param allow_resample: Indicates whether resampling is allowed when + the impulse_segment has a different sample + rate from this signal. + :type allow_resample: bool + """ + target_db = self.rms_db + self.convolve(impulse_segment, allow_resample=allow_resample) + self.normalize(target_db) + + def add_noise(self, + noise, + snr_dB, + allow_downsampling=False, + max_gain_db=300.0, + rng=None): + """Add the given noise segment at a specific signal-to-noise ratio. + If the noise segment is longer than this segment, a random subsegment + of matching length is sampled from it and used instead. + + Note that this is an in-place transformation. + + :param noise: Noise signal to add. + :type noise: AudioSegment + :param snr_dB: Signal-to-Noise Ratio, in decibels. + :type snr_dB: float + :param allow_downsampling: Whether to allow the noise signal to be + downsampled to match the base signal sample + rate. + :type allow_downsampling: bool + :param max_gain_db: Maximum amount of gain to apply to noise signal + before adding it in. This is to prevent attempting + to apply infinite gain to a zero signal. + :type max_gain_db: float + :param rng: Random number generator state. + :type rng: None|random.Random + :raises ValueError: If the sample rate does not match between the two + audio segments when downsampling is not allowed, or + if the duration of noise segments is shorter than + original audio segments. + """ + rng = random.Random() if rng is None else rng + if allow_downsampling and noise.sample_rate > self.sample_rate: + noise = noise.resample(self.sample_rate) + if noise.sample_rate != self.sample_rate: + raise ValueError("Noise sample rate (%d Hz) is not equal to base " + "signal sample rate (%d Hz)." % (noise.sample_rate, + self.sample_rate)) + if noise.duration < self.duration: + raise ValueError("Noise signal (%f sec) must be at least as long as" + " base signal (%f sec)." % + (noise.duration, self.duration)) + noise_gain_db = min(self.rms_db - noise.rms_db - snr_dB, max_gain_db) + noise_new = copy.deepcopy(noise) + noise_new.random_subsegment(self.duration, rng=rng) + noise_new.gain_db(noise_gain_db) + self.superimpose(noise_new) + + @property + def samples(self): + """Return audio samples. + + :return: Audio samples. + :rtype: ndarray + """ + return self._samples.copy() + + @property + def sample_rate(self): + """Return audio sample rate. + + :return: Audio sample rate. + :rtype: int + """ + return self._sample_rate + + @property + def num_samples(self): + """Return number of samples. + + :return: Number of samples. + :rtype: int + """ + return self._samples.shape[0] + + @property + def duration(self): + """Return audio duration. + + :return: Audio duration in seconds. + :rtype: float + """ + return self._samples.shape[0] / float(self._sample_rate) + + @property + def rms_db(self): + """Return root mean square energy of the audio in decibels. + + :return: Root mean square energy in decibels. + :rtype: float + """ + # square root => multiply by 10 instead of 20 for dBs + mean_square = np.mean(self._samples**2) + return 10 * np.log10(mean_square) + + def _convert_samples_to_float32(self, samples): + """Convert sample type to float32. + + Audio sample type is usually integer or float-point. + Integers will be scaled to [-1, 1] in float32. + """ + return convert_samples_to_float32(samples) + + def _convert_samples_from_float32(self, samples, dtype): + """Convert sample type from float32 to dtype. + + Audio sample type is usually integer or float-point. For integer + type, float32 will be rescaled from [-1, 1] to the maximum range + supported by the integer type. + + This is for writing a audio file. + """ + return convert_samples_from_float32(samples, dtype) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/__init__.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/augmentation.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/augmentation.py new file mode 100644 index 0000000..4c5ca4f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/augmentation.py @@ -0,0 +1,230 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the data augmentation pipeline.""" +import json +import os +from collections.abc import Sequence +from inspect import signature +from pprint import pformat + +import numpy as np + +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["AugmentationPipeline"] + +import_alias = dict( + volume="paddlespeech.s2t.frontend.augmentor.impulse_response:VolumePerturbAugmentor", + shift="paddlespeech.s2t.frontend.augmentor.shift_perturb:ShiftPerturbAugmentor", + speed="paddlespeech.s2t.frontend.augmentor.speed_perturb:SpeedPerturbAugmentor", + resample="paddlespeech.s2t.frontend.augmentor.resample:ResampleAugmentor", + bayesian_normal="paddlespeech.s2t.frontend.augmentor.online_bayesian_normalization:OnlineBayesianNormalizationAugmentor", + noise="paddlespeech.s2t.frontend.augmentor.noise_perturb:NoisePerturbAugmentor", + impulse="paddlespeech.s2t.frontend.augmentor.impulse_response:ImpulseResponseAugmentor", + specaug="paddlespeech.s2t.frontend.augmentor.spec_augment:SpecAugmentor", ) + + +class AugmentationPipeline(): + """Build a pre-processing pipeline with various augmentation models.Such a + data augmentation pipeline is oftern leveraged to augment the training + samples to make the model invariant to certain types of perturbations in the + real world, improving model's generalization ability. + + The pipeline is built according the the augmentation configuration in json + string, e.g. + + .. code-block:: + + [ { + "type": "noise", + "params": {"min_snr_dB": 10, + "max_snr_dB": 20, + "noise_manifest_path": "datasets/manifest.noise"}, + "prob": 0.0 + }, + { + "type": "speed", + "params": {"min_speed_rate": 0.9, + "max_speed_rate": 1.1}, + "prob": 1.0 + }, + { + "type": "shift", + "params": {"min_shift_ms": -5, + "max_shift_ms": 5}, + "prob": 1.0 + }, + { + "type": "volume", + "params": {"min_gain_dBFS": -10, + "max_gain_dBFS": 10}, + "prob": 0.0 + }, + { + "type": "bayesian_normal", + "params": {"target_db": -20, + "prior_db": -20, + "prior_samples": 100}, + "prob": 0.0 + } + ] + + This augmentation configuration inserts two augmentation models + into the pipeline, with one is VolumePerturbAugmentor and the other + SpeedPerturbAugmentor. "prob" indicates the probability of the current + augmentor to take effect. If "prob" is zero, the augmentor does not take + effect. + + Params: + preprocess_conf(str): Augmentation configuration in `json file` or `json string`. + random_seed(int): Random seed. + + Raises: + ValueError: If the augmentation json config is in incorrect format". + """ + + SPEC_TYPES = {'specaug'} + + def __init__(self, preprocess_conf: str, random_seed: int=0): + self._rng = np.random.RandomState(random_seed) + self.conf = {'mode': 'sequential', 'process': []} + if preprocess_conf: + if os.path.isfile(preprocess_conf): + # json file + with open(preprocess_conf, 'r') as fin: + json_string = fin.read() + else: + # json string + json_string = preprocess_conf + process = json.loads(json_string) + self.conf['process'] += process + + self._augmentors, self._rates = self._parse_pipeline_from('all') + self._audio_augmentors, self._audio_rates = self._parse_pipeline_from( + 'audio') + self._spec_augmentors, self._spec_rates = self._parse_pipeline_from( + 'feature') + logger.info( + f"Augmentation: {pformat(list(zip(self._augmentors, self._rates)))}") + + def __call__(self, xs, uttid_list=None, **kwargs): + if not isinstance(xs, Sequence): + is_batch = False + xs = [xs] + else: + is_batch = True + + if isinstance(uttid_list, str): + uttid_list = [uttid_list for _ in range(len(xs))] + + if self.conf.get("mode", "sequential") == "sequential": + for idx, (func, rate) in enumerate( + zip(self._augmentors, self._rates), 0): + if self._rng.uniform(0., 1.) >= rate: + continue + + # Derive only the args which the func has + try: + param = signature(func).parameters + except ValueError: + # Some function, e.g. built-in function, are failed + param = {} + _kwargs = {k: v for k, v in kwargs.items() if k in param} + + try: + if uttid_list is not None and "uttid" in param: + xs = [ + func(x, u, **_kwargs) + for x, u in zip(xs, uttid_list) + ] + else: + xs = [func(x, **_kwargs) for x in xs] + except Exception: + logger.fatal("Catch a exception from {}th func: {}".format( + idx, func)) + raise + else: + raise NotImplementedError( + "Not supporting mode={}".format(self.conf["mode"])) + + if is_batch: + return xs + else: + return xs[0] + + def transform_audio(self, audio_segment): + """Run the pre-processing pipeline for data augmentation. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to process. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + for augmentor, rate in zip(self._audio_augmentors, self._audio_rates): + if self._rng.uniform(0., 1.) < rate: + augmentor.transform_audio(audio_segment) + + def transform_feature(self, spec_segment): + """spectrogram augmentation. + + Args: + spec_segment (np.ndarray): audio feature, (D, T). + """ + for augmentor, rate in zip(self._spec_augmentors, self._spec_rates): + if self._rng.uniform(0., 1.) < rate: + spec_segment = augmentor.transform_feature(spec_segment) + return spec_segment + + def _parse_pipeline_from(self, aug_type='all'): + """Parse the config json to build a augmentation pipelien.""" + assert aug_type in ('audio', 'feature', 'all'), aug_type + audio_confs = [] + feature_confs = [] + all_confs = [] + for config in self.conf['process']: + all_confs.append(config) + if config["type"] in self.SPEC_TYPES: + feature_confs.append(config) + else: + audio_confs.append(config) + + if aug_type == 'audio': + aug_confs = audio_confs + elif aug_type == 'feature': + aug_confs = feature_confs + elif aug_type == 'all': + aug_confs = all_confs + else: + raise ValueError(f"Not support: {aug_type}") + + augmentors = [ + self._get_augmentor(config["type"], config["params"]) + for config in aug_confs + ] + rates = [config["prob"] for config in aug_confs] + return augmentors, rates + + def _get_augmentor(self, augmentor_type, params): + """Return an augmentation model by the type name, and pass in params.""" + class_obj = dynamic_import(augmentor_type, import_alias) + assert issubclass(class_obj, AugmentorBase) + try: + obj = class_obj(self._rng, **params) + except Exception: + raise ValueError("Unknown augmentor type [%s]." % augmentor_type) + return obj diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/base.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/base.py new file mode 100644 index 0000000..18d003c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/base.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the abstract base class for augmentation models.""" +from abc import ABCMeta +from abc import abstractmethod + + +class AugmentorBase(): + """Abstract base class for augmentation model (augmentor) class. + All augmentor classes should inherit from this class, and implement the + following abstract methods. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def __init__(self): + pass + + @abstractmethod + def __call__(self, xs): + raise NotImplementedError("AugmentorBase: Not impl __call__") + + @abstractmethod + def transform_audio(self, audio_segment): + """Adds various effects to the input audio segment. Such effects + will augment the training data to make the model invariant to certain + types of perturbations in the real world, improving model's + generalization ability. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + raise NotImplementedError("AugmentorBase: Not impl transform_audio") + + @abstractmethod + def transform_feature(self, spec_segment): + """Adds various effects to the input audo feature segment. Such effects + will augment the training data to make the model invariant to certain + types of time_mask or freq_mask in the real world, improving model's + generalization ability. + + Args: + spec_segment (Spectrogram): Spectrogram segment to add effects to. + """ + raise NotImplementedError("AugmentorBase: Not impl transform_feature") diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/impulse_response.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/impulse_response.py new file mode 100644 index 0000000..5ba45bb --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/impulse_response.py @@ -0,0 +1,52 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the impulse response augmentation model.""" +import jsonlines + +from paddlespeech.s2t.frontend.audio import AudioSegment +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class ImpulseResponseAugmentor(AugmentorBase): + """Augmentation model for adding impulse response effect. + + :param rng: Random generator object. + :type rng: random.Random + :param impulse_manifest_path: Manifest path for impulse audio data. + :type impulse_manifest_path: str + """ + + def __init__(self, rng, impulse_manifest_path): + self._rng = rng + with jsonlines.open(impulse_manifest_path, 'r') as reader: + self._impulse_manifest = list(reader) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Add impulse response effect. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + impulse_json = self._rng.choice( + self._impulse_manifest, 1, replace=False)[0] + impulse_segment = AudioSegment.from_file(impulse_json['audio_filepath']) + audio_segment.convolve(impulse_segment, allow_resample=True) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/noise_perturb.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/noise_perturb.py new file mode 100644 index 0000000..71165da --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/noise_perturb.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the noise perturb augmentation model.""" +import jsonlines + +from paddlespeech.s2t.frontend.audio import AudioSegment +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class NoisePerturbAugmentor(AugmentorBase): + """Augmentation model for adding background noise. + + :param rng: Random generator object. + :type rng: random.Random + :param min_snr_dB: Minimal signal noise ratio, in decibels. + :type min_snr_dB: float + :param max_snr_dB: Maximal signal noise ratio, in decibels. + :type max_snr_dB: float + :param noise_manifest_path: Manifest path for noise audio data. + :type noise_manifest_path: str + """ + + def __init__(self, rng, min_snr_dB, max_snr_dB, noise_manifest_path): + self._min_snr_dB = min_snr_dB + self._max_snr_dB = max_snr_dB + self._rng = rng + with jsonlines.open(noise_manifest_path, 'r') as reader: + self._noise_manifest = list(reader) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Add background noise audio. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + noise_json = self._rng.choice(self._noise_manifest, 1, replace=False)[0] + if noise_json['duration'] < audio_segment.duration: + raise RuntimeError("The duration of sampled noise audio is smaller " + "than the audio segment to add effects to.") + diff_duration = noise_json['duration'] - audio_segment.duration + start = self._rng.uniform(0, diff_duration) + end = start + audio_segment.duration + noise_segment = AudioSegment.slice_from_file( + noise_json['audio_filepath'], start=start, end=end) + snr_dB = self._rng.uniform(self._min_snr_dB, self._max_snr_dB) + audio_segment.add_noise( + noise_segment, snr_dB, allow_downsampling=True, rng=self._rng) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/online_bayesian_normalization.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/online_bayesian_normalization.py new file mode 100644 index 0000000..f9d1530 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/online_bayesian_normalization.py @@ -0,0 +1,63 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contain the online bayesian normalization augmentation model.""" +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class OnlineBayesianNormalizationAugmentor(AugmentorBase): + """Augmentation model for adding online bayesian normalization. + + :param rng: Random generator object. + :type rng: random.Random + :param target_db: Target RMS value in decibels. + :type target_db: float + :param prior_db: Prior RMS estimate in decibels. + :type prior_db: float + :param prior_samples: Prior strength in number of samples. + :type prior_samples: int + :param startup_delay: Default 0.0s. If provided, this function will + accrue statistics for the first startup_delay + seconds before applying online normalization. + :type starup_delay: float. + """ + + def __init__(self, + rng, + target_db, + prior_db, + prior_samples, + startup_delay=0.0): + self._target_db = target_db + self._prior_db = prior_db + self._prior_samples = prior_samples + self._rng = rng + self._startup_delay = startup_delay + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Normalizes the input audio using the online Bayesian approach. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegment|SpeechSegment + """ + audio_segment.normalize_online_bayesian(self._target_db, self._prior_db, + self._prior_samples, + self._startup_delay) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/resample.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/resample.py new file mode 100644 index 0000000..4e6402f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/resample.py @@ -0,0 +1,48 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contain the resample augmentation model.""" +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class ResampleAugmentor(AugmentorBase): + """Augmentation model for resampling. + + See more info here: + https://ccrma.stanford.edu/~jos/resample/index.html + + :param rng: Random generator object. + :type rng: random.Random + :param new_sample_rate: New sample rate in Hz. + :type new_sample_rate: int + """ + + def __init__(self, rng, new_sample_rate): + self._new_sample_rate = new_sample_rate + self._rng = rng + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Resamples the input audio to a target sample rate. + + Note that this is an in-place transformation. + + :param audio: Audio segment to add effects to. + :type audio: AudioSegment|SpeechSegment + """ + audio_segment.resample(self._new_sample_rate) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/shift_perturb.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/shift_perturb.py new file mode 100644 index 0000000..ed6f162 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/shift_perturb.py @@ -0,0 +1,49 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the volume perturb augmentation model.""" +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class ShiftPerturbAugmentor(AugmentorBase): + """Augmentation model for adding random shift perturbation. + + :param rng: Random generator object. + :type rng: random.Random + :param min_shift_ms: Minimal shift in milliseconds. + :type min_shift_ms: float + :param max_shift_ms: Maximal shift in milliseconds. + :type max_shift_ms: float + """ + + def __init__(self, rng, min_shift_ms, max_shift_ms): + self._min_shift_ms = min_shift_ms + self._max_shift_ms = max_shift_ms + self._rng = rng + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Shift audio. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + shift_ms = self._rng.uniform(self._min_shift_ms, self._max_shift_ms) + audio_segment.shift(shift_ms) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/spec_augment.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/spec_augment.py new file mode 100644 index 0000000..e91cfdc --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/spec_augment.py @@ -0,0 +1,256 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the volume perturb augmentation model.""" +import random + +import numpy as np +from PIL import Image +from PIL.Image import BICUBIC + +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + + +class SpecAugmentor(AugmentorBase): + """Augmentation model for Time warping, Frequency masking, Time masking. + + SpecAugment: A Simple Data Augmentation Method for Automatic Speech Recognition + https://arxiv.org/abs/1904.08779 + + SpecAugment on Large Scale Datasets + https://arxiv.org/abs/1912.05533 + + """ + + def __init__(self, + rng, + F, + T, + n_freq_masks, + n_time_masks, + p=1.0, + W=40, + adaptive_number_ratio=0, + adaptive_size_ratio=0, + max_n_time_masks=20, + replace_with_zero=True, + warp_mode='PIL'): + """SpecAugment class. + Args: + rng (random.Random): random generator object. + F (int): parameter for frequency masking + T (int): parameter for time masking + n_freq_masks (int): number of frequency masks + n_time_masks (int): number of time masks + p (float): parameter for upperbound of the time mask + W (int): parameter for time warping + adaptive_number_ratio (float): adaptive multiplicity ratio for time masking + adaptive_size_ratio (float): adaptive size ratio for time masking + max_n_time_masks (int): maximum number of time masking + replace_with_zero (bool): pad zero on mask if true else use mean + warp_mode (str): "PIL" (default, fast, not differentiable) + or "sparse_image_warp" (slow, differentiable) + """ + super().__init__() + self._rng = rng + self.inplace = True + self.replace_with_zero = replace_with_zero + + self.mode = warp_mode + self.W = W + self.F = F + self.T = T + self.n_freq_masks = n_freq_masks + self.n_time_masks = n_time_masks + self.p = p + + # adaptive SpecAugment + self.adaptive_number_ratio = adaptive_number_ratio + self.adaptive_size_ratio = adaptive_size_ratio + self.max_n_time_masks = max_n_time_masks + + if adaptive_number_ratio > 0: + self.n_time_masks = 0 + logger.info('n_time_masks is set ot zero for adaptive SpecAugment.') + if adaptive_size_ratio > 0: + self.T = 0 + logger.info('T is set to zero for adaptive SpecAugment.') + + self._freq_mask = None + self._time_mask = None + + def librispeech_basic(self): + self.W = 80 + self.F = 27 + self.T = 100 + self.n_freq_masks = 1 + self.n_time_masks = 1 + self.p = 1.0 + + def librispeech_double(self): + self.W = 80 + self.F = 27 + self.T = 100 + self.n_freq_masks = 2 + self.n_time_masks = 2 + self.p = 1.0 + + def switchboard_mild(self): + self.W = 40 + self.F = 15 + self.T = 70 + self.n_freq_masks = 2 + self.n_time_masks = 2 + self.p = 0.2 + + def switchboard_strong(self): + self.W = 40 + self.F = 27 + self.T = 70 + self.n_freq_masks = 2 + self.n_time_masks = 2 + self.p = 0.2 + + @property + def freq_mask(self): + return self._freq_mask + + @property + def time_mask(self): + return self._time_mask + + def __repr__(self): + return f"specaug: F-{self.F}, T-{self.T}, F-n-{self.n_freq_masks}, T-n-{self.n_time_masks}" + + def time_warp(self, x, mode='PIL'): + """time warp for spec augment + move random center frame by the random width ~ uniform(-window, window) + + Args: + x (np.ndarray): spectrogram (time, freq) + mode (str): PIL or sparse_image_warp + + Raises: + NotImplementedError: [description] + NotImplementedError: [description] + + Returns: + np.ndarray: time warped spectrogram (time, freq) + """ + window = max_time_warp = self.W + if window == 0: + return x + + if mode == "PIL": + t = x.shape[0] + if t - window <= window: + return x + # NOTE: randrange(a, b) emits a, a + 1, ..., b - 1 + center = random.randrange(window, t - window) + warped = random.randrange(center - window, center + + window) + 1 # 1 ... t - 1 + + left = Image.fromarray(x[:center]).resize((x.shape[1], warped), + BICUBIC) + right = Image.fromarray(x[center:]).resize((x.shape[1], t - warped), + BICUBIC) + if self.inplace: + x[:warped] = left + x[warped:] = right + return x + return np.concatenate((left, right), 0) + elif mode == "sparse_image_warp": + raise NotImplementedError('sparse_image_warp') + else: + raise NotImplementedError( + "unknown resize mode: " + mode + + ", choose one from (PIL, sparse_image_warp).") + + def mask_freq(self, x, replace_with_zero=False): + """freq mask + + Args: + x (np.ndarray): spectrogram (time, freq) + replace_with_zero (bool, optional): Defaults to False. + + Returns: + np.ndarray: freq mask spectrogram (time, freq) + """ + n_bins = x.shape[1] + for i in range(0, self.n_freq_masks): + f = int(self._rng.uniform(low=0, high=self.F)) + f_0 = int(self._rng.uniform(low=0, high=n_bins - f)) + assert f_0 <= f_0 + f + if replace_with_zero: + x[:, f_0:f_0 + f] = 0 + else: + x[:, f_0:f_0 + f] = x.mean() + self._freq_mask = (f_0, f_0 + f) + return x + + def mask_time(self, x, replace_with_zero=False): + """time mask + + Args: + x (np.ndarray): spectrogram (time, freq) + replace_with_zero (bool, optional): Defaults to False. + + Returns: + np.ndarray: time mask spectrogram (time, freq) + """ + n_frames = x.shape[0] + + if self.adaptive_number_ratio > 0: + n_masks = int(n_frames * self.adaptive_number_ratio) + n_masks = min(n_masks, self.max_n_time_masks) + else: + n_masks = self.n_time_masks + + if self.adaptive_size_ratio > 0: + T = self.adaptive_size_ratio * n_frames + else: + T = self.T + + for i in range(n_masks): + t = int(self._rng.uniform(low=0, high=T)) + t = min(t, int(n_frames * self.p)) + t_0 = int(self._rng.uniform(low=0, high=n_frames - t)) + assert t_0 <= t_0 + t + if replace_with_zero: + x[t_0:t_0 + t, :] = 0 + else: + x[t_0:t_0 + t, :] = x.mean() + self._time_mask = (t_0, t_0 + t) + return x + + def __call__(self, x, train=True): + if not train: + return x + return self.transform_feature(x) + + def transform_feature(self, x: np.ndarray): + """ + Args: + x (np.ndarray): `[T, F]` + Returns: + x (np.ndarray): `[T, F]` + """ + assert isinstance(x, np.ndarray) + assert x.ndim == 2 + x = self.time_warp(x, self.mode) + x = self.mask_freq(x, self.replace_with_zero) + x = self.mask_time(x, self.replace_with_zero) + return x diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/speed_perturb.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/speed_perturb.py new file mode 100644 index 0000000..af0b23e --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/speed_perturb.py @@ -0,0 +1,106 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contain the speech perturbation augmentation model.""" +import numpy as np + +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class SpeedPerturbAugmentor(AugmentorBase): + """Augmentation model for adding speed perturbation.""" + + def __init__(self, rng, min_speed_rate=0.9, max_speed_rate=1.1, + num_rates=3): + """speed perturbation. + + The speed perturbation in kaldi uses sox-speed instead of sox-tempo, + and sox-speed just to resample the input, + i.e pitch and tempo are changed both. + + "Why use speed option instead of tempo -s in SoX for speed perturbation" + https://groups.google.com/forum/#!topic/kaldi-help/8OOG7eE4sZ8 + + Sox speed: + https://pysox.readthedocs.io/en/latest/api.html#sox.transform.Transformer + + See reference paper here: + http://www.danielpovey.com/files/2015_interspeech_augmentation.pdf + + Espnet: + https://espnet.github.io/espnet/_modules/espnet/transform/perturb.html + + Nemo: + https://github.com/NVIDIA/NeMo/blob/main/nemo/collections/asr/parts/perturb.py#L92 + + Args: + rng (random.Random): Random generator object. + min_speed_rate (float): Lower bound of new speed rate to sample and should + not be smaller than 0.9. + max_speed_rate (float): Upper bound of new speed rate to sample and should + not be larger than 1.1. + num_rates (int, optional): Number of discrete rates to allow. + Can be a positive or negative integer. Defaults to 3. + If a positive integer greater than 0 is provided, the range of + speed rates will be discretized into `num_rates` values. + If a negative integer or 0 is provided, the full range of speed rates + will be sampled uniformly. + Note: If a positive integer is provided and the resultant discretized + range of rates contains the value '1.0', then those samples with rate=1.0, + will not be augmented at all and simply skipped. This is to unnecessary + augmentation and increase computation time. Effective augmentation chance + in such a case is = `prob * (num_rates - 1 / num_rates) * 100`% chance + where `prob` is the global probability of a sample being augmented. + + Raises: + ValueError: when speed_rate error + """ + if min_speed_rate < 0.9: + raise ValueError( + "Sampling speed below 0.9 can cause unnatural effects") + if max_speed_rate > 1.1: + raise ValueError( + "Sampling speed above 1.1 can cause unnatural effects") + self._min_rate = min_speed_rate + self._max_rate = max_speed_rate + self._rng = rng + self._num_rates = num_rates + if num_rates > 0: + self._rates = np.linspace( + self._min_rate, self._max_rate, self._num_rates, endpoint=True) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Sample a new speed rate from the given range and + changes the speed of the given audio clip. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegment|SpeechSegment + """ + if self._num_rates < 0: + speed_rate = self._rng.uniform(self._min_rate, self._max_rate) + else: + speed_rate = self._rng.choice(self._rates) + + # Skip perturbation in case of identity speed rate + if speed_rate == 1.0: + return + + audio_segment.change_speed(speed_rate) diff --git a/ernie-sat/paddlespeech/s2t/frontend/augmentor/volume_perturb.py b/ernie-sat/paddlespeech/s2t/frontend/augmentor/volume_perturb.py new file mode 100644 index 0000000..8cd2dc0 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/augmentor/volume_perturb.py @@ -0,0 +1,55 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the volume perturb augmentation model.""" +from paddlespeech.s2t.frontend.augmentor.base import AugmentorBase + + +class VolumePerturbAugmentor(AugmentorBase): + """Augmentation model for adding random volume perturbation. + + This is used for multi-loudness training of PCEN. See + + https://arxiv.org/pdf/1607.05666v1.pdf + + for more details. + + :param rng: Random generator object. + :type rng: random.Random + :param min_gain_dBFS: Minimal gain in dBFS. + :type min_gain_dBFS: float + :param max_gain_dBFS: Maximal gain in dBFS. + :type max_gain_dBFS: float + """ + + def __init__(self, rng, min_gain_dBFS, max_gain_dBFS): + self._min_gain_dBFS = min_gain_dBFS + self._max_gain_dBFS = max_gain_dBFS + self._rng = rng + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + self.transform_audio(x) + return x + + def transform_audio(self, audio_segment): + """Change audio loadness. + + Note that this is an in-place transformation. + + :param audio_segment: Audio segment to add effects to. + :type audio_segment: AudioSegmenet|SpeechSegment + """ + gain = self._rng.uniform(self._min_gain_dBFS, self._max_gain_dBFS) + audio_segment.gain_db(gain) diff --git a/ernie-sat/paddlespeech/s2t/frontend/featurizer/__init__.py b/ernie-sat/paddlespeech/s2t/frontend/featurizer/__init__.py new file mode 100644 index 0000000..6992700 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/featurizer/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .audio_featurizer import AudioFeaturizer #noqa: F401 +from .speech_featurizer import SpeechFeaturizer +from .text_featurizer import TextFeaturizer diff --git a/ernie-sat/paddlespeech/s2t/frontend/featurizer/audio_featurizer.py b/ernie-sat/paddlespeech/s2t/frontend/featurizer/audio_featurizer.py new file mode 100644 index 0000000..6f3b646 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/featurizer/audio_featurizer.py @@ -0,0 +1,363 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the audio featurizer class.""" +import numpy as np +from python_speech_features import delta +from python_speech_features import logfbank +from python_speech_features import mfcc + + +class AudioFeaturizer(): + """Audio featurizer, for extracting features from audio contents of + AudioSegment or SpeechSegment. + + Currently, it supports feature types of linear spectrogram and mfcc. + + :param spectrum_type: Specgram feature type. Options: 'linear'. + :type spectrum_type: str + :param stride_ms: Striding size (in milliseconds) for generating frames. + :type stride_ms: float + :param window_ms: Window size (in milliseconds) for generating frames. + :type window_ms: float + :param max_freq: When spectrum_type is 'linear', only FFT bins + corresponding to frequencies between [0, max_freq] are + returned; when spectrum_type is 'mfcc', max_feq is the + highest band edge of mel filters. + :types max_freq: None|float + :param target_sample_rate: Audio are resampled (if upsampling or + downsampling is allowed) to this before + extracting spectrogram features. + :type target_sample_rate: float + :param use_dB_normalization: Whether to normalize the audio to a certain + decibels before extracting the features. + :type use_dB_normalization: bool + :param target_dB: Target audio decibels for normalization. + :type target_dB: float + """ + + def __init__(self, + spectrum_type: str='linear', + feat_dim: int=None, + delta_delta: bool=False, + stride_ms=10.0, + window_ms=20.0, + n_fft=None, + max_freq=None, + target_sample_rate=16000, + use_dB_normalization=True, + target_dB=-20, + dither=1.0): + self._spectrum_type = spectrum_type + # mfcc and fbank using `feat_dim` + self._feat_dim = feat_dim + # mfcc and fbank using `delta-delta` + self._delta_delta = delta_delta + self._stride_ms = stride_ms + self._window_ms = window_ms + self._max_freq = max_freq + self._target_sample_rate = target_sample_rate + self._use_dB_normalization = use_dB_normalization + self._target_dB = target_dB + self._fft_point = n_fft + self._dither = dither + + def featurize(self, + audio_segment, + allow_downsampling=True, + allow_upsampling=True): + """Extract audio features from AudioSegment or SpeechSegment. + + :param audio_segment: Audio/speech segment to extract features from. + :type audio_segment: AudioSegment|SpeechSegment + :param allow_downsampling: Whether to allow audio downsampling before + featurizing. + :type allow_downsampling: bool + :param allow_upsampling: Whether to allow audio upsampling before + featurizing. + :type allow_upsampling: bool + :return: Spectrogram audio feature in 2darray. + :rtype: ndarray + :raises ValueError: If audio sample rate is not supported. + """ + # upsampling or downsampling + if ((audio_segment.sample_rate > self._target_sample_rate and + allow_downsampling) or + (audio_segment.sample_rate < self._target_sample_rate and + allow_upsampling)): + audio_segment.resample(self._target_sample_rate) + if audio_segment.sample_rate != self._target_sample_rate: + raise ValueError("Audio sample rate is not supported. " + "Turn allow_downsampling or allow up_sampling on.") + # decibel normalization + if self._use_dB_normalization: + audio_segment.normalize(target_db=self._target_dB) + # extract spectrogram + return self._compute_specgram(audio_segment) + + @property + def stride_ms(self): + return self._stride_ms + + @property + def feature_size(self): + """audio feature size""" + feat_dim = 0 + if self._spectrum_type == 'linear': + fft_point = self._window_ms if self._fft_point is None else self._fft_point + feat_dim = int(fft_point * (self._target_sample_rate / 1000) / 2 + + 1) + elif self._spectrum_type == 'mfcc': + # mfcc, delta, delta-delta + feat_dim = int(self._feat_dim * + 3) if self._delta_delta else int(self._feat_dim) + elif self._spectrum_type == 'fbank': + # fbank, delta, delta-delta + feat_dim = int(self._feat_dim * + 3) if self._delta_delta else int(self._feat_dim) + else: + raise ValueError("Unknown spectrum_type %s. " + "Supported values: linear." % self._spectrum_type) + return feat_dim + + def _compute_specgram(self, audio_segment): + """Extract various audio features.""" + sample_rate = audio_segment.sample_rate + if self._spectrum_type == 'linear': + samples = audio_segment.samples + return self._compute_linear_specgram( + samples, + sample_rate, + stride_ms=self._stride_ms, + window_ms=self._window_ms, + max_freq=self._max_freq) + elif self._spectrum_type == 'mfcc': + samples = audio_segment.to('int16') + return self._compute_mfcc( + samples, + sample_rate, + feat_dim=self._feat_dim, + stride_ms=self._stride_ms, + window_ms=self._window_ms, + max_freq=self._max_freq, + dither=self._dither, + delta_delta=self._delta_delta) + elif self._spectrum_type == 'fbank': + samples = audio_segment.to('int16') + return self._compute_fbank( + samples, + sample_rate, + feat_dim=self._feat_dim, + stride_ms=self._stride_ms, + window_ms=self._window_ms, + max_freq=self._max_freq, + dither=self._dither, + delta_delta=self._delta_delta) + else: + raise ValueError("Unknown spectrum_type %s. " + "Supported values: linear." % self._spectrum_type) + + def _specgram_real(self, samples, window_size, stride_size, sample_rate): + """Compute the spectrogram for samples from a real signal.""" + # extract strided windows + truncate_size = (len(samples) - window_size) % stride_size + samples = samples[:len(samples) - truncate_size] + nshape = (window_size, (len(samples) - window_size) // stride_size + 1) + nstrides = (samples.strides[0], samples.strides[0] * stride_size) + windows = np.lib.stride_tricks.as_strided( + samples, shape=nshape, strides=nstrides) + assert np.all( + windows[:, 1] == samples[stride_size:(stride_size + window_size)]) + # window weighting, squared Fast Fourier Transform (fft), scaling + weighting = np.hanning(window_size)[:, None] + # https://numpy.org/doc/stable/reference/generated/numpy.fft.rfft.html + fft = np.fft.rfft(windows * weighting, n=None, axis=0) + fft = np.absolute(fft) + fft = fft**2 + scale = np.sum(weighting**2) * sample_rate + fft[1:-1, :] *= (2.0 / scale) + fft[(0, -1), :] /= scale + # prepare fft frequency list + freqs = float(sample_rate) / window_size * np.arange(fft.shape[0]) + return fft, freqs + + def _compute_linear_specgram(self, + samples, + sample_rate, + stride_ms=10.0, + window_ms=20.0, + max_freq=None, + eps=1e-14): + """Compute the linear spectrogram from FFT energy. + + Args: + samples ([type]): [description] + sample_rate ([type]): [description] + stride_ms (float, optional): [description]. Defaults to 10.0. + window_ms (float, optional): [description]. Defaults to 20.0. + max_freq ([type], optional): [description]. Defaults to None. + eps ([type], optional): [description]. Defaults to 1e-14. + + Raises: + ValueError: [description] + ValueError: [description] + + Returns: + np.ndarray: log spectrogram, (time, freq) + """ + if max_freq is None: + max_freq = sample_rate / 2 + if max_freq > sample_rate / 2: + raise ValueError("max_freq must not be greater than half of " + "sample rate.") + if stride_ms > window_ms: + raise ValueError("Stride size must not be greater than " + "window size.") + stride_size = int(0.001 * sample_rate * stride_ms) + window_size = int(0.001 * sample_rate * window_ms) + specgram, freqs = self._specgram_real( + samples, + window_size=window_size, + stride_size=stride_size, + sample_rate=sample_rate) + ind = np.where(freqs <= max_freq)[0][-1] + 1 + # (freq, time) + spec = np.log(specgram[:ind, :] + eps) + return np.transpose(spec) + + def _concat_delta_delta(self, feat): + """append delat, delta-delta feature. + + Args: + feat (np.ndarray): (T, D) + + Returns: + np.ndarray: feat with delta-delta, (T, 3*D) + """ + # Deltas + d_feat = delta(feat, 2) + # Deltas-Deltas + dd_feat = delta(feat, 2) + # concat above three features + concat_feat = np.concatenate((feat, d_feat, dd_feat), axis=1) + return concat_feat + + def _compute_mfcc(self, + samples, + sample_rate, + feat_dim=13, + stride_ms=10.0, + window_ms=25.0, + max_freq=None, + dither=1.0, + delta_delta=True): + """Compute mfcc from samples. + + Args: + samples (np.ndarray, np.int16): the audio signal from which to compute features. + sample_rate (float): the sample rate of the signal we are working with, in Hz. + feat_dim (int): the number of cepstrum to return, default 13. + stride_ms (float, optional): stride length in ms. Defaults to 10.0. + window_ms (float, optional): window length in ms. Defaults to 25.0. + max_freq ([type], optional): highest band edge of mel filters. In Hz, default is samplerate/2. Defaults to None. + delta_delta (bool, optional): Whether with delta delta. Defaults to False. + + Raises: + ValueError: max_freq > samplerate/2 + ValueError: stride_ms > window_ms + + Returns: + np.ndarray: mfcc feature, (D, T). + """ + if max_freq is None: + max_freq = sample_rate / 2 + if max_freq > sample_rate / 2: + raise ValueError("max_freq must not be greater than half of " + "sample rate.") + if stride_ms > window_ms: + raise ValueError("Stride size must not be greater than " + "window size.") + # compute the 13 cepstral coefficients, and the first one is replaced + # by log(frame energy), (T, D) + mfcc_feat = mfcc( + signal=samples, + samplerate=sample_rate, + winlen=0.001 * window_ms, + winstep=0.001 * stride_ms, + numcep=feat_dim, + nfilt=23, + nfft=512, + lowfreq=20, + highfreq=max_freq, + dither=dither, + remove_dc_offset=True, + preemph=0.97, + ceplifter=22, + useEnergy=True, + winfunc='povey') + if delta_delta: + mfcc_feat = self._concat_delta_delta(mfcc_feat) + return mfcc_feat + + def _compute_fbank(self, + samples, + sample_rate, + feat_dim=40, + stride_ms=10.0, + window_ms=25.0, + max_freq=None, + dither=1.0, + delta_delta=False): + """Compute logfbank from samples. + + Args: + samples (np.ndarray, np.int16): the audio signal from which to compute features. Should be an N*1 array + sample_rate (float): the sample rate of the signal we are working with, in Hz. + feat_dim (int): the number of cepstrum to return, default 13. + stride_ms (float, optional): stride length in ms. Defaults to 10.0. + window_ms (float, optional): window length in ms. Defaults to 20.0. + max_freq (float, optional): highest band edge of mel filters. In Hz, default is samplerate/2. Defaults to None. + delta_delta (bool, optional): Whether with delta delta. Defaults to False. + + Raises: + ValueError: max_freq > samplerate/2 + ValueError: stride_ms > window_ms + + Returns: + np.ndarray: mfcc feature, (D, T). + """ + if max_freq is None: + max_freq = sample_rate / 2 + if max_freq > sample_rate / 2: + raise ValueError("max_freq must not be greater than half of " + "sample rate.") + if stride_ms > window_ms: + raise ValueError("Stride size must not be greater than " + "window size.") + # (T, D) + fbank_feat = logfbank( + signal=samples, + samplerate=sample_rate, + winlen=0.001 * window_ms, + winstep=0.001 * stride_ms, + nfilt=feat_dim, + nfft=512, + lowfreq=20, + highfreq=max_freq, + dither=dither, + remove_dc_offset=True, + preemph=0.97, + wintype='povey') + if delta_delta: + fbank_feat = self._concat_delta_delta(fbank_feat) + return fbank_feat diff --git a/ernie-sat/paddlespeech/s2t/frontend/featurizer/speech_featurizer.py b/ernie-sat/paddlespeech/s2t/frontend/featurizer/speech_featurizer.py new file mode 100644 index 0000000..9dc8682 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/featurizer/speech_featurizer.py @@ -0,0 +1,106 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the speech featurizer class.""" +from paddlespeech.s2t.frontend.featurizer.audio_featurizer import AudioFeaturizer +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer + + +class SpeechFeaturizer(): + """Speech and Text feature extraction. + """ + + def __init__(self, + unit_type, + vocab_filepath, + spm_model_prefix=None, + spectrum_type='linear', + feat_dim=None, + delta_delta=False, + stride_ms=10.0, + window_ms=20.0, + n_fft=None, + max_freq=None, + target_sample_rate=16000, + use_dB_normalization=True, + target_dB=-20, + dither=1.0, + maskctc=False): + self.stride_ms = stride_ms + self.window_ms = window_ms + + self.audio_feature = AudioFeaturizer( + spectrum_type=spectrum_type, + feat_dim=feat_dim, + delta_delta=delta_delta, + stride_ms=stride_ms, + window_ms=window_ms, + n_fft=n_fft, + max_freq=max_freq, + target_sample_rate=target_sample_rate, + use_dB_normalization=use_dB_normalization, + target_dB=target_dB, + dither=dither) + self.feature_size = self.audio_feature.feature_size + + self.text_feature = TextFeaturizer( + unit_type=unit_type, + vocab=vocab_filepath, + spm_model_prefix=spm_model_prefix, + maskctc=maskctc) + self.vocab_size = self.text_feature.vocab_size + + def featurize(self, speech_segment, keep_transcription_text): + """Extract features for speech segment. + + 1. For audio parts, extract the audio features. + 2. For transcript parts, keep the original text or convert text string + to a list of token indices in char-level. + + Args: + speech_segment (SpeechSegment): Speech segment to extract features from. + keep_transcription_text (bool): True, keep transcript text, False, token ids + + Returns: + tuple: 1) spectrogram audio feature in 2darray, 2) list oftoken indices. + """ + spec_feature = self.audio_feature.featurize(speech_segment) + + if keep_transcription_text: + return spec_feature, speech_segment.transcript + + if speech_segment.has_token: + text_ids = speech_segment.token_ids + else: + text_ids = self.text_feature.featurize(speech_segment.transcript) + return spec_feature, text_ids + + def text_featurize(self, text, keep_transcription_text): + """Extract features for speech segment. + + 1. For audio parts, extract the audio features. + 2. For transcript parts, keep the original text or convert text string + to a list of token indices in char-level. + + Args: + text (str): text. + keep_transcription_text (bool): True, keep transcript text, False, token ids + + Returns: + (str|List[int]): text, or list of token indices. + """ + if keep_transcription_text: + return text + + text_ids = self.text_feature.featurize(text) + return text_ids diff --git a/ernie-sat/paddlespeech/s2t/frontend/featurizer/text_featurizer.py b/ernie-sat/paddlespeech/s2t/frontend/featurizer/text_featurizer.py new file mode 100644 index 0000000..0c0fa5e --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/featurizer/text_featurizer.py @@ -0,0 +1,235 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the text featurizer class.""" +from pprint import pformat +from typing import Union + +import sentencepiece as spm + +from ..utility import BLANK +from ..utility import EOS +from ..utility import load_dict +from ..utility import MASKCTC +from ..utility import SOS +from ..utility import SPACE +from ..utility import UNK +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["TextFeaturizer"] + + +class TextFeaturizer(): + def __init__(self, unit_type, vocab, spm_model_prefix=None, maskctc=False): + """Text featurizer, for processing or extracting features from text. + + Currently, it supports char/word/sentence-piece level tokenizing and conversion into + a list of token indices. Note that the token indexing order follows the + given vocabulary file. + + Args: + unit_type (str): unit type, e.g. char, word, spm + vocab Option[str, list]: Filepath to load vocabulary for token indices conversion, or vocab list. + spm_model_prefix (str, optional): spm model prefix. Defaults to None. + """ + assert unit_type in ('char', 'spm', 'word') + self.unit_type = unit_type + self.unk = UNK + self.maskctc = maskctc + + if vocab: + self.vocab_dict, self._id2token, self.vocab_list, self.unk_id, self.eos_id, self.blank_id = self._load_vocabulary_from_file( + vocab, maskctc) + self.vocab_size = len(self.vocab_list) + else: + logger.warning("TextFeaturizer: not have vocab file or vocab list.") + + if unit_type == 'spm': + spm_model = spm_model_prefix + '.model' + self.sp = spm.SentencePieceProcessor() + self.sp.Load(spm_model) + + def tokenize(self, text, replace_space=True): + if self.unit_type == 'char': + tokens = self.char_tokenize(text, replace_space) + elif self.unit_type == 'word': + tokens = self.word_tokenize(text) + else: # spm + tokens = self.spm_tokenize(text) + return tokens + + def detokenize(self, tokens): + if self.unit_type == 'char': + text = self.char_detokenize(tokens) + elif self.unit_type == 'word': + text = self.word_detokenize(tokens) + else: # spm + text = self.spm_detokenize(tokens) + return text + + def featurize(self, text): + """Convert text string to a list of token indices. + + Args: + text (str): Text to process. + + Returns: + List[int]: List of token indices. + """ + tokens = self.tokenize(text) + ids = [] + for token in tokens: + if token not in self.vocab_dict: + logger.debug(f"Text Token: {token} -> {self.unk}") + token = self.unk + ids.append(self.vocab_dict[token]) + return ids + + def defeaturize(self, idxs): + """Convert a list of token indices to text string, + ignore index after eos_id. + + Args: + idxs (List[int]): List of token indices. + + Returns: + str: Text. + """ + tokens = [] + for idx in idxs: + if idx == self.eos_id: + break + tokens.append(self._id2token[idx]) + text = self.detokenize(tokens) + return text + + def char_tokenize(self, text, replace_space=True): + """Character tokenizer. + + Args: + text (str): text string. + replace_space (bool): False only used by build_vocab.py. + + Returns: + List[str]: tokens. + """ + text = text.strip() + if replace_space: + text_list = [SPACE if item == " " else item for item in list(text)] + else: + text_list = list(text) + return text_list + + def char_detokenize(self, tokens): + """Character detokenizer. + + Args: + tokens (List[str]): tokens. + + Returns: + str: text string. + """ + tokens = [t.replace(SPACE, " ") for t in tokens] + return "".join(tokens) + + def word_tokenize(self, text): + """Word tokenizer, separate by .""" + return text.strip().split() + + def word_detokenize(self, tokens): + """Word detokenizer, separate by .""" + return " ".join(tokens) + + def spm_tokenize(self, text): + """spm tokenize. + + Args: + text (str): text string. + + Returns: + List[str]: sentence pieces str code + """ + stats = {"num_empty": 0, "num_filtered": 0} + + def valid(line): + return True + + def encode(l): + return self.sp.EncodeAsPieces(l) + + def encode_line(line): + line = line.strip() + if len(line) > 0: + line = encode(line) + if valid(line): + return line + else: + stats["num_filtered"] += 1 + else: + stats["num_empty"] += 1 + return None + + enc_line = encode_line(text) + return enc_line + + def spm_detokenize(self, tokens, input_format='piece'): + """spm detokenize. + + Args: + ids (List[str]): tokens. + + Returns: + str: text + """ + if input_format == "piece": + + def decode(l): + return "".join(self.sp.DecodePieces(l)) + elif input_format == "id": + + def decode(l): + return "".join(self.sp.DecodeIds(l)) + + return decode(tokens) + + def _load_vocabulary_from_file(self, vocab: Union[str, list], + maskctc: bool): + """Load vocabulary from file.""" + if isinstance(vocab, list): + vocab_list = vocab + else: + vocab_list = load_dict(vocab, maskctc) + assert vocab_list is not None + logger.debug(f"Vocab: {pformat(vocab_list)}") + + id2token = dict( + [(idx, token) for (idx, token) in enumerate(vocab_list)]) + token2id = dict( + [(token, idx) for (idx, token) in enumerate(vocab_list)]) + + blank_id = vocab_list.index(BLANK) if BLANK in vocab_list else -1 + maskctc_id = vocab_list.index(MASKCTC) if MASKCTC in vocab_list else -1 + unk_id = vocab_list.index(UNK) if UNK in vocab_list else -1 + eos_id = vocab_list.index(EOS) if EOS in vocab_list else -1 + sos_id = vocab_list.index(SOS) if SOS in vocab_list else -1 + space_id = vocab_list.index(SPACE) if SPACE in vocab_list else -1 + + logger.info(f"BLANK id: {blank_id}") + logger.info(f"UNK id: {unk_id}") + logger.info(f"EOS id: {eos_id}") + logger.info(f"SOS id: {sos_id}") + logger.info(f"SPACE id: {space_id}") + logger.info(f"MASKCTC id: {maskctc_id}") + return token2id, id2token, vocab_list, unk_id, eos_id, blank_id diff --git a/ernie-sat/paddlespeech/s2t/frontend/normalizer.py b/ernie-sat/paddlespeech/s2t/frontend/normalizer.py new file mode 100644 index 0000000..b596b2a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/normalizer.py @@ -0,0 +1,200 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains feature normalizers.""" +import json + +import jsonlines +import numpy as np +import paddle +from paddle.io import DataLoader +from paddle.io import Dataset + +from paddlespeech.s2t.frontend.audio import AudioSegment +from paddlespeech.s2t.frontend.utility import load_cmvn +from paddlespeech.s2t.utils.log import Log + +__all__ = ["FeatureNormalizer"] + +logger = Log(__name__).getlog() + + +# https://github.com/PaddlePaddle/Paddle/pull/31481 +class CollateFunc(object): + def __init__(self, feature_func): + self.feature_func = feature_func + + def __call__(self, batch): + mean_stat = None + var_stat = None + number = 0 + for item in batch: + audioseg = AudioSegment.from_file(item['feat']) + feat = self.feature_func(audioseg) #(T, D) + + sums = np.sum(feat, axis=0) + if mean_stat is None: + mean_stat = sums + else: + mean_stat += sums + + square_sums = np.sum(np.square(feat), axis=0) + if var_stat is None: + var_stat = square_sums + else: + var_stat += square_sums + + number += feat.shape[0] + return number, mean_stat, var_stat + + +class AudioDataset(Dataset): + def __init__(self, manifest_path, num_samples=-1, rng=None, random_seed=0): + self._rng = rng if rng else np.random.RandomState(random_seed) + + with jsonlines.open(manifest_path, 'r') as reader: + manifest = list(reader) + + if num_samples == -1: + sampled_manifest = manifest + else: + sampled_manifest = self._rng.choice( + manifest, num_samples, replace=False) + self.items = sampled_manifest + + def __len__(self): + return len(self.items) + + def __getitem__(self, idx): + return self.items[idx] + + +class FeatureNormalizer(object): + """Feature normalizer. Normalize features to be of zero mean and unit + stddev. + + if mean_std_filepath is provided (not None), the normalizer will directly + initilize from the file. Otherwise, both manifest_path and featurize_func + should be given for on-the-fly mean and stddev computing. + + :param mean_std_filepath: File containing the pre-computed mean and stddev. + :type mean_std_filepath: None|str + :param manifest_path: Manifest of instances for computing mean and stddev. + :type meanifest_path: None|str + :param featurize_func: Function to extract features. It should be callable + with ``featurize_func(audio_segment)``. + :type featurize_func: None|callable + :param num_samples: Number of random samples for computing mean and stddev. + :type num_samples: int + :param random_seed: Random seed for sampling instances. + :type random_seed: int + :raises ValueError: If both mean_std_filepath and manifest_path + (or both mean_std_filepath and featurize_func) are None. + """ + + def __init__(self, + mean_std_filepath, + manifest_path=None, + featurize_func=None, + num_samples=500, + num_workers=0, + random_seed=0): + if not mean_std_filepath: + if not (manifest_path and featurize_func): + raise ValueError("If mean_std_filepath is None, meanifest_path " + "and featurize_func should not be None.") + self._rng = np.random.RandomState(random_seed) + self._compute_mean_std(manifest_path, featurize_func, num_samples, + num_workers) + else: + mean_std = mean_std_filepath + self._read_mean_std_from_file(mean_std) + + def apply(self, features): + """Normalize features to be of zero mean and unit stddev. + + :param features: Input features to be normalized. + :type features: ndarray, shape (T, D) + :param eps: added to stddev to provide numerical stablibity. + :type eps: float + :return: Normalized features. + :rtype: ndarray + """ + return (features - self._mean) * self._istd + + def _read_mean_std_from_file(self, mean_std, eps=1e-20): + """Load mean and std from file.""" + if isinstance(mean_std, list): + mean = mean_std[0]['cmvn_stats']['mean'] + istd = mean_std[0]['cmvn_stats']['istd'] + else: + filetype = mean_std.split(".")[-1] + mean, istd = load_cmvn(mean_std, filetype=filetype) + self._mean = np.expand_dims(mean, axis=0) + self._istd = np.expand_dims(istd, axis=0) + + def write_to_file(self, filepath): + """Write the mean and stddev to the file. + + :param filepath: File to write mean and stddev. + :type filepath: str + """ + with open(filepath, 'w') as fout: + fout.write(json.dumps(self.cmvn_info)) + + def _compute_mean_std(self, + manifest_path, + featurize_func, + num_samples, + num_workers, + batch_size=64, + eps=1e-20): + """Compute mean and std from randomly sampled instances.""" + paddle.set_device('cpu') + + collate_func = CollateFunc(featurize_func) + dataset = AudioDataset(manifest_path, num_samples, self._rng) + data_loader = DataLoader( + dataset, + batch_size=batch_size, + shuffle=False, + num_workers=num_workers, + collate_fn=collate_func) + + with paddle.no_grad(): + all_mean_stat = None + all_var_stat = None + all_number = 0 + wav_number = 0 + for i, batch in enumerate(data_loader): + number, mean_stat, var_stat = batch + if i == 0: + all_mean_stat = mean_stat + all_var_stat = var_stat + else: + all_mean_stat += mean_stat + all_var_stat += var_stat + all_number += number + wav_number += batch_size + + if wav_number % 1000 == 0: + logger.info( + f'process {wav_number} wavs,{all_number} frames.') + + self.cmvn_info = { + 'mean_stat': list(all_mean_stat.tolist()), + 'var_stat': list(all_var_stat.tolist()), + 'frame_num': all_number, + } + + return self.cmvn_info diff --git a/ernie-sat/paddlespeech/s2t/frontend/speech.py b/ernie-sat/paddlespeech/s2t/frontend/speech.py new file mode 100644 index 0000000..9699710 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/speech.py @@ -0,0 +1,243 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains the speech segment class.""" +import numpy as np + +from paddlespeech.s2t.frontend.audio import AudioSegment + + +class SpeechSegment(AudioSegment): + """Speech Segment with Text + + Args: + AudioSegment (AudioSegment): Audio Segment + """ + + def __init__(self, + samples, + sample_rate, + transcript, + tokens=None, + token_ids=None): + """Speech segment abstraction, a subclass of AudioSegment, + with an additional transcript. + + Args: + samples (ndarray.float32): Audio samples [num_samples x num_channels]. + sample_rate (int): Audio sample rate. + transcript (str): Transcript text for the speech. + tokens (List[str], optinal): Transcript tokens for the speech. + token_ids (List[int], optional): Transcript token ids for the speech. + """ + AudioSegment.__init__(self, samples, sample_rate) + self._transcript = transcript + # must init `tokens` with `token_ids` at the same time + self._tokens = tokens + self._token_ids = token_ids + + def __eq__(self, other): + """Return whether two objects are equal. + + Returns: + bool: True, when equal to other + """ + if not AudioSegment.__eq__(self, other): + return False + if self._transcript != other._transcript: + return False + if self.has_token and other.has_token: + if self._tokens != other._tokens: + return False + if self._token_ids != other._token_ids: + return False + return True + + def __ne__(self, other): + """Return whether two objects are unequal.""" + return not self.__eq__(other) + + @classmethod + def from_file(cls, + filepath, + transcript, + tokens=None, + token_ids=None, + infos=None): + """Create speech segment from audio file and corresponding transcript. + + Args: + filepath (str|file): Filepath or file object to audio file. + transcript (str): Transcript text for the speech. + tokens (List[str], optional): text tokens. Defaults to None. + token_ids (List[int], optional): text token ids. Defaults to None. + infos (TarLocalData, optional): tar2obj and tar2infos. Defaults to None. + + Returns: + SpeechSegment: Speech segment instance. + """ + audio = AudioSegment.from_file(filepath, infos) + return cls(audio.samples, audio.sample_rate, transcript, tokens, + token_ids) + + @classmethod + def from_bytes(cls, bytes, transcript, tokens=None, token_ids=None): + """Create speech segment from a byte string and corresponding + + Args: + filepath (str|file): Filepath or file object to audio file. + transcript (str): Transcript text for the speech. + tokens (List[str], optional): text tokens. Defaults to None. + token_ids (List[int], optional): text token ids. Defaults to None. + + Returns: + SpeechSegment: Speech segment instance. + """ + audio = AudioSegment.from_bytes(bytes) + return cls(audio.samples, audio.sample_rate, transcript, tokens, + token_ids) + + @classmethod + def from_pcm(cls, + samples, + sample_rate, + transcript, + tokens=None, + token_ids=None): + """Create speech segment from pcm on online mode + Args: + samples (numpy.ndarray): Audio samples [num_samples x num_channels]. + sample_rate (int): Audio sample rate. + transcript (str): Transcript text for the speech. + tokens (List[str], optional): text tokens. Defaults to None. + token_ids (List[int], optional): text token ids. Defaults to None. + Returns: + SpeechSegment: Speech segment instance. + """ + audio = AudioSegment.from_pcm(samples, sample_rate) + return cls(audio.samples, audio.sample_rate, transcript, tokens, + token_ids) + + @classmethod + def concatenate(cls, *segments): + """Concatenate an arbitrary number of speech segments together, both + audio and transcript will be concatenated. + + :param *segments: Input speech segments to be concatenated. + :type *segments: tuple of SpeechSegment + :return: Speech segment instance. + :rtype: SpeechSegment + :raises ValueError: If the number of segments is zero, or if the + sample_rate of any two segments does not match. + :raises TypeError: If any segment is not SpeechSegment instance. + """ + if len(segments) == 0: + raise ValueError("No speech segments are given to concatenate.") + sample_rate = segments[0]._sample_rate + transcripts = "" + tokens = [] + token_ids = [] + for seg in segments: + if sample_rate != seg._sample_rate: + raise ValueError("Can't concatenate segments with " + "different sample rates") + if type(seg) is not cls: + raise TypeError("Only speech segments of the same type " + "instance can be concatenated.") + transcripts += seg._transcript + if self.has_token: + tokens += seg._tokens + token_ids += seg._token_ids + samples = np.concatenate([seg.samples for seg in segments]) + return cls(samples, sample_rate, transcripts, tokens, token_ids) + + @classmethod + def slice_from_file(cls, + filepath, + transcript, + tokens=None, + token_ids=None, + start=None, + end=None): + """Loads a small section of an speech without having to load + the entire file into the memory which can be incredibly wasteful. + + :param filepath: Filepath or file object to audio file. + :type filepath: str|file + :param start: Start time in seconds. If start is negative, it wraps + around from the end. If not provided, this function + reads from the very beginning. + :type start: float + :param end: End time in seconds. If end is negative, it wraps around + from the end. If not provided, the default behvaior is + to read to the end of the file. + :type end: float + :param transcript: Transcript text for the speech. if not provided, + the defaults is an empty string. + :type transript: str + :return: SpeechSegment instance of the specified slice of the input + speech file. + :rtype: SpeechSegment + """ + audio = AudioSegment.slice_from_file(filepath, start, end) + return cls(audio.samples, audio.sample_rate, transcript, tokens, + token_ids) + + @classmethod + def make_silence(cls, duration, sample_rate): + """Creates a silent speech segment of the given duration and + sample rate, transcript will be an empty string. + + Args: + duration (float): Length of silence in seconds. + sample_rate (float): Sample rate. + + Returns: + SpeechSegment: Silence of the given duration. + """ + audio = AudioSegment.make_silence(duration, sample_rate) + return cls(audio.samples, audio.sample_rate, "") + + @property + def has_token(self): + if self._tokens and self._token_ids: + return True + return False + + @property + def transcript(self): + """Return the transcript text. + + Returns: + str: Transcript text for the speech. + """ + + return self._transcript + + @property + def tokens(self): + """Return the transcript text tokens. + + Returns: + List[str]: text tokens. + """ + return self._tokens + + @property + def token_ids(self): + """Return the transcript text token ids. + + Returns: + List[int]: text token ids. + """ + return self._token_ids diff --git a/ernie-sat/paddlespeech/s2t/frontend/utility.py b/ernie-sat/paddlespeech/s2t/frontend/utility.py new file mode 100644 index 0000000..d35785d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/frontend/utility.py @@ -0,0 +1,393 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains data helper functions.""" +import json +import math +import tarfile +from collections import namedtuple +from typing import List +from typing import Optional +from typing import Text + +import jsonlines +import numpy as np + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "load_dict", "load_cmvn", "read_manifest", "rms_to_db", "rms_to_dbfs", + "max_dbfs", "mean_dbfs", "gain_db_to_ratio", "normalize_audio", "SOS", + "EOS", "UNK", "BLANK", "MASKCTC", "SPACE", "convert_samples_to_float32", + "convert_samples_from_float32" +] + +IGNORE_ID = -1 +# `sos` and `eos` using same token +SOS = "" +EOS = SOS +UNK = "" +BLANK = "" +MASKCTC = "" +SPACE = "" + + +def load_dict(dict_path: Optional[Text], maskctc=False) -> Optional[List[Text]]: + if dict_path is None: + return None + + with open(dict_path, "r") as f: + dictionary = f.readlines() + # first token is `` + # multi line: ` 0\n` + # one line: `` + # space is relpace with + char_list = [entry[:-1].split(" ")[0] for entry in dictionary] + if BLANK not in char_list: + char_list.insert(0, BLANK) + if EOS not in char_list: + char_list.append(EOS) + # for non-autoregressive maskctc model + if maskctc and MASKCTC not in char_list: + char_list.append(MASKCTC) + return char_list + + +def read_manifest( + manifest_path, + max_input_len=float('inf'), + min_input_len=0.0, + max_output_len=float('inf'), + min_output_len=0.0, + max_output_input_ratio=float('inf'), + min_output_input_ratio=0.0, ): + """Load and parse manifest file. + + Args: + manifest_path ([type]): Manifest file to load and parse. + max_input_len ([type], optional): maximum output seq length, + in seconds for raw wav, in frame numbers for feature data. + Defaults to float('inf'). + min_input_len (float, optional): minimum input seq length, + in seconds for raw wav, in frame numbers for feature data. + Defaults to 0.0. + max_output_len (float, optional): maximum input seq length, + in modeling units. Defaults to 500.0. + min_output_len (float, optional): minimum input seq length, + in modeling units. Defaults to 0.0. + max_output_input_ratio (float, optional): + maximum output seq length/output seq length ratio. Defaults to 10.0. + min_output_input_ratio (float, optional): + minimum output seq length/output seq length ratio. Defaults to 0.05. + + Raises: + IOError: If failed to parse the manifest. + + Returns: + List[dict]: Manifest parsing results. + """ + manifest = [] + with jsonlines.open(manifest_path, 'r') as reader: + for json_data in reader: + feat_len = json_data["input"][0]["shape"][ + 0] if "input" in json_data and "shape" in json_data["input"][ + 0] else 1.0 + token_len = json_data["output"][0]["shape"][ + 0] if "output" in json_data and "shape" in json_data["output"][ + 0] else 1.0 + conditions = [ + feat_len >= min_input_len, + feat_len <= max_input_len, + token_len >= min_output_len, + token_len <= max_output_len, + token_len / feat_len >= min_output_input_ratio, + token_len / feat_len <= max_output_input_ratio, + ] + if all(conditions): + manifest.append(json_data) + return manifest + + +# Tar File read +TarLocalData = namedtuple('TarLocalData', ['tar2info', 'tar2object']) + + +def parse_tar(file): + """Parse a tar file to get a tarfile object + and a map containing tarinfoes + """ + result = {} + f = tarfile.open(file) + for tarinfo in f.getmembers(): + result[tarinfo.name] = tarinfo + return f, result + + +def subfile_from_tar(file, local_data=None): + """Get subfile object from tar. + + tar:tarpath#filename + + It will return a subfile object from tar file + and cached tar file info for next reading request. + """ + tarpath, filename = file.split(':', 1)[1].split('#', 1) + + if local_data is None: + local_data = TarLocalData(tar2info={}, tar2object={}) + + assert isinstance(local_data, TarLocalData) + + if 'tar2info' not in local_data.__dict__: + local_data.tar2info = {} + if 'tar2object' not in local_data.__dict__: + local_data.tar2object = {} + + if tarpath not in local_data.tar2info: + fobj, infos = parse_tar(tarpath) + local_data.tar2info[tarpath] = infos + local_data.tar2object[tarpath] = fobj + else: + fobj = local_data.tar2object[tarpath] + infos = local_data.tar2info[tarpath] + return fobj.extractfile(infos[filename]) + + +def rms_to_db(rms: float): + """Root Mean Square to dB. + + Args: + rms ([float]): root mean square + + Returns: + float: dB + """ + return 20.0 * math.log10(max(1e-16, rms)) + + +def rms_to_dbfs(rms: float): + """Root Mean Square to dBFS. + https://fireattack.wordpress.com/2017/02/06/replaygain-loudness-normalization-and-applications/ + Audio is mix of sine wave, so 1 amp sine wave's Full scale is 0.7071, equal to -3.0103dB. + + dB = dBFS + 3.0103 + dBFS = db - 3.0103 + e.g. 0 dB = -3.0103 dBFS + + Args: + rms ([float]): root mean square + + Returns: + float: dBFS + """ + return rms_to_db(rms) - 3.0103 + + +def max_dbfs(sample_data: np.ndarray): + """Peak dBFS based on the maximum energy sample. + + Args: + sample_data ([np.ndarray]): float array, [-1, 1]. + + Returns: + float: dBFS + """ + # Peak dBFS based on the maximum energy sample. Will prevent overdrive if used for normalization. + return rms_to_dbfs(max(abs(np.min(sample_data)), abs(np.max(sample_data)))) + + +def mean_dbfs(sample_data): + """Peak dBFS based on the RMS energy. + + Args: + sample_data ([np.ndarray]): float array, [-1, 1]. + + Returns: + float: dBFS + """ + return rms_to_dbfs( + math.sqrt(np.mean(np.square(sample_data, dtype=np.float64)))) + + +def gain_db_to_ratio(gain_db: float): + """dB to ratio + + Args: + gain_db (float): gain in dB + + Returns: + float: scale in amp + """ + return math.pow(10.0, gain_db / 20.0) + + +def normalize_audio(sample_data: np.ndarray, dbfs: float=-3.0103): + """Nomalize audio to dBFS. + + Args: + sample_data (np.ndarray): input wave samples, [-1, 1]. + dbfs (float, optional): target dBFS. Defaults to -3.0103. + + Returns: + np.ndarray: normalized wave + """ + return np.maximum( + np.minimum(sample_data * gain_db_to_ratio(dbfs - max_dbfs(sample_data)), + 1.0), -1.0) + + +def _load_json_cmvn(json_cmvn_file): + """ Load the json format cmvn stats file and calculate cmvn + + Args: + json_cmvn_file: cmvn stats file in json format + + Returns: + a numpy array of [means, vars] + """ + with open(json_cmvn_file) as f: + cmvn_stats = json.load(f) + + means = cmvn_stats['mean_stat'] + variance = cmvn_stats['var_stat'] + count = cmvn_stats['frame_num'] + for i in range(len(means)): + means[i] /= count + variance[i] = variance[i] / count - means[i] * means[i] + if variance[i] < 1.0e-20: + variance[i] = 1.0e-20 + variance[i] = 1.0 / math.sqrt(variance[i]) + cmvn = np.array([means, variance]) + return cmvn + + +def _load_kaldi_cmvn(kaldi_cmvn_file): + """ Load the kaldi format cmvn stats file and calculate cmvn + + Args: + kaldi_cmvn_file: kaldi text style global cmvn file, which + is generated by: + compute-cmvn-stats --binary=false scp:feats.scp global_cmvn + + Returns: + a numpy array of [means, vars] + """ + means = [] + variance = [] + with open(kaldi_cmvn_file, 'r') as fid: + # kaldi binary file start with '\0B' + if fid.read(2) == '\0B': + logger.error('kaldi cmvn binary file is not supported, please ' + 'recompute it by: compute-cmvn-stats --binary=false ' + ' scp:feats.scp global_cmvn') + sys.exit(1) + fid.seek(0) + arr = fid.read().split() + assert (arr[0] == '[') + assert (arr[-2] == '0') + assert (arr[-1] == ']') + feat_dim = int((len(arr) - 2 - 2) / 2) + for i in range(1, feat_dim + 1): + means.append(float(arr[i])) + count = float(arr[feat_dim + 1]) + for i in range(feat_dim + 2, 2 * feat_dim + 2): + variance.append(float(arr[i])) + + for i in range(len(means)): + means[i] /= count + variance[i] = variance[i] / count - means[i] * means[i] + if variance[i] < 1.0e-20: + variance[i] = 1.0e-20 + variance[i] = 1.0 / math.sqrt(variance[i]) + cmvn = np.array([means, variance]) + return cmvn + + +def load_cmvn(cmvn_file: str, filetype: str): + """load cmvn from file. + + Args: + cmvn_file (str): cmvn path. + filetype (str): file type, optional[npz, json, kaldi]. + + Raises: + ValueError: file type not support. + + Returns: + Tuple[np.ndarray, np.ndarray]: mean, istd + """ + assert filetype in ['npz', 'json', 'kaldi'], filetype + filetype = filetype.lower() + if filetype == "json": + cmvn = _load_json_cmvn(cmvn_file) + elif filetype == "kaldi": + cmvn = _load_kaldi_cmvn(cmvn_file) + elif filetype == "npz": + eps = 1e-14 + npzfile = np.load(cmvn_file) + mean = np.squeeze(npzfile["mean"]) + std = np.squeeze(npzfile["std"]) + istd = 1 / (std + eps) + cmvn = [mean, istd] + else: + raise ValueError(f"cmvn file type no support: {filetype}") + return cmvn[0], cmvn[1] + + +def convert_samples_to_float32(samples): + """Convert sample type to float32. + + Audio sample type is usually integer or float-point. + Integers will be scaled to [-1, 1] in float32. + + PCM16 -> PCM32 + """ + float32_samples = samples.astype('float32') + if samples.dtype in np.sctypes['int']: + bits = np.iinfo(samples.dtype).bits + float32_samples *= (1. / 2**(bits - 1)) + elif samples.dtype in np.sctypes['float']: + pass + else: + raise TypeError("Unsupported sample type: %s." % samples.dtype) + return float32_samples + + +def convert_samples_from_float32(samples, dtype): + """Convert sample type from float32 to dtype. + + Audio sample type is usually integer or float-point. For integer + type, float32 will be rescaled from [-1, 1] to the maximum range + supported by the integer type. + + PCM32 -> PCM16 + """ + dtype = np.dtype(dtype) + output_samples = samples.copy() + if dtype in np.sctypes['int']: + bits = np.iinfo(dtype).bits + output_samples *= (2**(bits - 1) / 1.) + min_val = np.iinfo(dtype).min + max_val = np.iinfo(dtype).max + output_samples[output_samples > max_val] = max_val + output_samples[output_samples < min_val] = min_val + elif samples.dtype in np.sctypes['float']: + min_val = np.finfo(dtype).min + max_val = np.finfo(dtype).max + output_samples[output_samples > max_val] = max_val + output_samples[output_samples < min_val] = min_val + else: + raise TypeError("Unsupported sample type: %s." % samples.dtype) + return output_samples.astype(dtype) diff --git a/ernie-sat/paddlespeech/s2t/io/__init__.py b/ernie-sat/paddlespeech/s2t/io/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/io/batchfy.py b/ernie-sat/paddlespeech/s2t/io/batchfy.py new file mode 100644 index 0000000..f3630f2 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/batchfy.py @@ -0,0 +1,470 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import itertools + +import numpy as np + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["make_batchset"] + +logger = Log(__name__).getlog() + + +def batchfy_by_seq( + sorted_data, + batch_size, + max_length_in, + max_length_out, + min_batch_size=1, + shortest_first=False, + ikey="input", + iaxis=0, + okey="output", + oaxis=0, ): + """Make batch set from json dictionary + + :param List[(str, Dict[str, Any])] sorted_data: dictionary loaded from data.json + :param int batch_size: batch size + :param int max_length_in: maximum length of input to decide adaptive batch size + :param int max_length_out: maximum length of output to decide adaptive batch size + :param int min_batch_size: mininum batch size (for multi-gpu) + :param bool shortest_first: Sort from batch with shortest samples + to longest if true, otherwise reverse + :param str ikey: key to access input + (for ASR ikey="input", for TTS, MT ikey="output".) + :param int iaxis: dimension to access input + (for ASR, TTS iaxis=0, for MT iaxis="1".) + :param str okey: key to access output + (for ASR, MT okey="output". for TTS okey="input".) + :param int oaxis: dimension to access output + (for ASR, TTS, MT oaxis=0, reserved for future research, -1 means all axis.) + :return: List[List[Tuple[str, dict]]] list of batches + """ + if batch_size <= 0: + raise ValueError(f"Invalid batch_size={batch_size}") + + # check #utts is more than min_batch_size + if len(sorted_data) < min_batch_size: + raise ValueError( + f"#utts({len(sorted_data)}) is less than min_batch_size({min_batch_size})." + ) + + # make list of minibatches + minibatches = [] + start = 0 + while True: + _, info = sorted_data[start] + ilen = int(info[ikey][iaxis]["shape"][0]) + olen = (int(info[okey][oaxis]["shape"][0]) if oaxis >= 0 else + max(map(lambda x: int(x["shape"][0]), info[okey]))) + factor = max(int(ilen / max_length_in), int(olen / max_length_out)) + # change batchsize depending on the input and output length + # if ilen = 1000 and max_length_in = 800 + # then b = batchsize / 2 + # and max(min_batches, .) avoids batchsize = 0 + bs = max(min_batch_size, int(batch_size / (1 + factor))) + end = min(len(sorted_data), start + bs) + minibatch = sorted_data[start:end] + if shortest_first: + minibatch.reverse() + + # check each batch is more than minimum batchsize + if len(minibatch) < min_batch_size: + mod = min_batch_size - len(minibatch) % min_batch_size + additional_minibatch = [ + sorted_data[i] for i in np.random.randint(0, start, mod) + ] + if shortest_first: + additional_minibatch.reverse() + minibatch.extend(additional_minibatch) + minibatches.append(minibatch) + + if end == len(sorted_data): + break + start = end + + # batch: List[List[Tuple[str, dict]]] + return minibatches + + +def batchfy_by_bin( + sorted_data, + batch_bins, + num_batches=0, + min_batch_size=1, + shortest_first=False, + ikey="input", + okey="output", ): + """Make variably sized batch set, which maximizes + + the number of bins up to `batch_bins`. + + :param List[(str, Dict[str, Any])] sorted_data: dictionary loaded from data.json + :param int batch_bins: Maximum frames of a batch + :param int num_batches: # number of batches to use (for debug) + :param int min_batch_size: minimum batch size (for multi-gpu) + :param int test: Return only every `test` batches + :param bool shortest_first: Sort from batch with shortest samples + to longest if true, otherwise reverse + + :param str ikey: key to access input (for ASR ikey="input", for TTS ikey="output".) + :param str okey: key to access output (for ASR okey="output". for TTS okey="input".) + + :return: List[Tuple[str, Dict[str, List[Dict[str, Any]]]] list of batches + """ + if batch_bins <= 0: + raise ValueError(f"invalid batch_bins={batch_bins}") + length = len(sorted_data) + idim = int(sorted_data[0][1][ikey][0]["shape"][1]) + odim = int(sorted_data[0][1][okey][0]["shape"][1]) + logger.info("# utts: " + str(len(sorted_data))) + minibatches = [] + start = 0 + n = 0 + while True: + # Dynamic batch size depending on size of samples + b = 0 + next_size = 0 + max_olen = 0 + while next_size < batch_bins and (start + b) < length: + ilen = int(sorted_data[start + b][1][ikey][0]["shape"][0]) * idim + olen = int(sorted_data[start + b][1][okey][0]["shape"][0]) * odim + if olen > max_olen: + max_olen = olen + next_size = (max_olen + ilen) * (b + 1) + if next_size <= batch_bins: + b += 1 + elif next_size == 0: + raise ValueError( + f"Can't fit one sample in batch_bins ({batch_bins}): " + f"Please increase the value") + end = min(length, start + max(min_batch_size, b)) + batch = sorted_data[start:end] + if shortest_first: + batch.reverse() + minibatches.append(batch) + # Check for min_batch_size and fixes the batches if needed + i = -1 + while len(minibatches[i]) < min_batch_size: + missing = min_batch_size - len(minibatches[i]) + if -i == len(minibatches): + minibatches[i + 1].extend(minibatches[i]) + minibatches = minibatches[1:] + break + else: + minibatches[i].extend(minibatches[i - 1][:missing]) + minibatches[i - 1] = minibatches[i - 1][missing:] + i -= 1 + if end == length: + break + start = end + n += 1 + if num_batches > 0: + minibatches = minibatches[:num_batches] + lengths = [len(x) for x in minibatches] + logger.info( + str(len(minibatches)) + " batches containing from " + str(min(lengths)) + + " to " + str(max(lengths)) + " samples " + "(avg " + str( + int(np.mean(lengths))) + " samples).") + return minibatches + + +def batchfy_by_frame( + sorted_data, + max_frames_in, + max_frames_out, + max_frames_inout, + num_batches=0, + min_batch_size=1, + shortest_first=False, + ikey="input", + okey="output", ): + """Make variable batch set, which maximizes the number of frames to max_batch_frame. + + :param List[(str, Dict[str, Any])] sorteddata: dictionary loaded from data.json + :param int max_frames_in: Maximum input frames of a batch + :param int max_frames_out: Maximum output frames of a batch + :param int max_frames_inout: Maximum input+output frames of a batch + :param int num_batches: # number of batches to use (for debug) + :param int min_batch_size: minimum batch size (for multi-gpu) + :param int test: Return only every `test` batches + :param bool shortest_first: Sort from batch with shortest samples + to longest if true, otherwise reverse + + :param str ikey: key to access input (for ASR ikey="input", for TTS ikey="output".) + :param str okey: key to access output (for ASR okey="output". for TTS okey="input".) + + :return: List[Tuple[str, Dict[str, List[Dict[str, Any]]]] list of batches + """ + if max_frames_in <= 0 and max_frames_out <= 0 and max_frames_inout <= 0: + raise ValueError( + "At least, one of `--batch-frames-in`, `--batch-frames-out` or " + "`--batch-frames-inout` should be > 0") + length = len(sorted_data) + minibatches = [] + start = 0 + end = 0 + while end != length: + # Dynamic batch size depending on size of samples + b = 0 + max_olen = 0 + max_ilen = 0 + while (start + b) < length: + ilen = int(sorted_data[start + b][1][ikey][0]["shape"][0]) + if ilen > max_frames_in and max_frames_in != 0: + raise ValueError( + f"Can't fit one sample in --batch-frames-in ({max_frames_in}): " + f"Please increase the value") + olen = int(sorted_data[start + b][1][okey][0]["shape"][0]) + if olen > max_frames_out and max_frames_out != 0: + raise ValueError( + f"Can't fit one sample in --batch-frames-out ({max_frames_out}): " + f"Please increase the value") + if ilen + olen > max_frames_inout and max_frames_inout != 0: + raise ValueError( + f"Can't fit one sample in --batch-frames-out ({max_frames_inout}): " + f"Please increase the value") + max_olen = max(max_olen, olen) + max_ilen = max(max_ilen, ilen) + in_ok = max_ilen * (b + 1) <= max_frames_in or max_frames_in == 0 + out_ok = max_olen * (b + 1) <= max_frames_out or max_frames_out == 0 + inout_ok = (max_ilen + max_olen) * ( + b + 1) <= max_frames_inout or max_frames_inout == 0 + if in_ok and out_ok and inout_ok: + # add more seq in the minibatch + b += 1 + else: + # no more seq in the minibatch + break + end = min(length, start + b) + batch = sorted_data[start:end] + if shortest_first: + batch.reverse() + minibatches.append(batch) + # Check for min_batch_size and fixes the batches if needed + i = -1 + while len(minibatches[i]) < min_batch_size: + missing = min_batch_size - len(minibatches[i]) + if -i == len(minibatches): + minibatches[i + 1].extend(minibatches[i]) + minibatches = minibatches[1:] + break + else: + minibatches[i].extend(minibatches[i - 1][:missing]) + minibatches[i - 1] = minibatches[i - 1][missing:] + i -= 1 + start = end + if num_batches > 0: + minibatches = minibatches[:num_batches] + lengths = [len(x) for x in minibatches] + logger.info( + str(len(minibatches)) + " batches containing from " + str(min(lengths)) + + " to " + str(max(lengths)) + " samples" + "(avg " + str( + int(np.mean(lengths))) + " samples).") + + return minibatches + + +def batchfy_shuffle(data, batch_size, min_batch_size, num_batches, + shortest_first): + import random + + logger.info("use shuffled batch.") + sorted_data = random.sample(data.items(), len(data.items())) + logger.info("# utts: " + str(len(sorted_data))) + # make list of minibatches + minibatches = [] + start = 0 + while True: + end = min(len(sorted_data), start + batch_size) + # check each batch is more than minimum batchsize + minibatch = sorted_data[start:end] + if shortest_first: + minibatch.reverse() + if len(minibatch) < min_batch_size: + mod = min_batch_size - len(minibatch) % min_batch_size + additional_minibatch = [ + sorted_data[i] for i in np.random.randint(0, start, mod) + ] + if shortest_first: + additional_minibatch.reverse() + minibatch.extend(additional_minibatch) + minibatches.append(minibatch) + if end == len(sorted_data): + break + start = end + + # for debugging + if num_batches > 0: + minibatches = minibatches[:num_batches] + logger.info("# minibatches: " + str(len(minibatches))) + return minibatches + + +BATCH_COUNT_CHOICES = ["auto", "seq", "bin", "frame"] +BATCH_SORT_KEY_CHOICES = ["input", "output", "shuffle"] + + +def make_batchset( + data, + batch_size=0, + max_length_in=float("inf"), + max_length_out=float("inf"), + num_batches=0, + min_batch_size=1, + shortest_first=False, + batch_sort_key="input", + count="auto", + batch_bins=0, + batch_frames_in=0, + batch_frames_out=0, + batch_frames_inout=0, + iaxis=0, + oaxis=0, ): + """Make batch set from json dictionary + + if utts have "category" value, + + >>> data = [{'category': 'A', 'input': ..., 'utt':'utt1'}, + ... {'category': 'B', 'input': ..., 'utt':'utt2'}, + ... {'category': 'B', 'input': ..., 'utt':'utt3'}, + ... {'category': 'A', 'input': ..., 'utt':'utt4'}] + >>> make_batchset(data, batchsize=2, ...) + [[('utt1', ...), ('utt4', ...)], [('utt2', ...), ('utt3': ...)]] + + Note that if any utts doesn't have "category", + perform as same as batchfy_by_{count} + + :param List[Dict[str, Any]] data: dictionary loaded from data.json + :param int batch_size: maximum number of sequences in a minibatch. + :param int batch_bins: maximum number of bins (frames x dim) in a minibatch. + :param int batch_frames_in: maximum number of input frames in a minibatch. + :param int batch_frames_out: maximum number of output frames in a minibatch. + :param int batch_frames_out: maximum number of input+output frames in a minibatch. + :param str count: strategy to count maximum size of batch. + For choices, see io.batchfy.BATCH_COUNT_CHOICES + + :param int max_length_in: maximum length of input to decide adaptive batch size + :param int max_length_out: maximum length of output to decide adaptive batch size + :param int num_batches: # number of batches to use (for debug) + :param int min_batch_size: minimum batch size (for multi-gpu) + :param bool shortest_first: Sort from batch with shortest samples + to longest if true, otherwise reverse + :param str batch_sort_key: how to sort data before creating minibatches + ["input", "output", "shuffle"] + :param bool swap_io: if True, use "input" as output and "output" + as input in `data` dict + :param bool mt: if True, use 0-axis of "output" as output and 1-axis of "output" + as input in `data` dict + :param int iaxis: dimension to access input + (for ASR, TTS iaxis=0, for MT iaxis="1".) + :param int oaxis: dimension to access output (for ASR, TTS, MT oaxis=0, + reserved for future research, -1 means all axis.) + :return: List[List[Tuple[str, dict]]] list of batches + """ + # check args + if count not in BATCH_COUNT_CHOICES: + raise ValueError( + f"arg 'count' ({count}) should be one of {BATCH_COUNT_CHOICES}") + if batch_sort_key not in BATCH_SORT_KEY_CHOICES: + raise ValueError(f"arg 'batch_sort_key' ({batch_sort_key}) should be " + f"one of {BATCH_SORT_KEY_CHOICES}") + + ikey = "input" + okey = "output" + batch_sort_axis = 0 # index of list + if count == "auto": + if batch_size != 0: + count = "seq" + elif batch_bins != 0: + count = "bin" + elif batch_frames_in != 0 or batch_frames_out != 0 or batch_frames_inout != 0: + count = "frame" + else: + raise ValueError( + f"cannot detect `count` manually set one of {BATCH_COUNT_CHOICES}" + ) + logger.info(f"count is auto detected as {count}") + + if count != "seq" and batch_sort_key == "shuffle": + raise ValueError( + "batch_sort_key=shuffle is only available if batch_count=seq") + + category2data = {} # Dict[str, dict] + for v in data: + k = v['utt'] + category2data.setdefault(v.get("category"), {})[k] = v + + batches_list = [] # List[List[List[Tuple[str, dict]]]] + for d in category2data.values(): + if batch_sort_key == "shuffle": + batches = batchfy_shuffle(d, batch_size, min_batch_size, + num_batches, shortest_first) + batches_list.append(batches) + continue + + # sort it by input lengths (long to short) + sorted_data = sorted( + d.items(), + key=lambda data: float(data[1][batch_sort_key][batch_sort_axis]["shape"][0]), + reverse=not shortest_first, ) + logger.info("# utts: " + str(len(sorted_data))) + + if count == "seq": + batches = batchfy_by_seq( + sorted_data, + batch_size=batch_size, + max_length_in=max_length_in, + max_length_out=max_length_out, + min_batch_size=min_batch_size, + shortest_first=shortest_first, + ikey=ikey, + iaxis=iaxis, + okey=okey, + oaxis=oaxis, ) + if count == "bin": + batches = batchfy_by_bin( + sorted_data, + batch_bins=batch_bins, + min_batch_size=min_batch_size, + shortest_first=shortest_first, + ikey=ikey, + okey=okey, ) + if count == "frame": + batches = batchfy_by_frame( + sorted_data, + max_frames_in=batch_frames_in, + max_frames_out=batch_frames_out, + max_frames_inout=batch_frames_inout, + min_batch_size=min_batch_size, + shortest_first=shortest_first, + ikey=ikey, + okey=okey, ) + batches_list.append(batches) + + if len(batches_list) == 1: + batches = batches_list[0] + else: + # Concat list. This way is faster than "sum(batch_list, [])" + batches = list(itertools.chain(*batches_list)) + + # for debugging + if num_batches > 0: + batches = batches[:num_batches] + logger.info("# minibatches: " + str(len(batches))) + + # batch: List[List[Tuple[str, dict]]] + return batches diff --git a/ernie-sat/paddlespeech/s2t/io/collator.py b/ernie-sat/paddlespeech/s2t/io/collator.py new file mode 100644 index 0000000..b99fc80 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/collator.py @@ -0,0 +1,347 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import io + +import numpy as np + +from paddlespeech.s2t.frontend.augmentor.augmentation import AugmentationPipeline +from paddlespeech.s2t.frontend.featurizer.speech_featurizer import SpeechFeaturizer +from paddlespeech.s2t.frontend.normalizer import FeatureNormalizer +from paddlespeech.s2t.frontend.speech import SpeechSegment +from paddlespeech.s2t.frontend.utility import IGNORE_ID +from paddlespeech.s2t.frontend.utility import TarLocalData +from paddlespeech.s2t.io.reader import LoadInputsAndTargets +from paddlespeech.s2t.io.utility import pad_list +from paddlespeech.s2t.utils.log import Log + +__all__ = ["SpeechCollator", "TripletSpeechCollator"] + +logger = Log(__name__).getlog() + + +def _tokenids(text, keep_transcription_text): + # for training text is token ids + tokens = text # token ids + + if keep_transcription_text: + # text is string, convert to unicode ord + assert isinstance(text, str), (type(text), text) + tokens = [ord(t) for t in text] + + tokens = np.array(tokens, dtype=np.int64) + return tokens + + +class SpeechCollatorBase(): + def __init__( + self, + aug_file, + mean_std_filepath, + vocab_filepath, + spm_model_prefix, + random_seed=0, + unit_type="char", + spectrum_type='linear', # 'linear', 'mfcc', 'fbank' + feat_dim=0, # 'mfcc', 'fbank' + delta_delta=False, # 'mfcc', 'fbank' + stride_ms=10.0, # ms + window_ms=20.0, # ms + n_fft=None, # fft points + max_freq=None, # None for samplerate/2 + target_sample_rate=16000, # target sample rate + use_dB_normalization=True, + target_dB=-20, + dither=1.0, + keep_transcription_text=True): + """SpeechCollator Collator + + Args: + unit_type(str): token unit type, e.g. char, word, spm + vocab_filepath (str): vocab file path. + mean_std_filepath (str): mean and std file path, which suffix is *.npy + spm_model_prefix (str): spm model prefix, need if `unit_type` is spm. + augmentation_config (str, optional): augmentation json str. Defaults to '{}'. + stride_ms (float, optional): stride size in ms. Defaults to 10.0. + window_ms (float, optional): window size in ms. Defaults to 20.0. + n_fft (int, optional): fft points for rfft. Defaults to None. + max_freq (int, optional): max cut freq. Defaults to None. + target_sample_rate (int, optional): target sample rate which used for training. Defaults to 16000. + spectrum_type (str, optional): 'linear', 'mfcc' or 'fbank'. Defaults to 'linear'. + feat_dim (int, optional): audio feature dim, using by 'mfcc' or 'fbank'. Defaults to None. + delta_delta (bool, optional): audio feature with delta-delta, using by 'fbank' or 'mfcc'. Defaults to False. + use_dB_normalization (bool, optional): do dB normalization. Defaults to True. + target_dB (int, optional): target dB. Defaults to -20. + random_seed (int, optional): for random generator. Defaults to 0. + keep_transcription_text (bool, optional): True, when not in training mode, will not do tokenizer; Defaults to False. + if ``keep_transcription_text`` is False, text is token ids else is raw string. + + Do augmentations + Padding audio features with zeros to make them have the same shape (or + a user-defined shape) within one batch. + """ + self.keep_transcription_text = keep_transcription_text + self.train_mode = not keep_transcription_text + + self.stride_ms = stride_ms + self.window_ms = window_ms + self.feat_dim = feat_dim + + self.loader = LoadInputsAndTargets() + + # only for tar filetype + self._local_data = TarLocalData(tar2info={}, tar2object={}) + + self.augmentation = AugmentationPipeline( + preprocess_conf=aug_file.read(), random_seed=random_seed) + + self._normalizer = FeatureNormalizer( + mean_std_filepath) if mean_std_filepath else None + + self._speech_featurizer = SpeechFeaturizer( + unit_type=unit_type, + vocab_filepath=vocab_filepath, + spm_model_prefix=spm_model_prefix, + spectrum_type=spectrum_type, + feat_dim=feat_dim, + delta_delta=delta_delta, + stride_ms=stride_ms, + window_ms=window_ms, + n_fft=n_fft, + max_freq=max_freq, + target_sample_rate=target_sample_rate, + use_dB_normalization=use_dB_normalization, + target_dB=target_dB, + dither=dither) + + self.feature_size = self._speech_featurizer.audio_feature.feature_size + self.text_feature = self._speech_featurizer.text_feature + self.vocab_dict = self.text_feature.vocab_dict + self.vocab_list = self.text_feature.vocab_list + self.vocab_size = self.text_feature.vocab_size + + def process_utterance(self, audio_file, transcript): + """Load, augment, featurize and normalize for speech data. + + :param audio_file: Filepath or file object of audio file. + :type audio_file: str | file + :param transcript: Transcription text. + :type transcript: str + :return: Tuple of audio feature tensor and data of transcription part, + where transcription part could be token ids or text. + :rtype: tuple of (2darray, list) + """ + filetype = self.loader.file_type(audio_file) + + if filetype != 'sound': + spectrum = self.loader._get_from_loader(audio_file, filetype) + feat_dim = spectrum.shape[1] + assert feat_dim == self.feat_dim, f"expect feat dim {self.feat_dim}, but got {feat_dim}" + + if self.keep_transcription_text: + transcript_part = transcript + else: + text_ids = self.text_feature.featurize(transcript) + transcript_part = text_ids + else: + # read audio + speech_segment = SpeechSegment.from_file( + audio_file, transcript, infos=self._local_data) + # audio augment + self.augmentation.transform_audio(speech_segment) + + # extract speech feature + spectrum, transcript_part = self._speech_featurizer.featurize( + speech_segment, self.keep_transcription_text) + # CMVN spectrum + if self._normalizer: + spectrum = self._normalizer.apply(spectrum) + + # spectrum augment + spectrum = self.augmentation.transform_feature(spectrum) + return spectrum, transcript_part + + def __call__(self, batch): + """batch examples + + Args: + batch (List[Dict]): batch is [dict(audio, text, ...)] + audio (np.ndarray) shape (T, D) + text (List[int] or str): shape (U,) + + Returns: + tuple(utts, xs_pad, ilens, ys_pad, olens): batched data. + utts: (B,) + xs_pad : (B, Tmax, D) + ilens: (B,) + ys_pad : (B, Umax) + olens: (B,) + """ + audios = [] + audio_lens = [] + texts = [] + text_lens = [] + utts = [] + tids = [] # tokenids + + for idx, item in enumerate(batch): + utts.append(item['utt']) + + audio = item['input'][0]['feat'] + text = item['output'][0]['text'] + audio, text = self.process_utterance(audio, text) + + audios.append(audio) # [T, D] + audio_lens.append(audio.shape[0]) + + tokens = _tokenids(text, self.keep_transcription_text) + texts.append(tokens) + text_lens.append(tokens.shape[0]) + + #[B, T, D] + xs_pad = pad_list(audios, 0.0).astype(np.float32) + ilens = np.array(audio_lens).astype(np.int64) + ys_pad = pad_list(texts, IGNORE_ID).astype(np.int64) + olens = np.array(text_lens).astype(np.int64) + return utts, xs_pad, ilens, ys_pad, olens + + +class SpeechCollator(SpeechCollatorBase): + @classmethod + def from_config(cls, config): + """Build a SpeechCollator object from a config. + + Args: + config (yacs.config.CfgNode): configs object. + + Returns: + SpeechCollator: collator object. + """ + assert 'augmentation_config' in config + assert 'keep_transcription_text' in config + assert 'mean_std_filepath' in config + assert 'vocab_filepath' in config + assert 'spectrum_type' in config + assert 'n_fft' in config + assert config + + if isinstance(config.augmentation_config, (str, bytes)): + if config.augmentation_config: + aug_file = io.open( + config.augmentation_config, mode='r', encoding='utf8') + else: + aug_file = io.StringIO(initial_value='{}', newline='') + else: + aug_file = config.augmentation_config + assert isinstance(aug_file, io.StringIO) + + speech_collator = cls( + aug_file=aug_file, + random_seed=0, + mean_std_filepath=config.mean_std_filepath, + unit_type=config.unit_type, + vocab_filepath=config.vocab_filepath, + spm_model_prefix=config.spm_model_prefix, + spectrum_type=config.spectrum_type, + feat_dim=config.feat_dim, + delta_delta=config.delta_delta, + stride_ms=config.stride_ms, + window_ms=config.window_ms, + n_fft=config.n_fft, + max_freq=config.max_freq, + target_sample_rate=config.target_sample_rate, + use_dB_normalization=config.use_dB_normalization, + target_dB=config.target_dB, + dither=config.dither, + keep_transcription_text=config.keep_transcription_text) + return speech_collator + + +class TripletSpeechCollator(SpeechCollator): + def process_utterance(self, audio_file, translation, transcript): + """Load, augment, featurize and normalize for speech data. + + :param audio_file: Filepath or file object of audio file. + :type audio_file: str | file + :param translation: translation text. + :type translation: str + :return: Tuple of audio feature tensor and data of translation part, + where translation part could be token ids or text. + :rtype: tuple of (2darray, list) + """ + spectrum, translation_part = super().process_utterance(audio_file, + translation) + transcript_part = self._speech_featurizer.text_featurize( + transcript, self.keep_transcription_text) + return spectrum, translation_part, transcript_part + + def __call__(self, batch): + """batch examples + + Args: + batch (List[Dict]): batch is [dict(audio, text, ...)] + audio (np.ndarray) shape (T, D) + text (List[int] or str): shape (U,) + + Returns: + tuple(utts, xs_pad, ilens, ys_pad, olens): batched data. + utts: (B,) + xs_pad : (B, Tmax, D) + ilens: (B,) + ys_pad : [(B, Umax), (B, Umax)] + olens: [(B,), (B,)] + """ + utts = [] + audios = [] + audio_lens = [] + translation_text = [] + translation_text_lens = [] + transcription_text = [] + transcription_text_lens = [] + + for idx, item in enumerate(batch): + utts.append(item['utt']) + + audio = item['input'][0]['feat'] + translation = item['output'][0]['text'] + transcription = item['output'][1]['text'] + + audio, translation, transcription = self.process_utterance( + audio, translation, transcription) + + audios.append(audio) # [T, D] + audio_lens.append(audio.shape[0]) + + tokens = [[], []] + for idx, text in enumerate([translation, transcription]): + tokens[idx] = _tokenids(text, self.keep_transcription_text) + + translation_text.append(tokens[0]) + translation_text_lens.append(tokens[0].shape[0]) + transcription_text.append(tokens[1]) + transcription_text_lens.append(tokens[1].shape[0]) + + xs_pad = pad_list(audios, 0.0).astype(np.float32) #[B, T, D] + ilens = np.array(audio_lens).astype(np.int64) + + padded_translation = pad_list(translation_text, + IGNORE_ID).astype(np.int64) + translation_lens = np.array(translation_text_lens).astype(np.int64) + + padded_transcription = pad_list(transcription_text, + IGNORE_ID).astype(np.int64) + transcription_lens = np.array(transcription_text_lens).astype(np.int64) + + ys_pad = (padded_translation, padded_transcription) + olens = (translation_lens, transcription_lens) + return utts, xs_pad, ilens, ys_pad, olens diff --git a/ernie-sat/paddlespeech/s2t/io/converter.py b/ernie-sat/paddlespeech/s2t/io/converter.py new file mode 100644 index 0000000..a802ac7 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/converter.py @@ -0,0 +1,107 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import numpy as np + +from paddlespeech.s2t.io.utility import pad_list +from paddlespeech.s2t.utils.log import Log + +__all__ = ["CustomConverter"] + +logger = Log(__name__).getlog() + + +class CustomConverter(): + """Custom batch converter. + + Args: + subsampling_factor (int): The subsampling factor. + dtype (np.dtype): Data type to convert. + + """ + + def __init__(self, + subsampling_factor=1, + dtype=np.float32, + load_aux_input=False, + load_aux_output=False): + """Construct a CustomConverter object.""" + self.subsampling_factor = subsampling_factor + self.ignore_id = -1 + self.dtype = dtype + self.load_aux_input = load_aux_input + self.load_aux_output = load_aux_output + + def __call__(self, batch): + """Transform a batch and send it to a device. + + Args: + batch (list): The batch to transform. + + Returns: + tuple(np.ndarray, nn.ndarray, nn.ndarray) + + """ + # batch should be located in list + assert len(batch) == 1 + data, utts = batch[0] + xs_data, ys_data = [], [] + for ud in data: + if ud[0].ndim > 1: + # speech data (input): (speech_len, feat_dim) + xs_data.append(ud) + else: + # text data (output): (text_len, ) + ys_data.append(ud) + + assert xs_data[0][ + 0] is not None, "please check Reader and Augmentation impl." + + xs_pad, ilens = [], [] + for xs in xs_data: + # perform subsampling + if self.subsampling_factor > 1: + xs = [x[::self.subsampling_factor, :] for x in xs] + + # get batch of lengths of input sequences + ilens.append(np.array([x.shape[0] for x in xs])) + + # perform padding and convert to tensor + # currently only support real number + xs_pad.append(pad_list(xs, 0).astype(self.dtype)) + + if not self.load_aux_input: + xs_pad, ilens = xs_pad[0], ilens[0] + break + + # NOTE: this is for multi-output (e.g., speech translation) + ys_pad, olens = [], [] + + for ys in ys_data: + ys_pad.append( + pad_list([ + np.array(y[0][:]) if isinstance(y, tuple) else y for y in ys + ], self.ignore_id)) + + olens.append( + np.array([ + y[0].shape[0] if isinstance(y, tuple) else y.shape[0] + for y in ys + ])) + + if not self.load_aux_output: + ys_pad, olens = ys_pad[0], olens[0] + break + + return utts, xs_pad, ilens, ys_pad, olens diff --git a/ernie-sat/paddlespeech/s2t/io/dataloader.py b/ernie-sat/paddlespeech/s2t/io/dataloader.py new file mode 100644 index 0000000..55aa13f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/dataloader.py @@ -0,0 +1,201 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any +from typing import Dict +from typing import List +from typing import Text + +import jsonlines +import numpy as np +from paddle.io import BatchSampler +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler + +from paddlespeech.s2t.io.batchfy import make_batchset +from paddlespeech.s2t.io.converter import CustomConverter +from paddlespeech.s2t.io.dataset import TransformDataset +from paddlespeech.s2t.io.reader import LoadInputsAndTargets +from paddlespeech.s2t.utils.log import Log + +__all__ = ["BatchDataLoader"] + +logger = Log(__name__).getlog() + + +def feat_dim_and_vocab_size(data_json: List[Dict[Text, Any]], + mode: Text="asr", + iaxis=0, + oaxis=0): + if mode == 'asr': + feat_dim = data_json[0]['input'][oaxis]['shape'][1] + vocab_size = data_json[0]['output'][oaxis]['shape'][1] + else: + raise ValueError(f"{mode} mode not support!") + return feat_dim, vocab_size + + +def batch_collate(x): + """de-minibatch, since user compose batch. + + Args: + x (List[Tuple]): [(utts, xs, ilens, ys, olens)] + + Returns: + Tuple: (utts, xs, ilens, ys, olens) + """ + return x[0] + + +class BatchDataLoader(): + def __init__(self, + json_file: str, + train_mode: bool, + sortagrad: int=0, + batch_size: int=0, + maxlen_in: float=float('inf'), + maxlen_out: float=float('inf'), + minibatches: int=0, + mini_batch_size: int=1, + batch_count: str='auto', + batch_bins: int=0, + batch_frames_in: int=0, + batch_frames_out: int=0, + batch_frames_inout: int=0, + preprocess_conf=None, + n_iter_processes: int=1, + subsampling_factor: int=1, + load_aux_input: bool=False, + load_aux_output: bool=False, + num_encs: int=1, + dist_sampler: bool=False, + shortest_first: bool=False): + self.json_file = json_file + self.train_mode = train_mode + self.use_sortagrad = sortagrad == -1 or sortagrad > 0 + self.batch_size = batch_size + self.maxlen_in = maxlen_in + self.maxlen_out = maxlen_out + self.batch_count = batch_count + self.batch_bins = batch_bins + self.batch_frames_in = batch_frames_in + self.batch_frames_out = batch_frames_out + self.batch_frames_inout = batch_frames_inout + self.subsampling_factor = subsampling_factor + self.num_encs = num_encs + self.preprocess_conf = preprocess_conf + self.n_iter_processes = n_iter_processes + self.load_aux_input = load_aux_input + self.load_aux_output = load_aux_output + self.dist_sampler = dist_sampler + self.shortest_first = shortest_first + + # read json data + with jsonlines.open(json_file, 'r') as reader: + self.data_json = list(reader) + + self.feat_dim, self.vocab_size = feat_dim_and_vocab_size( + self.data_json, mode='asr') + + # make minibatch list (variable length) + self.minibaches = make_batchset( + self.data_json, + batch_size, + maxlen_in, + maxlen_out, + minibatches, # for debug + min_batch_size=mini_batch_size, + shortest_first=self.shortest_first or self.use_sortagrad, + count=batch_count, + batch_bins=batch_bins, + batch_frames_in=batch_frames_in, + batch_frames_out=batch_frames_out, + batch_frames_inout=batch_frames_inout, + iaxis=0, + oaxis=0, ) + + # data reader + self.reader = LoadInputsAndTargets( + mode="asr", + load_output=True, + preprocess_conf=preprocess_conf, + preprocess_args={"train": + train_mode}, # Switch the mode of preprocessing + ) + + # Setup a converter + if num_encs == 1: + self.converter = CustomConverter( + subsampling_factor=subsampling_factor, + dtype=np.float32, + load_aux_input=load_aux_input, + load_aux_output=load_aux_output) + else: + assert NotImplementedError("not impl CustomConverterMulEnc.") + + # hack to make batchsize argument as 1 + # actual bathsize is included in a list + # default collate function converts numpy array to paddle tensor + # we used an empty collate function instead which returns list + self.dataset = TransformDataset(self.minibaches, self.converter, + self.reader) + + if self.dist_sampler: + self.batch_sampler = DistributedBatchSampler( + dataset=self.dataset, + batch_size=1, + shuffle=not self.use_sortagrad if self.train_mode else False, + drop_last=False, ) + else: + self.batch_sampler = BatchSampler( + dataset=self.dataset, + batch_size=1, + shuffle=not self.use_sortagrad if self.train_mode else False, + drop_last=False, ) + + self.dataloader = DataLoader( + dataset=self.dataset, + batch_sampler=self.batch_sampler, + collate_fn=batch_collate, + num_workers=self.n_iter_processes, ) + + def __len__(self): + return len(self.dataloader) + + def __iter__(self): + return self.dataloader.__iter__() + + def __call__(self): + return self.__iter__() + + def __repr__(self): + echo = f"<{self.__class__.__module__}.{self.__class__.__name__} object at {hex(id(self))}> " + echo += f"train_mode: {self.train_mode}, " + echo += f"sortagrad: {self.use_sortagrad}, " + echo += f"batch_size: {self.batch_size}, " + echo += f"maxlen_in: {self.maxlen_in}, " + echo += f"maxlen_out: {self.maxlen_out}, " + echo += f"batch_count: {self.batch_count}, " + echo += f"batch_bins: {self.batch_bins}, " + echo += f"batch_frames_in: {self.batch_frames_in}, " + echo += f"batch_frames_out: {self.batch_frames_out}, " + echo += f"batch_frames_inout: {self.batch_frames_inout}, " + echo += f"subsampling_factor: {self.subsampling_factor}, " + echo += f"num_encs: {self.num_encs}, " + echo += f"num_workers: {self.n_iter_processes}, " + echo += f"load_aux_input: {self.load_aux_input}, " + echo += f"load_aux_output: {self.load_aux_output}, " + echo += f"dist_sampler: {self.dist_sampler}, " + echo += f"shortest_first: {self.shortest_first}, " + echo += f"file: {self.json_file}" + return echo diff --git a/ernie-sat/paddlespeech/s2t/io/dataset.py b/ernie-sat/paddlespeech/s2t/io/dataset.py new file mode 100644 index 0000000..0e94f04 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/dataset.py @@ -0,0 +1,231 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +# Modified from wenet(https://github.com/wenet-e2e/wenet) +import jsonlines +from paddle.io import Dataset + +from paddlespeech.s2t.frontend.utility import read_manifest +from paddlespeech.s2t.utils.log import Log + +__all__ = ["ManifestDataset", "TransformDataset"] + +logger = Log(__name__).getlog() + + +class ManifestDataset(Dataset): + @classmethod + def from_config(cls, config): + """Build a ManifestDataset object from a config. + + Args: + config (yacs.config.CfgNode): configs object. + + Returns: + ManifestDataset: dataet object. + """ + assert 'manifest' in config + assert config.manifest + + dataset = cls( + manifest_path=config.manifest, + max_input_len=config.max_input_len, + min_input_len=config.min_input_len, + max_output_len=config.max_output_len, + min_output_len=config.min_output_len, + max_output_input_ratio=config.max_output_input_ratio, + min_output_input_ratio=config.min_output_input_ratio, ) + return dataset + + def __init__(self, + manifest_path, + max_input_len=float('inf'), + min_input_len=0.0, + max_output_len=float('inf'), + min_output_len=0.0, + max_output_input_ratio=float('inf'), + min_output_input_ratio=0.0): + """Manifest Dataset + + Args: + manifest_path (str): manifest josn file path + max_input_len ([type], optional): maximum output seq length, + in seconds for raw wav, in frame numbers for feature data. Defaults to float('inf'). + min_input_len (float, optional): minimum input seq length, + in seconds for raw wav, in frame numbers for feature data. Defaults to 0.0. + max_output_len (float, optional): maximum input seq length, + in modeling units. Defaults to 500.0. + min_output_len (float, optional): minimum input seq length, + in modeling units. Defaults to 0.0. + max_output_input_ratio (float, optional): maximum output seq length/output seq length ratio. + Defaults to 10.0. + min_output_input_ratio (float, optional): minimum output seq length/output seq length ratio. + Defaults to 0.05. + + """ + super().__init__() + + # read manifest + self._manifest = read_manifest( + manifest_path=manifest_path, + max_input_len=max_input_len, + min_input_len=min_input_len, + max_output_len=max_output_len, + min_output_len=min_output_len, + max_output_input_ratio=max_output_input_ratio, + min_output_input_ratio=min_output_input_ratio) + self._manifest.sort(key=lambda x: x["input"][0]["shape"][0]) + + def __len__(self): + return len(self._manifest) + + def __getitem__(self, idx): + return self._manifest[idx] + + +class TransformDataset(Dataset): + """Transform Dataset. + + Args: + data: list object from make_batchset + converter: batch function + reader: read data + """ + + def __init__(self, data, converter, reader): + """Init function.""" + super().__init__() + self.data = data + self.converter = converter + self.reader = reader + + def __len__(self): + """Len function.""" + return len(self.data) + + def __getitem__(self, idx): + """[] operator.""" + return self.converter([self.reader(self.data[idx], return_uttid=True)]) + + +class AudioDataset(Dataset): + def __init__(self, + data_file, + max_length=10240, + min_length=0, + token_max_length=200, + token_min_length=1, + batch_type='static', + batch_size=1, + max_frames_in_batch=0, + sort=True, + raw_wav=True, + stride_ms=10): + """Dataset for loading audio data. + Attributes:: + data_file: input data file + Plain text data file, each line contains following 7 fields, + which is split by '\t': + utt:utt1 + feat:tmp/data/file1.wav or feat:tmp/data/fbank.ark:30 + feat_shape: 4.95(in seconds) or feat_shape:495,80(495 is in frames) + text:i love you + token: i l o v e y o u + tokenid: int id of this token + token_shape: M,N # M is the number of token, N is vocab size + max_length: drop utterance which is greater than max_length(10ms), unit 10ms. + min_length: drop utterance which is less than min_length(10ms), unit 10ms. + token_max_length: drop utterance which is greater than token_max_length, + especially when use char unit for english modeling + token_min_length: drop utterance which is less than token_max_length + batch_type: static or dynamic, see max_frames_in_batch(dynamic) + batch_size: number of utterances in a batch, + it's for static batch size. + max_frames_in_batch: max feature frames in a batch, + when batch_type is dynamic, it's for dynamic batch size. + Then batch_size is ignored, we will keep filling the + batch until the total frames in batch up to max_frames_in_batch. + sort: whether to sort all data, so the utterance with the same + length could be filled in a same batch. + raw_wav: use raw wave or extracted featute. + if raw wave is used, dynamic waveform-level augmentation could be used + and the feature is extracted by torchaudio. + if extracted featute(e.g. by kaldi) is used, only feature-level + augmentation such as specaug could be used. + """ + assert batch_type in ['static', 'dynamic'] + # read manifest + with jsonlines.open(data_file, 'r') as reader: + data = list(reader) + if sort: + data = sorted(data, key=lambda x: x["feat_shape"][0]) + if raw_wav: + path_suffix = data[0]['feat'].split(':')[0].splitext()[-1] + assert path_suffix not in ('.ark', '.scp') + # m second to n frame + data = list( + map(lambda x: (float(x['feat_shape'][0]) * 1000 / stride_ms), + data)) + + self.input_dim = data[0]['feat_shape'][1] + self.output_dim = data[0]['token_shape'][1] + + valid_data = [] + for i in range(len(data)): + length = data[i]['feat_shape'][0] + token_length = data[i]['token_shape'][0] + # remove too lang or too short utt for both input and output + # to prevent from out of memory + if length > max_length or length < min_length: + pass + elif token_length > token_max_length or token_length < token_min_length: + pass + else: + valid_data.append(data[i]) + logger.info(f"raw dataset len: {len(data)}") + data = valid_data + num_data = len(data) + logger.info(f"dataset len after filter: {num_data}") + + self.minibatch = [] + # Dynamic batch size + if batch_type == 'dynamic': + assert (max_frames_in_batch > 0) + self.minibatch.append([]) + num_frames_in_batch = 0 + for i in range(num_data): + length = data[i]['feat_shape'][0] + num_frames_in_batch += length + if num_frames_in_batch > max_frames_in_batch: + self.minibatch.append([]) + num_frames_in_batch = length + self.minibatch[-1].append(data[i]) + # Static batch size + else: + cur = 0 + while cur < num_data: + end = min(cur + batch_size, num_data) + item = [] + for i in range(cur, end): + item.append(data[i]) + self.minibatch.append(item) + cur = end + + def __len__(self): + """number of example(batch)""" + return len(self.minibatch) + + def __getitem__(self, idx): + """batch example of idx""" + return self.minibatch[idx] diff --git a/ernie-sat/paddlespeech/s2t/io/reader.py b/ernie-sat/paddlespeech/s2t/io/reader.py new file mode 100644 index 0000000..4e136bd --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/reader.py @@ -0,0 +1,414 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from collections import OrderedDict + +import kaldiio +import numpy as np +import soundfile + +from .utility import feat_type +from paddlespeech.s2t.transform.transformation import Transformation +from paddlespeech.s2t.utils.log import Log +# from paddlespeech.s2t.frontend.augmentor.augmentation import AugmentationPipeline as Transformation + +__all__ = ["LoadInputsAndTargets"] + +logger = Log(__name__).getlog() + + +class LoadInputsAndTargets(): + """Create a mini-batch from a list of dicts + + >>> batch = [('utt1', + ... dict(input=[dict(feat='some.ark:123', + ... filetype='mat', + ... name='input1', + ... shape=[100, 80])], + ... output=[dict(tokenid='1 2 3 4', + ... name='target1', + ... shape=[4, 31])]])) + >>> l = LoadInputsAndTargets() + >>> feat, target = l(batch) + + :param: str mode: Specify the task mode, "asr" or "tts" + :param: str preprocess_conf: The path of a json file for pre-processing + :param: bool load_input: If False, not to load the input data + :param: bool load_output: If False, not to load the output data + :param: bool sort_in_input_length: Sort the mini-batch in descending order + of the input length + :param: bool use_speaker_embedding: Used for tts mode only + :param: bool use_second_target: Used for tts mode only + :param: dict preprocess_args: Set some optional arguments for preprocessing + :param: Optional[dict] preprocess_args: Used for tts mode only + """ + + def __init__( + self, + mode="asr", + preprocess_conf=None, + load_input=True, + load_output=True, + sort_in_input_length=True, + preprocess_args=None, + keep_all_data_on_mem=False, ): + self._loaders = {} + + if mode not in ["asr"]: + raise ValueError("Only asr are allowed: mode={}".format(mode)) + + if preprocess_conf: + self.preprocessing = Transformation(preprocess_conf) + logger.warning( + "[Experimental feature] Some preprocessing will be done " + "for the mini-batch creation using {}".format( + self.preprocessing)) + else: + # If conf doesn't exist, this function don't touch anything. + self.preprocessing = None + + self.mode = mode + self.load_output = load_output + self.load_input = load_input + self.sort_in_input_length = sort_in_input_length + if preprocess_args: + assert isinstance(preprocess_args, dict), type(preprocess_args) + self.preprocess_args = dict(preprocess_args) + else: + self.preprocess_args = {} + self.keep_all_data_on_mem = keep_all_data_on_mem + + def __call__(self, batch, return_uttid=False): + """Function to load inputs and targets from list of dicts + + :param List[Tuple[str, dict]] batch: list of dict which is subset of + loaded data.json + :param bool return_uttid: return utterance ID information for visualization + :return: list of input token id sequences [(L_1), (L_2), ..., (L_B)] + :return: list of input feature sequences + [(T_1, D), (T_2, D), ..., (T_B, D)] + :rtype: list of float ndarray + :return: list of target token id sequences [(L_1), (L_2), ..., (L_B)] + :rtype: list of int ndarray + + """ + x_feats_dict = OrderedDict() # OrderedDict[str, List[np.ndarray]] + y_feats_dict = OrderedDict() # OrderedDict[str, List[np.ndarray]] + uttid_list = [] # List[str] + + for uttid, info in batch: + uttid_list.append(uttid) + + if self.load_input: + # Note(kamo): This for-loop is for multiple inputs + for idx, inp in enumerate(info["input"]): + # {"input": + # [{"feat": "some/path.h5:F01_050C0101_PED_REAL", + # "filetype": "hdf5", + # "name": "input1", ...}], ...} + x = self._get_from_loader( + filepath=inp["feat"], + filetype=inp.get("filetype", "mat")) + x_feats_dict.setdefault(inp["name"], []).append(x) + + if self.load_output: + for idx, inp in enumerate(info["output"]): + if "tokenid" in inp: + # ======= Legacy format for output ======= + # {"output": [{"tokenid": "1 2 3 4"}]) + x = np.fromiter( + map(int, inp["tokenid"].split()), dtype=np.int64) + else: + # ======= New format ======= + # {"input": + # [{"feat": "some/path.h5:F01_050C0101_PED_REAL", + # "filetype": "hdf5", + # "name": "target1", ...}], ...} + x = self._get_from_loader( + filepath=inp["feat"], + filetype=inp.get("filetype", "mat")) + + y_feats_dict.setdefault(inp["name"], []).append(x) + + if self.mode == "asr": + return_batch, uttid_list = self._create_batch_asr( + x_feats_dict, y_feats_dict, uttid_list) + else: + raise NotImplementedError(self.mode) + + if self.preprocessing is not None: + # Apply pre-processing all input features + for x_name in return_batch.keys(): + if x_name.startswith("input"): + return_batch[x_name] = self.preprocessing( + return_batch[x_name], uttid_list, + **self.preprocess_args) + + if return_uttid: + return tuple(return_batch.values()), uttid_list + + # Doesn't return the names now. + return tuple(return_batch.values()) + + def _create_batch_asr(self, x_feats_dict, y_feats_dict, uttid_list): + """Create a OrderedDict for the mini-batch + + :param OrderedDict x_feats_dict: + e.g. {"input1": [ndarray, ndarray, ...], + "input2": [ndarray, ndarray, ...]} + :param OrderedDict y_feats_dict: + e.g. {"target1": [ndarray, ndarray, ...], + "target2": [ndarray, ndarray, ...]} + :param: List[str] uttid_list: + Give uttid_list to sort in the same order as the mini-batch + :return: batch, uttid_list + :rtype: Tuple[OrderedDict, List[str]] + """ + # handle single-input and multi-input (paralell) asr mode + xs = list(x_feats_dict.values()) + + if self.load_output: + ys = list(y_feats_dict.values()) + assert len(xs[0]) == len(ys[0]), (len(xs[0]), len(ys[0])) + + # get index of non-zero length samples + nonzero_idx = list( + filter(lambda i: len(ys[0][i]) > 0, range(len(ys[0])))) + for n in range(1, len(y_feats_dict)): + nonzero_idx = filter(lambda i: len(ys[n][i]) > 0, nonzero_idx) + else: + # Note(kamo): Be careful not to make nonzero_idx to a generator + nonzero_idx = list(range(len(xs[0]))) + + if self.sort_in_input_length: + # sort in input lengths based on the first input + nonzero_sorted_idx = sorted( + nonzero_idx, key=lambda i: -len(xs[0][i])) + else: + nonzero_sorted_idx = nonzero_idx + + if len(nonzero_sorted_idx) != len(xs[0]): + logger.warning( + "Target sequences include empty tokenid (batch {} -> {}).". + format(len(xs[0]), len(nonzero_sorted_idx))) + + # remove zero-length samples + xs = [[x[i] for i in nonzero_sorted_idx] for x in xs] + uttid_list = [uttid_list[i] for i in nonzero_sorted_idx] + + x_names = list(x_feats_dict.keys()) + if self.load_output: + ys = [[y[i] for i in nonzero_sorted_idx] for y in ys] + y_names = list(y_feats_dict.keys()) + + # Keeping x_name and y_name, e.g. input1, for future extension + return_batch = OrderedDict([ + * [(x_name, x) for x_name, x in zip(x_names, xs)], + * [(y_name, y) for y_name, y in zip(y_names, ys)], + ]) + else: + return_batch = OrderedDict( + [(x_name, x) for x_name, x in zip(x_names, xs)]) + return return_batch, uttid_list + + def _get_from_loader(self, filepath, filetype): + """Return ndarray + + In order to make the fds to be opened only at the first referring, + the loader are stored in self._loaders + + >>> ndarray = loader.get_from_loader( + ... 'some/path.h5:F01_050C0101_PED_REAL', filetype='hdf5') + + :param: str filepath: + :param: str filetype: + :return: + :rtype: np.ndarray + """ + if filetype == "hdf5": + # e.g. + # {"input": [{"feat": "some/path.h5:F01_050C0101_PED_REAL", + # "filetype": "hdf5", + # -> filepath = "some/path.h5", key = "F01_050C0101_PED_REAL" + filepath, key = filepath.split(":", 1) + + loader = self._loaders.get(filepath) + if loader is None: + # To avoid disk access, create loader only for the first time + loader = h5py.File(filepath, "r") + self._loaders[filepath] = loader + return loader[key][()] + elif filetype == "sound.hdf5": + # e.g. + # {"input": [{"feat": "some/path.h5:F01_050C0101_PED_REAL", + # "filetype": "sound.hdf5", + # -> filepath = "some/path.h5", key = "F01_050C0101_PED_REAL" + filepath, key = filepath.split(":", 1) + + loader = self._loaders.get(filepath) + if loader is None: + # To avoid disk access, create loader only for the first time + loader = SoundHDF5File(filepath, "r", dtype="int16") + self._loaders[filepath] = loader + array, rate = loader[key] + return array + elif filetype == "sound": + # e.g. + # {"input": [{"feat": "some/path.wav", + # "filetype": "sound"}, + # Assume PCM16 + if not self.keep_all_data_on_mem: + array, _ = soundfile.read(filepath, dtype="int16") + return array + if filepath not in self._loaders: + array, _ = soundfile.read(filepath, dtype="int16") + self._loaders[filepath] = array + return self._loaders[filepath] + elif filetype == "npz": + # e.g. + # {"input": [{"feat": "some/path.npz:F01_050C0101_PED_REAL", + # "filetype": "npz", + filepath, key = filepath.split(":", 1) + + loader = self._loaders.get(filepath) + if loader is None: + # To avoid disk access, create loader only for the first time + loader = np.load(filepath) + self._loaders[filepath] = loader + return loader[key] + elif filetype == "npy": + # e.g. + # {"input": [{"feat": "some/path.npy", + # "filetype": "npy"}, + if not self.keep_all_data_on_mem: + return np.load(filepath) + if filepath not in self._loaders: + self._loaders[filepath] = np.load(filepath) + return self._loaders[filepath] + elif filetype in ["mat", "vec"]: + # e.g. + # {"input": [{"feat": "some/path.ark:123", + # "filetype": "mat"}]}, + # In this case, "123" indicates the starting points of the matrix + # load_mat can load both matrix and vector + if not self.keep_all_data_on_mem: + return kaldiio.load_mat(filepath) + if filepath not in self._loaders: + self._loaders[filepath] = kaldiio.load_mat(filepath) + return self._loaders[filepath] + elif filetype == "scp": + # e.g. + # {"input": [{"feat": "some/path.scp:F01_050C0101_PED_REAL", + # "filetype": "scp", + filepath, key = filepath.split(":", 1) + loader = self._loaders.get(filepath) + if loader is None: + # To avoid disk access, create loader only for the first time + loader = kaldiio.load_scp(filepath) + self._loaders[filepath] = loader + return loader[key] + else: + raise NotImplementedError( + "Not supported: loader_type={}".format(filetype)) + + def file_type(self, filepath): + return feat_type(filepath) + + +class SoundHDF5File(): + """Collecting sound files to a HDF5 file + + >>> f = SoundHDF5File('a.flac.h5', mode='a') + >>> array = np.random.randint(0, 100, 100, dtype=np.int16) + >>> f['id'] = (array, 16000) + >>> array, rate = f['id'] + + + :param: str filepath: + :param: str mode: + :param: str format: The type used when saving wav. flac, nist, htk, etc. + :param: str dtype: + + """ + + def __init__(self, + filepath, + mode="r+", + format=None, + dtype="int16", + **kwargs): + self.filepath = filepath + self.mode = mode + self.dtype = dtype + + self.file = h5py.File(filepath, mode, **kwargs) + if format is None: + # filepath = a.flac.h5 -> format = flac + second_ext = os.path.splitext(os.path.splitext(filepath)[0])[1] + format = second_ext[1:] + if format.upper() not in soundfile.available_formats(): + # If not found, flac is selected + format = "flac" + + # This format affects only saving + self.format = format + + def __repr__(self): + return ''.format( + self.filepath, self.mode, self.format, self.dtype) + + def create_dataset(self, name, shape=None, data=None, **kwds): + f = io.BytesIO() + array, rate = data + soundfile.write(f, array, rate, format=self.format) + self.file.create_dataset( + name, shape=shape, data=np.void(f.getvalue()), **kwds) + + def __setitem__(self, name, data): + self.create_dataset(name, data=data) + + def __getitem__(self, key): + data = self.file[key][()] + f = io.BytesIO(data.tobytes()) + array, rate = soundfile.read(f, dtype=self.dtype) + return array, rate + + def keys(self): + return self.file.keys() + + def values(self): + for k in self.file: + yield self[k] + + def items(self): + for k in self.file: + yield k, self[k] + + def __iter__(self): + return iter(self.file) + + def __contains__(self, item): + return item in self.file + + def __len__(self, item): + return len(self.file) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.file.close() + + def close(self): + self.file.close() diff --git a/ernie-sat/paddlespeech/s2t/io/sampler.py b/ernie-sat/paddlespeech/s2t/io/sampler.py new file mode 100644 index 0000000..ac55af1 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/sampler.py @@ -0,0 +1,251 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import numpy as np +from paddle import distributed as dist +from paddle.io import BatchSampler +from paddle.io import DistributedBatchSampler + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "SortagradDistributedBatchSampler", + "SortagradBatchSampler", +] + + +def _batch_shuffle(indices, batch_size, epoch, clipped=False): + """Put similarly-sized instances into minibatches for better efficiency + and make a batch-wise shuffle. + + 1. Sort the audio clips by duration. + 2. Generate a random number `k`, k in [0, batch_size). + 3. Randomly shift `k` instances in order to create different batches + for different epochs. Create minibatches. + 4. Shuffle the minibatches. + + :param indices: indexes. List of int. + :type indices: list + :param batch_size: Batch size. This size is also used for generate + a random number for batch shuffle. + :type batch_size: int + :param clipped: Whether to clip the heading (small shift) and trailing + (incomplete batch) instances. + :type clipped: bool + :return: Batch shuffled mainifest. + :rtype: list + """ + rng = np.random.RandomState(epoch) + shift_len = rng.randint(0, batch_size - 1) + batch_indices = list(zip(* [iter(indices[shift_len:])] * batch_size)) + rng.shuffle(batch_indices) + batch_indices = [item for batch in batch_indices for item in batch] + assert clipped is False + if not clipped: + res_len = len(indices) - shift_len - len(batch_indices) + # when res_len is 0, will return whole list, len(List[-0:]) = len(List[:]) + if res_len != 0: + batch_indices.extend(indices[-res_len:]) + batch_indices.extend(indices[0:shift_len]) + assert len(indices) == len( + batch_indices + ), f"_batch_shuffle: {len(indices)} : {len(batch_indices)} : {res_len} - {shift_len}" + return batch_indices + + +class SortagradDistributedBatchSampler(DistributedBatchSampler): + def __init__(self, + dataset, + batch_size, + num_replicas=None, + rank=None, + shuffle=False, + drop_last=False, + sortagrad=False, + shuffle_method="batch_shuffle"): + """Sortagrad Sampler for multi gpus. + + Args: + dataset (paddle.io.Dataset): + batch_size (int): batch size for one gpu + num_replicas (int, optional): world size or numbers of gpus. Defaults to None. + rank (int, optional): rank id. Defaults to None. + shuffle (bool, optional): True for do shuffle, or else. Defaults to False. + drop_last (bool, optional): whether drop last batch which is less than batch size. Defaults to False. + sortagrad (bool, optional): True, do sortgrad in first epoch, then shuffle as usual; or else. Defaults to False. + shuffle_method (str, optional): shuffle method, "instance_shuffle" or "batch_shuffle". Defaults to "batch_shuffle". + """ + super().__init__(dataset, batch_size, num_replicas, rank, shuffle, + drop_last) + self._sortagrad = sortagrad + self._shuffle_method = shuffle_method + + def __iter__(self): + num_samples = len(self.dataset) + indices = np.arange(num_samples).tolist() + indices += indices[:(self.total_size - len(indices))] + assert len(indices) == self.total_size + + # sort (by duration) or batch-wise shuffle the manifest + if self.shuffle: + if self.epoch == 0 and self._sortagrad: + logger.info( + f'rank: {dist.get_rank()} dataset sortagrad! epoch {self.epoch}' + ) + else: + logger.info( + f'rank: {dist.get_rank()} dataset shuffle! epoch {self.epoch}' + ) + if self._shuffle_method == "batch_shuffle": + # using `batch_size * nrank`, or will cause instability loss and nan or inf grad, + # since diff batch examlpe length in batches case instability loss in diff rank, + # e.g. rank0 maxlength 20, rank3 maxlength 1000 + indices = _batch_shuffle( + indices, + self.batch_size * self.nranks, + self.epoch, + clipped=False) + elif self._shuffle_method == "instance_shuffle": + np.random.RandomState(self.epoch).shuffle(indices) + else: + raise ValueError("Unknown shuffle method %s." % + self._shuffle_method) + assert len( + indices + ) == self.total_size, f"batch shuffle examples error: {len(indices)} : {self.total_size}" + + # slice `self.batch_size` examples by rank id + def _get_indices_by_batch_size(indices): + subsampled_indices = [] + last_batch_size = self.total_size % (self.batch_size * self.nranks) + assert last_batch_size % self.nranks == 0 + last_local_batch_size = last_batch_size // self.nranks + + for i in range(self.local_rank * self.batch_size, + len(indices) - last_batch_size, + self.batch_size * self.nranks): + subsampled_indices.extend(indices[i:i + self.batch_size]) + + indices = indices[len(indices) - last_batch_size:] + subsampled_indices.extend( + indices[self.local_rank * last_local_batch_size:( + self.local_rank + 1) * last_local_batch_size]) + return subsampled_indices + + if self.nranks > 1: + indices = _get_indices_by_batch_size(indices) + + assert len(indices) == self.num_samples + _sample_iter = iter(indices) + + batch_indices = [] + for idx in _sample_iter: + batch_indices.append(idx) + if len(batch_indices) == self.batch_size: + logger.debug( + f"rank: {dist.get_rank()} batch index: {batch_indices} ") + yield batch_indices + batch_indices = [] + if not self.drop_last and len(batch_indices) > 0: + yield batch_indices + + def __len__(self): + num_samples = self.num_samples + num_samples += int(not self.drop_last) * (self.batch_size - 1) + return num_samples // self.batch_size + + +class SortagradBatchSampler(BatchSampler): + def __init__(self, + dataset, + batch_size, + shuffle=False, + drop_last=False, + sortagrad=False, + shuffle_method="batch_shuffle"): + """Sortagrad Sampler for one gpu. + + Args: + dataset (paddle.io.Dataset): + batch_size (int): batch size for one gpu + shuffle (bool, optional): True for do shuffle, or else. Defaults to False. + drop_last (bool, optional): whether drop last batch which is less than batch size. Defaults to False. + sortagrad (bool, optional): True, do sortgrad in first epoch, then shuffle as usual; or else. Defaults to False. + shuffle_method (str, optional): shuffle method, "instance_shuffle" or "batch_shuffle". Defaults to "batch_shuffle". + """ + self.dataset = dataset + + assert isinstance(batch_size, int) and batch_size > 0, \ + "batch_size should be a positive integer" + self.batch_size = batch_size + assert isinstance(shuffle, bool), \ + "shuffle should be a boolean value" + self.shuffle = shuffle + assert isinstance(drop_last, bool), \ + "drop_last should be a boolean number" + + self.drop_last = drop_last + self.epoch = 0 + self.num_samples = int(math.ceil(len(self.dataset) * 1.0)) + self.total_size = self.num_samples + self._sortagrad = sortagrad + self._shuffle_method = shuffle_method + + def __iter__(self): + num_samples = len(self.dataset) + indices = np.arange(num_samples).tolist() + indices += indices[:(self.total_size - len(indices))] + assert len(indices) == self.total_size + + # sort (by duration) or batch-wise shuffle the manifest + if self.shuffle: + if self.epoch == 0 and self._sortagrad: + logger.info(f'dataset sortagrad! epoch {self.epoch}') + else: + logger.info(f'dataset shuffle! epoch {self.epoch}') + if self._shuffle_method == "batch_shuffle": + indices = _batch_shuffle( + indices, self.batch_size, self.epoch, clipped=False) + elif self._shuffle_method == "instance_shuffle": + np.random.RandomState(self.epoch).shuffle(indices) + else: + raise ValueError("Unknown shuffle method %s." % + self._shuffle_method) + assert len( + indices + ) == self.total_size, f"batch shuffle examples error: {len(indices)} : {self.total_size}" + + assert len(indices) == self.num_samples + _sample_iter = iter(indices) + + batch_indices = [] + for idx in _sample_iter: + batch_indices.append(idx) + if len(batch_indices) == self.batch_size: + logger.debug( + f"rank: {dist.get_rank()} batch index: {batch_indices} ") + yield batch_indices + batch_indices = [] + if not self.drop_last and len(batch_indices) > 0: + yield batch_indices + + self.epoch += 1 + + def __len__(self): + num_samples = self.num_samples + num_samples += int(not self.drop_last) * (self.batch_size - 1) + return num_samples // self.batch_size diff --git a/ernie-sat/paddlespeech/s2t/io/utility.py b/ernie-sat/paddlespeech/s2t/io/utility.py new file mode 100644 index 0000000..c08b553 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/io/utility.py @@ -0,0 +1,109 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from io import BytesIO +from typing import List + +import numpy as np + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["pad_list", "pad_sequence", "feat_type"] + +logger = Log(__name__).getlog() + + +def pad_list(sequences: List[np.ndarray], + padding_value: float=0.0) -> np.ndarray: + return pad_sequence(sequences, True, padding_value) + + +def pad_sequence(sequences: List[np.ndarray], + batch_first: bool=True, + padding_value: float=0.0) -> np.ndarray: + r"""Pad a list of variable length Tensors with ``padding_value`` + + ``pad_sequence`` stacks a list of Tensors along a new dimension, + and pads them to equal length. For example, if the input is list of + sequences with size ``L x *`` and if batch_first is False, and ``T x B x *`` + otherwise. + + `B` is batch size. It is equal to the number of elements in ``sequences``. + `T` is length of the longest sequence. + `L` is length of the sequence. + `*` is any number of trailing dimensions, including none. + + Example: + >>> a = np.ones([25, 300]) + >>> b = np.ones([22, 300]) + >>> c = np.ones([15, 300]) + >>> pad_sequence([a, b, c]).shape + [25, 3, 300] + + Note: + This function returns a np.ndarray of size ``T x B x *`` or ``B x T x *`` + where `T` is the length of the longest sequence. This function assumes + trailing dimensions and type of all the Tensors in sequences are same. + + Args: + sequences (list[np.ndarray]): list of variable length sequences. + batch_first (bool, optional): output will be in ``B x T x *`` if True, or in + ``T x B x *`` otherwise + padding_value (float, optional): value for padded elements. Default: 0. + + Returns: + np.ndarray of size ``T x B x *`` if :attr:`batch_first` is ``False``. + np.ndarray of size ``B x T x *`` otherwise + """ + + # assuming trailing dimensions and type of all the Tensors + # in sequences are same and fetching those from sequences[0] + max_size = sequences[0].shape + trailing_dims = max_size[1:] + max_len = max([s.shape[0] for s in sequences]) + if batch_first: + out_dims = (len(sequences), max_len) + trailing_dims + else: + out_dims = (max_len, len(sequences)) + trailing_dims + + out_tensor = np.full(out_dims, padding_value, dtype=sequences[0].dtype) + for i, tensor in enumerate(sequences): + length = tensor.shape[0] + # use index notation to prevent duplicate references to the tensor + if batch_first: + out_tensor[i, :length, ...] = tensor + else: + out_tensor[:length, i, ...] = tensor + + return out_tensor + + +def feat_type(filepath): + # deal with Byteio type for paddlespeech server + if isinstance(filepath, BytesIO): + return 'sound' + + suffix = filepath.split(":")[0].split('.')[-1].lower() + if suffix == 'ark': + return 'mat' + elif suffix == 'scp': + return 'scp' + elif suffix == 'npy': + return 'npy' + elif suffix == 'npz': + return 'npz' + elif suffix in ['wav', 'flac']: + # PCM16 + return 'sound' + else: + raise ValueError(f"Not support filetype: {suffix}") diff --git a/ernie-sat/paddlespeech/s2t/models/__init__.py b/ernie-sat/paddlespeech/s2t/models/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/models/asr_interface.py b/ernie-sat/paddlespeech/s2t/models/asr_interface.py new file mode 100644 index 0000000..8c2db27 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/asr_interface.py @@ -0,0 +1,162 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""ASR Interface module.""" +import argparse + +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + + +class ASRInterface: + """ASR Interface model implementation.""" + + @staticmethod + def add_arguments(parser): + """Add arguments to parser.""" + return parser + + @classmethod + def build(cls, idim: int, odim: int, **kwargs): + """Initialize this class with python-level args. + + Args: + idim (int): The number of an input feature dim. + odim (int): The number of output vocab. + + Returns: + ASRinterface: A new instance of ASRInterface. + + """ + args = argparse.Namespace(**kwargs) + return cls(idim, odim, args) + + def forward(self, xs, ilens, ys, olens): + """Compute loss for training. + + :param xs: batch of padded source sequences paddle.Tensor (B, Tmax, idim) + :param ilens: batch of lengths of source sequences (B), paddle.Tensor + :param ys: batch of padded target sequences paddle.Tensor (B, Lmax) + :param olens: batch of lengths of target sequences (B), paddle.Tensor + :return: loss value + :rtype: paddle.Tensor + """ + raise NotImplementedError("forward method is not implemented") + + def recognize(self, x, recog_args, char_list=None, rnnlm=None): + """Recognize x for evaluation. + + :param ndarray x: input acouctic feature (B, T, D) or (T, D) + :param namespace recog_args: argment namespace contraining options + :param list char_list: list of characters + :param paddle.nn.Layer rnnlm: language model module + :return: N-best decoding results + :rtype: list + """ + raise NotImplementedError("recognize method is not implemented") + + def recognize_batch(self, x, recog_args, char_list=None, rnnlm=None): + """Beam search implementation for batch. + + :param paddle.Tensor x: encoder hidden state sequences (B, Tmax, Henc) + :param namespace recog_args: argument namespace containing options + :param list char_list: list of characters + :param paddle.nn.Module rnnlm: language model module + :return: N-best decoding results + :rtype: list + """ + raise NotImplementedError("Batch decoding is not supported yet.") + + def calculate_all_attentions(self, xs, ilens, ys): + """Calculate attention. + + :param list xs: list of padded input sequences [(T1, idim), (T2, idim), ...] + :param ndarray ilens: batch of lengths of input sequences (B) + :param list ys: list of character id sequence tensor [(L1), (L2), (L3), ...] + :return: attention weights (B, Lmax, Tmax) + :rtype: float ndarray + """ + raise NotImplementedError( + "calculate_all_attentions method is not implemented") + + def calculate_all_ctc_probs(self, xs, ilens, ys): + """Calculate CTC probability. + + :param list xs_pad: list of padded input sequences [(T1, idim), (T2, idim), ...] + :param ndarray ilens: batch of lengths of input sequences (B) + :param list ys: list of character id sequence tensor [(L1), (L2), (L3), ...] + :return: CTC probabilities (B, Tmax, vocab) + :rtype: float ndarray + """ + raise NotImplementedError( + "calculate_all_ctc_probs method is not implemented") + + @property + def attention_plot_class(self): + """Get attention plot class.""" + from paddlespeech.s2t.training.extensions.plot import PlotAttentionReport + + return PlotAttentionReport + + @property + def ctc_plot_class(self): + """Get CTC plot class.""" + from paddlespeech.s2t.training.extensions.plot import PlotCTCReport + + return PlotCTCReport + + def get_total_subsampling_factor(self): + """Get total subsampling factor.""" + raise NotImplementedError( + "get_total_subsampling_factor method is not implemented") + + def encode(self, feat): + """Encode feature in `beam_search` (optional). + + Args: + x (numpy.ndarray): input feature (T, D) + Returns: + paddle.Tensor: encoded feature (T, D) + """ + raise NotImplementedError("encode method is not implemented") + + def scorers(self): + """Get scorers for `beam_search` (optional). + + Returns: + dict[str, ScorerInterface]: dict of `ScorerInterface` objects + + """ + raise NotImplementedError("decoders method is not implemented") + + +predefined_asr = { + "transformer": "paddlespeech.s2t.models.u2:U2Model", + "conformer": "paddlespeech.s2t.models.u2:U2Model", +} + + +def dynamic_import_asr(module): + """Import ASR models dynamically. + + Args: + module (str): asr name. e.g., transformer, conformer + + Returns: + type: ASR class + + """ + model_class = dynamic_import(module, predefined_asr) + assert issubclass(model_class, + ASRInterface), f"{module} does not implement ASRInterface" + return model_class diff --git a/ernie-sat/paddlespeech/s2t/models/ds2/__init__.py b/ernie-sat/paddlespeech/s2t/models/ds2/__init__.py new file mode 100644 index 0000000..b322206 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .deepspeech2 import DeepSpeech2InferModel +from .deepspeech2 import DeepSpeech2Model +from paddlespeech.s2t.utils import dynamic_pip_install + +try: + import paddlespeech_ctcdecoders +except ImportError: + try: + package_name = 'paddlespeech_ctcdecoders' + dynamic_pip_install.install(package_name) + except Exception: + raise RuntimeError( + "Can not install package paddlespeech_ctcdecoders on your system. \ + The DeepSpeech2 model is not supported for your system") + +__all__ = ['DeepSpeech2Model', 'DeepSpeech2InferModel'] diff --git a/ernie-sat/paddlespeech/s2t/models/ds2/conv.py b/ernie-sat/paddlespeech/s2t/models/ds2/conv.py new file mode 100644 index 0000000..4e766e7 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2/conv.py @@ -0,0 +1,171 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.s2t.modules.activation import brelu +from paddlespeech.s2t.modules.mask import make_non_pad_mask +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['ConvStack', "conv_output_size"] + + +def conv_output_size(I, F, P, S): + # https://stanford.edu/~shervine/teaching/cs-230/cheatsheet-convolutional-neural-networks#hyperparameters + # Output size after Conv: + # By noting I the length of the input volume size, + # F the length of the filter, + # P the amount of zero padding, + # S the stride, + # then the output size O of the feature map along that dimension is given by: + # O = (I - F + Pstart + Pend) // S + 1 + # When Pstart == Pend == P, we can replace Pstart + Pend by 2P. + # When Pstart == Pend == 0 + # O = (I - F - S) // S + # https://iq.opengenus.org/output-size-of-convolution/ + # Output height = (Input height + padding height top + padding height bottom - kernel height) / (stride height) + 1 + # Output width = (Output width + padding width right + padding width left - kernel width) / (stride width) + 1 + return (I - F + 2 * P - S) // S + + +# receptive field calculator +# https://fomoro.com/research/article/receptive-field-calculator +# https://stanford.edu/~shervine/teaching/cs-230/cheatsheet-convolutional-neural-networks#hyperparameters +# https://distill.pub/2019/computing-receptive-fields/ +# Rl-1 = Sl * Rl + (Kl - Sl) + + +class ConvBn(nn.Layer): + """Convolution layer with batch normalization. + + :param kernel_size: The x dimension of a filter kernel. Or input a tuple for + two image dimension. + :type kernel_size: int|tuple|list + :param num_channels_in: Number of input channels. + :type num_channels_in: int + :param num_channels_out: Number of output channels. + :type num_channels_out: int + :param stride: The x dimension of the stride. Or input a tuple for two + image dimension. + :type stride: int|tuple|list + :param padding: The x dimension of the padding. Or input a tuple for two + image dimension. + :type padding: int|tuple|list + :param act: Activation type, relu|brelu + :type act: string + :return: Batch norm layer after convolution layer. + :rtype: Variable + + """ + + def __init__(self, num_channels_in, num_channels_out, kernel_size, stride, + padding, act): + + super().__init__() + assert len(kernel_size) == 2 + assert len(stride) == 2 + assert len(padding) == 2 + self.kernel_size = kernel_size + self.stride = stride + self.padding = padding + + self.conv = nn.Conv2D( + num_channels_in, + num_channels_out, + kernel_size=kernel_size, + stride=stride, + padding=padding, + weight_attr=None, + bias_attr=False, + data_format='NCHW') + + self.bn = nn.BatchNorm2D( + num_channels_out, + weight_attr=None, + bias_attr=None, + data_format='NCHW') + self.act = F.relu if act == 'relu' else brelu + + def forward(self, x, x_len): + """ + x(Tensor): audio, shape [B, C, D, T] + """ + x = self.conv(x) + x = self.bn(x) + x = self.act(x) + + x_len = (x_len - self.kernel_size[1] + 2 * self.padding[1] + ) // self.stride[1] + 1 + + # reset padding part to 0 + masks = make_non_pad_mask(x_len) #[B, T] + masks = masks.unsqueeze(1).unsqueeze(1) # [B, 1, 1, T] + # TODO(Hui Zhang): not support bool multiply + # masks = masks.type_as(x) + masks = masks.astype(x.dtype) + x = x.multiply(masks) + return x, x_len + + +class ConvStack(nn.Layer): + """Convolution group with stacked convolution layers. + + :param feat_size: audio feature dim. + :type feat_size: int + :param num_stacks: Number of stacked convolution layers. + :type num_stacks: int + """ + + def __init__(self, feat_size, num_stacks): + super().__init__() + self.feat_size = feat_size # D + self.num_stacks = num_stacks + + self.conv_in = ConvBn( + num_channels_in=1, + num_channels_out=32, + kernel_size=(41, 11), #[D, T] + stride=(2, 3), + padding=(20, 5), + act='brelu') + + out_channel = 32 + convs = [ + ConvBn( + num_channels_in=32, + num_channels_out=out_channel, + kernel_size=(21, 11), + stride=(2, 1), + padding=(10, 5), + act='brelu') for i in range(num_stacks - 1) + ] + self.conv_stack = nn.LayerList(convs) + + # conv output feat_dim + output_height = (feat_size - 1) // 2 + 1 + for i in range(self.num_stacks - 1): + output_height = (output_height - 1) // 2 + 1 + self.output_height = out_channel * output_height + + def forward(self, x, x_len): + """ + x: shape [B, C, D, T] + x_len : shape [B] + """ + x, x_len = self.conv_in(x, x_len) + for i, conv in enumerate(self.conv_stack): + x, x_len = conv(x, x_len) + return x, x_len diff --git a/ernie-sat/paddlespeech/s2t/models/ds2/deepspeech2.py b/ernie-sat/paddlespeech/s2t/models/ds2/deepspeech2.py new file mode 100644 index 0000000..9c6b66c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2/deepspeech2.py @@ -0,0 +1,267 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Deepspeech2 ASR Model""" +import paddle +from paddle import nn + +from paddlespeech.s2t.models.ds2.conv import ConvStack +from paddlespeech.s2t.models.ds2.rnn import RNNStack +from paddlespeech.s2t.modules.ctc import CTCDecoder +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.checkpoint import Checkpoint +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['DeepSpeech2Model', 'DeepSpeech2InferModel'] + + +class CRNNEncoder(nn.Layer): + def __init__(self, + feat_size, + dict_size, + num_conv_layers=2, + num_rnn_layers=3, + rnn_size=1024, + use_gru=False, + share_rnn_weights=True): + super().__init__() + self.rnn_size = rnn_size + self.feat_size = feat_size # 161 for linear + self.dict_size = dict_size + + self.conv = ConvStack(feat_size, num_conv_layers) + + i_size = self.conv.output_height # H after conv stack + self.rnn = RNNStack( + i_size=i_size, + h_size=rnn_size, + num_stacks=num_rnn_layers, + use_gru=use_gru, + share_rnn_weights=share_rnn_weights) + + @property + def output_size(self): + return self.rnn_size * 2 + + def forward(self, audio, audio_len): + """Compute Encoder outputs + + Args: + audio (Tensor): [B, Tmax, D] + text (Tensor): [B, Umax] + audio_len (Tensor): [B] + text_len (Tensor): [B] + Returns: + x (Tensor): encoder outputs, [B, T, D] + x_lens (Tensor): encoder length, [B] + """ + # [B, T, D] -> [B, D, T] + audio = audio.transpose([0, 2, 1]) + # [B, D, T] -> [B, C=1, D, T] + x = audio.unsqueeze(1) + x_lens = audio_len + + # convolution group + x, x_lens = self.conv(x, x_lens) + + # convert data from convolution feature map to sequence of vectors + #B, C, D, T = paddle.shape(x) # not work under jit + x = x.transpose([0, 3, 1, 2]) #[B, T, C, D] + #x = x.reshape([B, T, C * D]) #[B, T, C*D] # not work under jit + x = x.reshape([0, 0, -1]) #[B, T, C*D] + + # remove padding part + x, x_lens = self.rnn(x, x_lens) #[B, T, D] + return x, x_lens + + +class DeepSpeech2Model(nn.Layer): + """The DeepSpeech2 network structure. + + :param audio_data: Audio spectrogram data layer. + :type audio_data: Variable + :param text_data: Transcription text data layer. + :type text_data: Variable + :param audio_len: Valid sequence length data layer. + :type audio_len: Variable + :param masks: Masks data layer to reset padding. + :type masks: Variable + :param dict_size: Dictionary size for tokenized transcription. + :type dict_size: int + :param num_conv_layers: Number of stacking convolution layers. + :type num_conv_layers: int + :param num_rnn_layers: Number of stacking RNN layers. + :type num_rnn_layers: int + :param rnn_size: RNN layer size (dimension of RNN cells). + :type rnn_size: int + :param use_gru: Use gru if set True. Use simple rnn if set False. + :type use_gru: bool + :param share_rnn_weights: Whether to share input-hidden weights between + forward and backward direction RNNs. + It is only available when use_gru=False. + :type share_weights: bool + :return: A tuple of an output unnormalized log probability layer ( + before softmax) and a ctc cost layer. + :rtype: tuple of LayerOutput + """ + + def __init__(self, + feat_size, + dict_size, + num_conv_layers=2, + num_rnn_layers=3, + rnn_size=1024, + use_gru=False, + share_rnn_weights=True, + blank_id=0, + ctc_grad_norm_type=None): + super().__init__() + self.encoder = CRNNEncoder( + feat_size=feat_size, + dict_size=dict_size, + num_conv_layers=num_conv_layers, + num_rnn_layers=num_rnn_layers, + rnn_size=rnn_size, + use_gru=use_gru, + share_rnn_weights=share_rnn_weights) + assert (self.encoder.output_size == rnn_size * 2) + + self.decoder = CTCDecoder( + odim=dict_size, # is in vocab + enc_n_units=self.encoder.output_size, + blank_id=blank_id, + dropout_rate=0.0, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=ctc_grad_norm_type) + + def forward(self, audio, audio_len, text, text_len): + """Compute Model loss + + Args: + audio (Tensors): [B, T, D] + audio_len (Tensor): [B] + text (Tensor): [B, U] + text_len (Tensor): [B] + + Returns: + loss (Tensor): [1] + """ + eouts, eouts_len = self.encoder(audio, audio_len) + loss = self.decoder(eouts, eouts_len, text, text_len) + return loss + + @paddle.no_grad() + def decode(self, audio, audio_len): + # decoders only accept string encoded in utf-8 + + # Make sure the decoder has been initialized + eouts, eouts_len = self.encoder(audio, audio_len) + probs = self.decoder.softmax(eouts) + batch_size = probs.shape[0] + self.decoder.reset_decoder(batch_size=batch_size) + self.decoder.next(probs, eouts_len) + trans_best, trans_beam = self.decoder.decode() + + return trans_best + + @classmethod + def from_pretrained(cls, dataloader, config, checkpoint_path): + """Build a DeepSpeech2Model model from a pretrained model. + Parameters + ---------- + dataloader: paddle.io.DataLoader + + config: yacs.config.CfgNode + model configs + + checkpoint_path: Path or str + the path of pretrained model checkpoint, without extension name + + Returns + ------- + DeepSpeech2Model + The model built from pretrained result. + """ + model = cls( + feat_size=dataloader.collate_fn.feature_size, + dict_size=dataloader.collate_fn.vocab_size, + num_conv_layers=config.num_conv_layers, + num_rnn_layers=config.num_rnn_layers, + rnn_size=config.rnn_layer_size, + use_gru=config.use_gru, + share_rnn_weights=config.share_rnn_weights, + blank_id=config.blank_id, + ctc_grad_norm_type=config.get('ctc_grad_norm_type', None), ) + infos = Checkpoint().load_parameters( + model, checkpoint_path=checkpoint_path) + logger.info(f"checkpoint info: {infos}") + layer_tools.summary(model) + return model + + @classmethod + def from_config(cls, config): + """Build a DeepSpeec2Model from config + Parameters + + config: yacs.config.CfgNode + config + Returns + ------- + DeepSpeech2Model + The model built from config. + """ + model = cls( + feat_size=config.input_dim, + dict_size=config.output_dim, + num_conv_layers=config.num_conv_layers, + num_rnn_layers=config.num_rnn_layers, + rnn_size=config.rnn_layer_size, + use_gru=config.use_gru, + share_rnn_weights=config.share_rnn_weights, + blank_id=config.blank_id, + ctc_grad_norm_type=config.get('ctc_grad_norm_type', None), ) + return model + + +class DeepSpeech2InferModel(DeepSpeech2Model): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, audio, audio_len): + """export model function + + Args: + audio (Tensor): [B, T, D] + audio_len (Tensor): [B] + + Returns: + probs: probs after softmax + """ + eouts, eouts_len = self.encoder(audio, audio_len) + probs = self.decoder.softmax(eouts) + return probs, eouts_len + + def export(self): + static_model = paddle.jit.to_static( + self, + input_spec=[ + paddle.static.InputSpec( + shape=[None, None, self.encoder.feat_size], + dtype='float32'), # audio, [B,T,D] + paddle.static.InputSpec(shape=[None], + dtype='int64'), # audio_length, [B] + ]) + return static_model diff --git a/ernie-sat/paddlespeech/s2t/models/ds2/rnn.py b/ernie-sat/paddlespeech/s2t/models/ds2/rnn.py new file mode 100644 index 0000000..f655b2d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2/rnn.py @@ -0,0 +1,315 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import paddle +from paddle import nn +from paddle.nn import functional as F +from paddle.nn import initializer as I + +from paddlespeech.s2t.modules.activation import brelu +from paddlespeech.s2t.modules.mask import make_non_pad_mask +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['RNNStack'] + + +class RNNCell(nn.RNNCellBase): + r""" + Elman RNN (SimpleRNN) cell. Given the inputs and previous states, it + computes the outputs and updates states. + The formula used is as follows: + .. math:: + h_{t} & = act(x_{t} + b_{ih} + W_{hh}h_{t-1} + b_{hh}) + y_{t} & = h_{t} + + where :math:`act` is for :attr:`activation`. + """ + + def __init__(self, + hidden_size: int, + activation="tanh", + weight_ih_attr=None, + weight_hh_attr=None, + bias_ih_attr=None, + bias_hh_attr=None, + name=None): + super().__init__() + std = 1.0 / math.sqrt(hidden_size) + self.weight_hh = self.create_parameter( + (hidden_size, hidden_size), + weight_hh_attr, + default_initializer=I.Uniform(-std, std)) + self.bias_ih = None + self.bias_hh = self.create_parameter( + (hidden_size, ), + bias_hh_attr, + is_bias=True, + default_initializer=I.Uniform(-std, std)) + + self.hidden_size = hidden_size + if activation not in ["tanh", "relu", "brelu"]: + raise ValueError( + "activation for SimpleRNNCell should be tanh or relu, " + "but get {}".format(activation)) + self.activation = activation + self._activation_fn = paddle.tanh \ + if activation == "tanh" \ + else F.relu + if activation == 'brelu': + self._activation_fn = brelu + + def forward(self, inputs, states=None): + if states is None: + states = self.get_initial_states(inputs, self.state_shape) + pre_h = states + i2h = inputs + if self.bias_ih is not None: + i2h += self.bias_ih + h2h = paddle.matmul(pre_h, self.weight_hh, transpose_y=True) + if self.bias_hh is not None: + h2h += self.bias_hh + h = self._activation_fn(i2h + h2h) + return h, h + + @property + def state_shape(self): + return (self.hidden_size, ) + + +class GRUCell(nn.RNNCellBase): + r""" + Gated Recurrent Unit (GRU) RNN cell. Given the inputs and previous states, + it computes the outputs and updates states. + The formula for GRU used is as follows: + .. math:: + r_{t} & = \sigma(W_{ir}x_{t} + b_{ir} + W_{hr}h_{t-1} + b_{hr}) + z_{t} & = \sigma(W_{iz}x_{t} + b_{iz} + W_{hz}h_{t-1} + b_{hz}) + \widetilde{h}_{t} & = \tanh(W_{ic}x_{t} + b_{ic} + r_{t} * (W_{hc}h_{t-1} + b_{hc})) + h_{t} & = z_{t} * h_{t-1} + (1 - z_{t}) * \widetilde{h}_{t} + y_{t} & = h_{t} + + where :math:`\sigma` is the sigmoid fucntion, and * is the elemetwise + multiplication operator. + """ + + def __init__(self, + input_size: int, + hidden_size: int, + weight_ih_attr=None, + weight_hh_attr=None, + bias_ih_attr=None, + bias_hh_attr=None, + name=None): + super().__init__() + std = 1.0 / math.sqrt(hidden_size) + self.weight_hh = self.create_parameter( + (3 * hidden_size, hidden_size), + weight_hh_attr, + default_initializer=I.Uniform(-std, std)) + self.bias_ih = None + self.bias_hh = self.create_parameter( + (3 * hidden_size, ), + bias_hh_attr, + is_bias=True, + default_initializer=I.Uniform(-std, std)) + + self.hidden_size = hidden_size + self.input_size = input_size + self._gate_activation = F.sigmoid + self._activation = paddle.tanh + + def forward(self, inputs, states=None): + if states is None: + states = self.get_initial_states(inputs, self.state_shape) + + pre_hidden = states + x_gates = inputs + if self.bias_ih is not None: + x_gates = x_gates + self.bias_ih + h_gates = paddle.matmul(pre_hidden, self.weight_hh, transpose_y=True) + if self.bias_hh is not None: + h_gates = h_gates + self.bias_hh + + x_r, x_z, x_c = paddle.split(x_gates, num_or_sections=3, axis=1) + h_r, h_z, h_c = paddle.split(h_gates, num_or_sections=3, axis=1) + + r = self._gate_activation(x_r + h_r) + z = self._gate_activation(x_z + h_z) + c = self._activation(x_c + r * h_c) # apply reset gate after mm + h = (pre_hidden - c) * z + c + # https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/fluid/layers/dynamic_gru_cn.html#dynamic-gru + + return h, h + + @property + def state_shape(self): + r""" + The `state_shape` of GRUCell is a shape `[hidden_size]` (-1 for batch + size would be automatically inserted into shape). The shape corresponds + to the shape of :math:`h_{t-1}`. + """ + return (self.hidden_size, ) + + +class BiRNNWithBN(nn.Layer): + """Bidirectonal simple rnn layer with sequence-wise batch normalization. + The batch normalization is only performed on input-state weights. + + :param size: Dimension of RNN cells. + :type size: int + :param share_weights: Whether to share input-hidden weights between + forward and backward directional RNNs. + :type share_weights: bool + :return: Bidirectional simple rnn layer. + :rtype: Variable + """ + + def __init__(self, i_size: int, h_size: int, share_weights: bool): + super().__init__() + self.share_weights = share_weights + if self.share_weights: + #input-hidden weights shared between bi-directional rnn. + self.fw_fc = nn.Linear(i_size, h_size, bias_attr=False) + # batch norm is only performed on input-state projection + self.fw_bn = nn.BatchNorm1D( + h_size, bias_attr=None, data_format='NLC') + self.bw_fc = self.fw_fc + self.bw_bn = self.fw_bn + else: + self.fw_fc = nn.Linear(i_size, h_size, bias_attr=False) + self.fw_bn = nn.BatchNorm1D( + h_size, bias_attr=None, data_format='NLC') + self.bw_fc = nn.Linear(i_size, h_size, bias_attr=False) + self.bw_bn = nn.BatchNorm1D( + h_size, bias_attr=None, data_format='NLC') + + self.fw_cell = RNNCell(hidden_size=h_size, activation='brelu') + self.bw_cell = RNNCell(hidden_size=h_size, activation='brelu') + self.fw_rnn = nn.RNN( + self.fw_cell, is_reverse=False, time_major=False) #[B, T, D] + self.bw_rnn = nn.RNN( + self.fw_cell, is_reverse=True, time_major=False) #[B, T, D] + + def forward(self, x: paddle.Tensor, x_len: paddle.Tensor): + # x, shape [B, T, D] + fw_x = self.fw_bn(self.fw_fc(x)) + bw_x = self.bw_bn(self.bw_fc(x)) + fw_x, _ = self.fw_rnn(inputs=fw_x, sequence_length=x_len) + bw_x, _ = self.bw_rnn(inputs=bw_x, sequence_length=x_len) + x = paddle.concat([fw_x, bw_x], axis=-1) + return x, x_len + + +class BiGRUWithBN(nn.Layer): + """Bidirectonal gru layer with sequence-wise batch normalization. + The batch normalization is only performed on input-state weights. + + :param name: Name of the layer. + :type name: string + :param input: Input layer. + :type input: Variable + :param size: Dimension of GRU cells. + :type size: int + :param act: Activation type. + :type act: string + :return: Bidirectional GRU layer. + :rtype: Variable + """ + + def __init__(self, i_size: int, h_size: int): + super().__init__() + hidden_size = h_size * 3 + + self.fw_fc = nn.Linear(i_size, hidden_size, bias_attr=False) + self.fw_bn = nn.BatchNorm1D( + hidden_size, bias_attr=None, data_format='NLC') + self.bw_fc = nn.Linear(i_size, hidden_size, bias_attr=False) + self.bw_bn = nn.BatchNorm1D( + hidden_size, bias_attr=None, data_format='NLC') + + self.fw_cell = GRUCell(input_size=hidden_size, hidden_size=h_size) + self.bw_cell = GRUCell(input_size=hidden_size, hidden_size=h_size) + self.fw_rnn = nn.RNN( + self.fw_cell, is_reverse=False, time_major=False) #[B, T, D] + self.bw_rnn = nn.RNN( + self.fw_cell, is_reverse=True, time_major=False) #[B, T, D] + + def forward(self, x, x_len): + # x, shape [B, T, D] + fw_x = self.fw_bn(self.fw_fc(x)) + bw_x = self.bw_bn(self.bw_fc(x)) + fw_x, _ = self.fw_rnn(inputs=fw_x, sequence_length=x_len) + bw_x, _ = self.bw_rnn(inputs=bw_x, sequence_length=x_len) + x = paddle.concat([fw_x, bw_x], axis=-1) + return x, x_len + + +class RNNStack(nn.Layer): + """RNN group with stacked bidirectional simple RNN or GRU layers. + + :param input: Input layer. + :type input: Variable + :param size: Dimension of RNN cells in each layer. + :type size: int + :param num_stacks: Number of stacked rnn layers. + :type num_stacks: int + :param use_gru: Use gru if set True. Use simple rnn if set False. + :type use_gru: bool + :param share_rnn_weights: Whether to share input-hidden weights between + forward and backward directional RNNs. + It is only available when use_gru=False. + :type share_weights: bool + :return: Output layer of the RNN group. + :rtype: Variable + """ + + def __init__(self, + i_size: int, + h_size: int, + num_stacks: int, + use_gru: bool, + share_rnn_weights: bool): + super().__init__() + rnn_stacks = [] + for i in range(num_stacks): + if use_gru: + #default:GRU using tanh + rnn_stacks.append(BiGRUWithBN(i_size=i_size, h_size=h_size)) + else: + rnn_stacks.append( + BiRNNWithBN( + i_size=i_size, + h_size=h_size, + share_weights=share_rnn_weights)) + i_size = h_size * 2 + + self.rnn_stacks = nn.LayerList(rnn_stacks) + + def forward(self, x: paddle.Tensor, x_len: paddle.Tensor): + """ + x: shape [B, T, D] + x_len: shpae [B] + """ + for i, rnn in enumerate(self.rnn_stacks): + x, x_len = rnn(x, x_len) + masks = make_non_pad_mask(x_len) #[B, T] + masks = masks.unsqueeze(-1) # [B, T, 1] + # TODO(Hui Zhang): not support bool multiply + masks = masks.astype(x.dtype) + x = x.multiply(masks) + + return x, x_len diff --git a/ernie-sat/paddlespeech/s2t/models/ds2_online/__init__.py b/ernie-sat/paddlespeech/s2t/models/ds2_online/__init__.py new file mode 100644 index 0000000..c5fdab1 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2_online/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .deepspeech2 import DeepSpeech2InferModelOnline +from .deepspeech2 import DeepSpeech2ModelOnline +from paddlespeech.s2t.utils import dynamic_pip_install + +try: + import paddlespeech_ctcdecoders +except ImportError: + try: + package_name = 'paddlespeech_ctcdecoders' + dynamic_pip_install.install(package_name) + except Exception: + raise RuntimeError( + "Can not install package paddlespeech_ctcdecoders on your system. \ + The DeepSpeech2 model is not supported for your system") + +__all__ = ['DeepSpeech2ModelOnline', 'DeepSpeech2InferModelOnline'] diff --git a/ernie-sat/paddlespeech/s2t/models/ds2_online/conv.py b/ernie-sat/paddlespeech/s2t/models/ds2_online/conv.py new file mode 100644 index 0000000..25a9715 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2_online/conv.py @@ -0,0 +1,33 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle + +from paddlespeech.s2t.modules.subsampling import Conv2dSubsampling4 + + +class Conv2dSubsampling4Online(Conv2dSubsampling4): + def __init__(self, idim: int, odim: int, dropout_rate: float): + super().__init__(idim, odim, dropout_rate, None) + self.output_dim = ((idim - 1) // 2 - 1) // 2 * odim + self.receptive_field_length = 2 * ( + 3 - 1) + 3 # stride_1 * (kernel_size_2 - 1) + kerel_size_1 + + def forward(self, x: paddle.Tensor, + x_len: paddle.Tensor) -> [paddle.Tensor, paddle.Tensor]: + x = x.unsqueeze(1) # (b, c=1, t, f) + x = self.conv(x) + #b, c, t, f = paddle.shape(x) #not work under jit + x = x.transpose([0, 2, 1, 3]).reshape([0, 0, -1]) + x_len = ((x_len - 1) // 2 - 1) // 2 + return x, x_len diff --git a/ernie-sat/paddlespeech/s2t/models/ds2_online/deepspeech2.py b/ernie-sat/paddlespeech/s2t/models/ds2_online/deepspeech2.py new file mode 100644 index 0000000..9574a62 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/ds2_online/deepspeech2.py @@ -0,0 +1,397 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Deepspeech2 ASR Online Model""" +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.s2t.models.ds2_online.conv import Conv2dSubsampling4Online +from paddlespeech.s2t.modules.ctc import CTCDecoder +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.checkpoint import Checkpoint +from paddlespeech.s2t.utils.log import Log +logger = Log(__name__).getlog() + +__all__ = ['DeepSpeech2ModelOnline', 'DeepSpeech2InferModelOnline'] + + +class CRNNEncoder(nn.Layer): + def __init__(self, + feat_size, + dict_size, + num_conv_layers=2, + num_rnn_layers=4, + rnn_size=1024, + rnn_direction='forward', + num_fc_layers=2, + fc_layers_size_list=[512, 256], + use_gru=False): + super().__init__() + self.rnn_size = rnn_size + self.feat_size = feat_size # 161 for linear + self.dict_size = dict_size + self.num_rnn_layers = num_rnn_layers + self.num_fc_layers = num_fc_layers + self.rnn_direction = rnn_direction + self.fc_layers_size_list = fc_layers_size_list + self.use_gru = use_gru + self.conv = Conv2dSubsampling4Online(feat_size, 32, dropout_rate=0.0) + + self.output_dim = self.conv.output_dim + + i_size = self.conv.output_dim + self.rnn = nn.LayerList() + self.layernorm_list = nn.LayerList() + self.fc_layers_list = nn.LayerList() + if rnn_direction == 'bidirect' or rnn_direction == 'bidirectional': + layernorm_size = 2 * rnn_size + elif rnn_direction == 'forward': + layernorm_size = rnn_size + else: + raise Exception("Wrong rnn direction") + for i in range(0, num_rnn_layers): + if i == 0: + rnn_input_size = i_size + else: + rnn_input_size = layernorm_size + if use_gru is True: + self.rnn.append( + nn.GRU( + input_size=rnn_input_size, + hidden_size=rnn_size, + num_layers=1, + direction=rnn_direction)) + else: + self.rnn.append( + nn.LSTM( + input_size=rnn_input_size, + hidden_size=rnn_size, + num_layers=1, + direction=rnn_direction)) + self.layernorm_list.append(nn.LayerNorm(layernorm_size)) + self.output_dim = layernorm_size + + fc_input_size = layernorm_size + for i in range(self.num_fc_layers): + self.fc_layers_list.append( + nn.Linear(fc_input_size, fc_layers_size_list[i])) + fc_input_size = fc_layers_size_list[i] + self.output_dim = fc_layers_size_list[i] + + @property + def output_size(self): + return self.output_dim + + def forward(self, x, x_lens, init_state_h_box=None, init_state_c_box=None): + """Compute Encoder outputs + + Args: + x (Tensor): [B, T, D] + x_lens (Tensor): [B] + init_state_h_box(Tensor): init_states h for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + init_state_c_box(Tensor): init_states c for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + Return: + x (Tensor): encoder outputs, [B, T, D] + x_lens (Tensor): encoder length, [B] + final_state_h_box(Tensor): final_states h for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + final_state_c_box(Tensor): final_states c for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + """ + if init_state_h_box is not None: + init_state_list = None + + if self.use_gru is True: + init_state_h_list = paddle.split( + init_state_h_box, self.num_rnn_layers, axis=0) + init_state_list = init_state_h_list + else: + init_state_h_list = paddle.split( + init_state_h_box, self.num_rnn_layers, axis=0) + init_state_c_list = paddle.split( + init_state_c_box, self.num_rnn_layers, axis=0) + init_state_list = [(init_state_h_list[i], init_state_c_list[i]) + for i in range(self.num_rnn_layers)] + else: + init_state_list = [None] * self.num_rnn_layers + + x, x_lens = self.conv(x, x_lens) + final_chunk_state_list = [] + for i in range(0, self.num_rnn_layers): + x, final_state = self.rnn[i](x, init_state_list[i], + x_lens) #[B, T, D] + final_chunk_state_list.append(final_state) + x = self.layernorm_list[i](x) + + for i in range(self.num_fc_layers): + x = self.fc_layers_list[i](x) + x = F.relu(x) + + if self.use_gru is True: + final_chunk_state_h_box = paddle.concat( + final_chunk_state_list, axis=0) + final_chunk_state_c_box = init_state_c_box + else: + final_chunk_state_h_list = [ + final_chunk_state_list[i][0] for i in range(self.num_rnn_layers) + ] + final_chunk_state_c_list = [ + final_chunk_state_list[i][1] for i in range(self.num_rnn_layers) + ] + final_chunk_state_h_box = paddle.concat( + final_chunk_state_h_list, axis=0) + final_chunk_state_c_box = paddle.concat( + final_chunk_state_c_list, axis=0) + + return x, x_lens, final_chunk_state_h_box, final_chunk_state_c_box + + def forward_chunk_by_chunk(self, x, x_lens, decoder_chunk_size=8): + """Compute Encoder outputs + + Args: + x (Tensor): [B, T, D] + x_lens (Tensor): [B] + decoder_chunk_size: The chunk size of decoder + Returns: + eouts_list (List of Tensor): The list of encoder outputs in chunk_size: [B, chunk_size, D] * num_chunks + eouts_lens_list (List of Tensor): The list of encoder length in chunk_size: [B] * num_chunks + final_state_h_box(Tensor): final_states h for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + final_state_c_box(Tensor): final_states c for RNN layers: [num_rnn_layers * num_directions, batch_size, hidden_size] + """ + subsampling_rate = self.conv.subsampling_rate + receptive_field_length = self.conv.receptive_field_length + chunk_size = (decoder_chunk_size - 1 + ) * subsampling_rate + receptive_field_length + chunk_stride = subsampling_rate * decoder_chunk_size + max_len = x.shape[1] + assert (chunk_size <= max_len) + + eouts_chunk_list = [] + eouts_chunk_lens_list = [] + if (max_len - chunk_size) % chunk_stride != 0: + padding_len = chunk_stride - (max_len - chunk_size) % chunk_stride + else: + padding_len = 0 + padding = paddle.zeros((x.shape[0], padding_len, x.shape[2])) + padded_x = paddle.concat([x, padding], axis=1) + num_chunk = (max_len + padding_len - chunk_size) / chunk_stride + 1 + num_chunk = int(num_chunk) + chunk_state_h_box = None + chunk_state_c_box = None + final_state_h_box = None + final_state_c_box = None + for i in range(0, num_chunk): + start = i * chunk_stride + end = start + chunk_size + x_chunk = padded_x[:, start:end, :] + + x_len_left = paddle.where(x_lens - i * chunk_stride < 0, + paddle.zeros_like(x_lens), + x_lens - i * chunk_stride) + x_chunk_len_tmp = paddle.ones_like(x_lens) * chunk_size + x_chunk_lens = paddle.where(x_len_left < x_chunk_len_tmp, + x_len_left, x_chunk_len_tmp) + + eouts_chunk, eouts_chunk_lens, chunk_state_h_box, chunk_state_c_box = self.forward( + x_chunk, x_chunk_lens, chunk_state_h_box, chunk_state_c_box) + + eouts_chunk_list.append(eouts_chunk) + eouts_chunk_lens_list.append(eouts_chunk_lens) + final_state_h_box = chunk_state_h_box + final_state_c_box = chunk_state_c_box + return eouts_chunk_list, eouts_chunk_lens_list, final_state_h_box, final_state_c_box + + +class DeepSpeech2ModelOnline(nn.Layer): + """The DeepSpeech2 network structure for online. + + :param audio: Audio spectrogram data layer. + :type audio: Variable + :param text: Transcription text data layer. + :type text: Variable + :param audio_len: Valid sequence length data layer. + :type audio_len: Variable + :param feat_size: feature size for audio. + :type feat_size: int + :param dict_size: Dictionary size for tokenized transcription. + :type dict_size: int + :param num_conv_layers: Number of stacking convolution layers. + :type num_conv_layers: int + :param num_rnn_layers: Number of stacking RNN layers. + :type num_rnn_layers: int + :param rnn_size: RNN layer size (dimension of RNN cells). + :type rnn_size: int + :param num_fc_layers: Number of stacking FC layers. + :type num_fc_layers: int + :param fc_layers_size_list: The list of FC layer sizes. + :type fc_layers_size_list: [int,] + :param use_gru: Use gru if set True. Use simple rnn if set False. + :type use_gru: bool + :return: A tuple of an output unnormalized log probability layer ( + before softmax) and a ctc cost layer. + :rtype: tuple of LayerOutput + """ + + def __init__( + self, + feat_size, + dict_size, + num_conv_layers=2, + num_rnn_layers=4, + rnn_size=1024, + rnn_direction='forward', + num_fc_layers=2, + fc_layers_size_list=[512, 256], + use_gru=False, + blank_id=0, + ctc_grad_norm_type=None, ): + super().__init__() + self.encoder = CRNNEncoder( + feat_size=feat_size, + dict_size=dict_size, + num_conv_layers=num_conv_layers, + num_rnn_layers=num_rnn_layers, + rnn_direction=rnn_direction, + num_fc_layers=num_fc_layers, + fc_layers_size_list=fc_layers_size_list, + rnn_size=rnn_size, + use_gru=use_gru) + + self.decoder = CTCDecoder( + odim=dict_size, # is in vocab + enc_n_units=self.encoder.output_size, + blank_id=blank_id, + dropout_rate=0.0, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=ctc_grad_norm_type) + + def forward(self, audio, audio_len, text, text_len): + """Compute Model loss + + Args: + audio (Tensor): [B, T, D] + audio_len (Tensor): [B] + text (Tensor): [B, U] + text_len (Tensor): [B] + + Returns: + loss (Tensor): [1] + """ + eouts, eouts_len, final_state_h_box, final_state_c_box = self.encoder( + audio, audio_len, None, None) + loss = self.decoder(eouts, eouts_len, text, text_len) + return loss + + @paddle.no_grad() + def decode(self, audio, audio_len): + # decoders only accept string encoded in utf-8 + # Make sure the decoder has been initialized + eouts, eouts_len, final_state_h_box, final_state_c_box = self.encoder( + audio, audio_len, None, None) + probs = self.decoder.softmax(eouts) + batch_size = probs.shape[0] + self.decoder.reset_decoder(batch_size=batch_size) + self.decoder.next(probs, eouts_len) + trans_best, trans_beam = self.decoder.decode() + return trans_best + + @classmethod + def from_pretrained(cls, dataloader, config, checkpoint_path): + """Build a DeepSpeech2Model model from a pretrained model. + Parameters + ---------- + dataloader: paddle.io.DataLoader + + config: yacs.config.CfgNode + model configs + + checkpoint_path: Path or str + the path of pretrained model checkpoint, without extension name + + Returns + ------- + DeepSpeech2ModelOnline + The model built from pretrained result. + """ + model = cls( + feat_size=dataloader.collate_fn.feature_size, + dict_size=dataloader.collate_fn.vocab_size, + num_conv_layers=config.num_conv_layers, + num_rnn_layers=config.num_rnn_layers, + rnn_size=config.rnn_layer_size, + rnn_direction=config.rnn_direction, + num_fc_layers=config.num_fc_layers, + fc_layers_size_list=config.fc_layers_size_list, + use_gru=config.use_gru, + blank_id=config.blank_id, + ctc_grad_norm_type=config.get('ctc_grad_norm_type', None), ) + infos = Checkpoint().load_parameters( + model, checkpoint_path=checkpoint_path) + logger.info(f"checkpoint info: {infos}") + layer_tools.summary(model) + return model + + @classmethod + def from_config(cls, config): + """Build a DeepSpeec2ModelOnline from config + Parameters + + config: yacs.config.CfgNode + config + Returns + ------- + DeepSpeech2ModelOnline + The model built from config. + """ + model = cls( + feat_size=config.input_dim, + dict_size=config.output_dim, + num_conv_layers=config.num_conv_layers, + num_rnn_layers=config.num_rnn_layers, + rnn_size=config.rnn_layer_size, + rnn_direction=config.rnn_direction, + num_fc_layers=config.num_fc_layers, + fc_layers_size_list=config.fc_layers_size_list, + use_gru=config.use_gru, + blank_id=config.blank_id, + ctc_grad_norm_type=config.get('ctc_grad_norm_type', None), ) + return model + + +class DeepSpeech2InferModelOnline(DeepSpeech2ModelOnline): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def forward(self, audio_chunk, audio_chunk_lens, chunk_state_h_box, + chunk_state_c_box): + eouts_chunk, eouts_chunk_lens, final_state_h_box, final_state_c_box = self.encoder( + audio_chunk, audio_chunk_lens, chunk_state_h_box, chunk_state_c_box) + probs_chunk = self.decoder.softmax(eouts_chunk) + return probs_chunk, eouts_chunk_lens, final_state_h_box, final_state_c_box + + def export(self): + static_model = paddle.jit.to_static( + self, + input_spec=[ + paddle.static.InputSpec( + shape=[None, None, + self.encoder.feat_size], #[B, chunk_size, feat_dim] + dtype='float32'), + paddle.static.InputSpec(shape=[None], + dtype='int64'), # audio_length, [B] + paddle.static.InputSpec( + shape=[None, None, None], dtype='float32'), + paddle.static.InputSpec( + shape=[None, None, None], dtype='float32') + ]) + return static_model diff --git a/ernie-sat/paddlespeech/s2t/models/lm/__init__.py b/ernie-sat/paddlespeech/s2t/models/lm/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/lm/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/models/lm/dataset.py b/ernie-sat/paddlespeech/s2t/models/lm/dataset.py new file mode 100644 index 0000000..25a47be --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/lm/dataset.py @@ -0,0 +1,74 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from paddle.io import Dataset + +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.io.utility import pad_list + + +class TextDataset(Dataset): + @classmethod + def from_file(cls, file_path): + dataset = cls(file_path) + return dataset + + def __init__(self, file_path): + self._manifest = [] + with open(file_path) as f: + for line in f: + self._manifest.append(line.strip()) + + def __len__(self): + return len(self._manifest) + + def __getitem__(self, idx): + return self._manifest[idx] + + +class TextCollatorSpm(): + def __init__(self, unit_type, vocab_filepath, spm_model_prefix): + assert (vocab_filepath is not None) + self.text_featurizer = TextFeaturizer( + unit_type=unit_type, + vocab=vocab_filepath, + spm_model_prefix=spm_model_prefix) + self.eos_id = self.text_featurizer.eos_id + self.blank_id = self.text_featurizer.blank_id + + def __call__(self, batch): + """ + return type [List, np.array [B, T], np.array [B, T], np.array[B]] + """ + keys = [] + texts = [] + texts_input = [] + texts_output = [] + text_lens = [] + + for idx, item in enumerate(batch): + key = item.split(" ")[0].strip() + text = " ".join(item.split(" ")[1:]) + keys.append(key) + token_ids = self.text_featurizer.featurize(text) + texts_input.append( + np.array([self.eos_id] + token_ids).astype(np.int64)) + texts_output.append( + np.array(token_ids + [self.eos_id]).astype(np.int64)) + text_lens.append(len(token_ids) + 1) + + ys_input_pad = pad_list(texts_input, self.blank_id).astype(np.int64) + ys_output_pad = pad_list(texts_output, self.blank_id).astype(np.int64) + y_lens = np.array(text_lens).astype(np.int64) + return keys, ys_input_pad, ys_output_pad, y_lens diff --git a/ernie-sat/paddlespeech/s2t/models/lm/transformer.py b/ernie-sat/paddlespeech/s2t/models/lm/transformer.py new file mode 100644 index 0000000..85bd7c2 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/lm/transformer.py @@ -0,0 +1,266 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from typing import Any +from typing import List +from typing import Tuple + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from paddlespeech.s2t.decoders.scorers.scorer_interface import BatchScorerInterface +from paddlespeech.s2t.models.lm_interface import LMInterface +from paddlespeech.s2t.modules.encoder import TransformerEncoder +from paddlespeech.s2t.modules.mask import subsequent_mask +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + + +class TransformerLM(nn.Layer, LMInterface, BatchScorerInterface): + def __init__(self, + n_vocab: int, + pos_enc: str=None, + embed_unit: int=128, + att_unit: int=256, + head: int=2, + unit: int=1024, + layer: int=4, + dropout_rate: float=0.5, + emb_dropout_rate: float=0.0, + att_dropout_rate: float=0.0, + tie_weights: bool=False, + **kwargs): + nn.Layer.__init__(self) + + if pos_enc == "sinusoidal": + pos_enc_layer_type = "abs_pos" + elif pos_enc is None: + pos_enc_layer_type = "no_pos" + else: + raise ValueError(f"unknown pos-enc option: {pos_enc}") + + self.embed = nn.Embedding(n_vocab, embed_unit) + + if emb_dropout_rate == 0.0: + self.embed_drop = None + else: + self.embed_drop = nn.Dropout(emb_dropout_rate) + + self.encoder = TransformerEncoder( + input_size=embed_unit, + output_size=att_unit, + attention_heads=head, + linear_units=unit, + num_blocks=layer, + dropout_rate=dropout_rate, + attention_dropout_rate=att_dropout_rate, + input_layer="linear", + pos_enc_layer_type=pos_enc_layer_type, + concat_after=False, + static_chunk_size=1, + use_dynamic_chunk=False, + use_dynamic_left_chunk=False) + + self.decoder = nn.Linear(att_unit, n_vocab) + + logger.info("Tie weights set to {}".format(tie_weights)) + logger.info("Dropout set to {}".format(dropout_rate)) + logger.info("Emb Dropout set to {}".format(emb_dropout_rate)) + logger.info("Att Dropout set to {}".format(att_dropout_rate)) + + if tie_weights: + assert ( + att_unit == embed_unit + ), "Tie Weights: True need embedding and final dimensions to match" + self.decoder.weight = self.embed.weight + + def _target_mask(self, ys_in_pad): + ys_mask = ys_in_pad != 0 + m = subsequent_mask(ys_mask.size(-1)).unsqueeze(0) + return ys_mask.unsqueeze(-2) & m + + def forward(self, x: paddle.Tensor, t: paddle.Tensor + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Compute LM loss value from buffer sequences. + + Args: + x (paddle.Tensor): Input ids. (batch, len) + t (paddle.Tensor): Target ids. (batch, len) + + Returns: + tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: Tuple of + loss to backward (scalar), + negative log-likelihood of t: -log p(t) (scalar) and + the number of elements in x (scalar) + + Notes: + The last two return values are used + in perplexity: p(t)^{-n} = exp(-log p(t) / n) + + """ + batch_size = x.size(0) + xm = x != 0 + xlen = xm.sum(axis=1) + if self.embed_drop is not None: + emb = self.embed_drop(self.embed(x)) + else: + emb = self.embed(x) + h, _ = self.encoder(emb, xlen) + y = self.decoder(h) + loss = F.cross_entropy( + y.view(-1, y.shape[-1]), t.view(-1), reduction="none") + mask = xm.to(loss.dtype) + logp = loss * mask.view(-1) + nll = logp.view(batch_size, -1).sum(-1) + nll_count = mask.sum(-1) + logp = logp.sum() + count = mask.sum() + return logp / count, logp, count, nll, nll_count + + # beam search API (see ScorerInterface) + def score(self, y: paddle.Tensor, state: Any, + x: paddle.Tensor) -> Tuple[paddle.Tensor, Any]: + """Score new token. + + Args: + y (paddle.Tensor): 1D paddle.int64 prefix tokens. + state: Scorer state for prefix tokens + x (paddle.Tensor): encoder feature that generates ys. + + Returns: + tuple[paddle.Tensor, Any]: Tuple of + paddle.float32 scores for next token (n_vocab) + and next state for ys + + """ + y = y.unsqueeze(0) + + if self.embed_drop is not None: + emb = self.embed_drop(self.embed(y)) + else: + emb = self.embed(y) + + h, _, cache = self.encoder.forward_one_step( + emb, self._target_mask(y), cache=state) + h = self.decoder(h[:, -1]) + logp = F.log_softmax(h).squeeze(0) + return logp, cache + + # batch beam search API (see BatchScorerInterface) + def batch_score(self, + ys: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor) -> Tuple[paddle.Tensor, List[Any]]: + """Score new token batch (required). + + Args: + ys (paddle.Tensor): paddle.int64 prefix tokens (n_batch, ylen). + states (List[Any]): Scorer states for prefix tokens. + xs (paddle.Tensor): + The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + # merge states + n_batch = len(ys) + n_layers = len(self.encoder.encoders) + if states[0] is None: + batch_state = None + else: + # transpose state of [batch, layer] into [layer, batch] + batch_state = [ + paddle.stack([states[b][i] for b in range(n_batch)]) + for i in range(n_layers) + ] + + if self.embed_drop is not None: + emb = self.embed_drop(self.embed(ys)) + else: + emb = self.embed(ys) + + # batch decoding + h, _, states = self.encoder.forward_one_step( + emb, self._target_mask(ys), cache=batch_state) + h = self.decoder(h[:, -1]) + logp = F.log_softmax(h) + + # transpose state of [layer, batch] into [batch, layer] + state_list = [[states[i][b] for i in range(n_layers)] + for b in range(n_batch)] + return logp, state_list + + +if __name__ == "__main__": + tlm = TransformerLM( + n_vocab=5002, + pos_enc=None, + embed_unit=128, + att_unit=512, + head=8, + unit=2048, + layer=16, + dropout_rate=0.5, ) + + # n_vocab: int, + # pos_enc: str=None, + # embed_unit: int=128, + # att_unit: int=256, + # head: int=2, + # unit: int=1024, + # layer: int=4, + # dropout_rate: float=0.5, + # emb_dropout_rate: float = 0.0, + # att_dropout_rate: float = 0.0, + # tie_weights: bool = False,): + paddle.set_device("cpu") + model_dict = paddle.load("transformerLM.pdparams") + tlm.set_state_dict(model_dict) + + tlm.eval() + #Test the score + input2 = np.array([5]) + input2 = paddle.to_tensor(input2) + state = None + output, state = tlm.score(input2, state, None) + + input3 = np.array([5, 10]) + input3 = paddle.to_tensor(input3) + output, state = tlm.score(input3, state, None) + + input4 = np.array([5, 10, 0]) + input4 = paddle.to_tensor(input4) + output, state = tlm.score(input4, state, None) + print("output", output) + """ + #Test the batch score + batch_size = 2 + inp2 = np.array([[5], [10]]) + inp2 = paddle.to_tensor(inp2) + output, states = tlm.batch_score( + inp2, [(None,None,0)] * batch_size) + inp3 = np.array([[100], [30]]) + inp3 = paddle.to_tensor(inp3) + output, states = tlm.batch_score( + inp3, states) + print("output", output) + #print("cache", cache) + #np.save("output_pd.npy", output) + """ diff --git a/ernie-sat/paddlespeech/s2t/models/lm_interface.py b/ernie-sat/paddlespeech/s2t/models/lm_interface.py new file mode 100644 index 0000000..c8f3776 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/lm_interface.py @@ -0,0 +1,83 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Language model interface.""" +import argparse + +from paddlespeech.s2t.decoders.scorers.scorer_interface import ScorerInterface +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + + +class LMInterface(ScorerInterface): + """LM Interface model implementation.""" + + @staticmethod + def add_arguments(parser): + """Add arguments to command line argument parser.""" + return parser + + @classmethod + def build(cls, n_vocab: int, **kwargs): + """Initialize this class with python-level args. + + Args: + idim (int): The number of vocabulary. + + Returns: + LMinterface: A new instance of LMInterface. + + """ + args = argparse.Namespace(**kwargs) + return cls(n_vocab, args) + + def forward(self, x, t): + """Compute LM loss value from buffer sequences. + + Args: + x (torch.Tensor): Input ids. (batch, len) + t (torch.Tensor): Target ids. (batch, len) + + Returns: + tuple[torch.Tensor, torch.Tensor, torch.Tensor]: Tuple of + loss to backward (scalar), + negative log-likelihood of t: -log p(t) (scalar) and + the number of elements in x (scalar) + + Notes: + The last two return values are used + in perplexity: p(t)^{-n} = exp(-log p(t) / n) + + """ + raise NotImplementedError("forward method is not implemented") + + +predefined_lms = { + "transformer": "paddlespeech.s2t.models.lm.transformer:TransformerLM", +} + + +def dynamic_import_lm(module): + """Import LM class dynamically. + + Args: + module (str): module_name:class_name or alias in `predefined_lms` + + Returns: + type: LM class + + """ + model_class = dynamic_import(module, predefined_lms) + assert issubclass(model_class, + LMInterface), f"{module} does not implement LMInterface" + return model_class diff --git a/ernie-sat/paddlespeech/s2t/models/st_interface.py b/ernie-sat/paddlespeech/s2t/models/st_interface.py new file mode 100644 index 0000000..4d36859 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/st_interface.py @@ -0,0 +1,76 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""ST Interface module.""" +from .asr_interface import ASRInterface +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + + +class STInterface(ASRInterface): + """ST Interface model implementation. + + NOTE: This class is inherited from ASRInterface to enable joint translation + and recognition when performing multi-task learning with the ASR task. + + """ + + def translate(self, + x, + trans_args, + char_list=None, + rnnlm=None, + ensemble_models=[]): + """Recognize x for evaluation. + + :param ndarray x: input acouctic feature (B, T, D) or (T, D) + :param namespace trans_args: argment namespace contraining options + :param list char_list: list of characters + :param paddle.nn.Layer rnnlm: language model module + :return: N-best decoding results + :rtype: list + """ + raise NotImplementedError("translate method is not implemented") + + def translate_batch(self, x, trans_args, char_list=None, rnnlm=None): + """Beam search implementation for batch. + + :param paddle.Tensor x: encoder hidden state sequences (B, Tmax, Henc) + :param namespace trans_args: argument namespace containing options + :param list char_list: list of characters + :param paddle.nn.Layer rnnlm: language model module + :return: N-best decoding results + :rtype: list + """ + raise NotImplementedError("Batch decoding is not supported yet.") + + +predefined_st = { + "transformer": "paddlespeech.s2t.models.u2_st:U2STModel", +} + + +def dynamic_import_st(module): + """Import ST models dynamically. + + Args: + module (str): module_name:class_name or alias in `predefined_st` + + Returns: + type: ST class + + """ + model_class = dynamic_import(module, predefined_st) + assert issubclass(model_class, + STInterface), f"{module} does not implement STInterface" + return model_class diff --git a/ernie-sat/paddlespeech/s2t/models/u2/__init__.py b/ernie-sat/paddlespeech/s2t/models/u2/__init__.py new file mode 100644 index 0000000..a9010f1 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/u2/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .u2 import U2InferModel +from .u2 import U2Model +from .updater import U2Evaluator +from .updater import U2Updater + +__all__ = ["U2Model", "U2InferModel", "U2Evaluator", "U2Updater"] diff --git a/ernie-sat/paddlespeech/s2t/models/u2/u2.py b/ernie-sat/paddlespeech/s2t/models/u2/u2.py new file mode 100644 index 0000000..6a98607 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/u2/u2.py @@ -0,0 +1,926 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""U2 ASR Model +Unified Streaming and Non-streaming Two-pass End-to-end Model for Speech Recognition +(https://arxiv.org/pdf/2012.05481.pdf) +""" +import sys +import time +from collections import defaultdict +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +import paddle +from paddle import jit +from paddle import nn + +from paddlespeech.s2t.decoders.scorers.ctc import CTCPrefixScorer +from paddlespeech.s2t.frontend.utility import IGNORE_ID +from paddlespeech.s2t.frontend.utility import load_cmvn +from paddlespeech.s2t.models.asr_interface import ASRInterface +from paddlespeech.s2t.modules.cmvn import GlobalCMVN +from paddlespeech.s2t.modules.ctc import CTCDecoderBase +from paddlespeech.s2t.modules.decoder import TransformerDecoder +from paddlespeech.s2t.modules.encoder import ConformerEncoder +from paddlespeech.s2t.modules.encoder import TransformerEncoder +from paddlespeech.s2t.modules.initializer import DefaultInitializerContext +from paddlespeech.s2t.modules.loss import LabelSmoothingLoss +from paddlespeech.s2t.modules.mask import make_pad_mask +from paddlespeech.s2t.modules.mask import mask_finished_preds +from paddlespeech.s2t.modules.mask import mask_finished_scores +from paddlespeech.s2t.modules.mask import subsequent_mask +from paddlespeech.s2t.utils import checkpoint +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.ctc_utils import remove_duplicates_and_blank +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.tensor_utils import add_sos_eos +from paddlespeech.s2t.utils.tensor_utils import pad_sequence +from paddlespeech.s2t.utils.tensor_utils import th_accuracy +from paddlespeech.s2t.utils.utility import log_add +from paddlespeech.s2t.utils.utility import UpdateConfig + +__all__ = ["U2Model", "U2InferModel"] + +logger = Log(__name__).getlog() + + +class U2BaseModel(ASRInterface, nn.Layer): + """CTC-Attention hybrid Encoder-Decoder model""" + + def __init__(self, + vocab_size: int, + encoder: TransformerEncoder, + decoder: TransformerDecoder, + ctc: CTCDecoderBase, + ctc_weight: float=0.5, + ignore_id: int=IGNORE_ID, + lsm_weight: float=0.0, + length_normalized_loss: bool=False, + **kwargs): + assert 0.0 <= ctc_weight <= 1.0, ctc_weight + + nn.Layer.__init__(self) + + # note that eos is the same as sos (equivalent ID) + self.sos = vocab_size - 1 + self.eos = vocab_size - 1 + self.vocab_size = vocab_size + self.ignore_id = ignore_id + self.ctc_weight = ctc_weight + + self.encoder = encoder + self.decoder = decoder + self.ctc = ctc + self.criterion_att = LabelSmoothingLoss( + size=vocab_size, + padding_idx=ignore_id, + smoothing=lsm_weight, + normalize_length=length_normalized_loss, ) + + def forward( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + text: paddle.Tensor, + text_lengths: paddle.Tensor, + ) -> Tuple[Optional[paddle.Tensor], Optional[paddle.Tensor], Optional[ + paddle.Tensor]]: + """Frontend + Encoder + Decoder + Calc loss + Args: + speech: (Batch, Length, ...) + speech_lengths: (Batch, ) + text: (Batch, Length) + text_lengths: (Batch,) + Returns: + total_loss, attention_loss, ctc_loss + """ + assert text_lengths.dim() == 1, text_lengths.shape + # Check that batch_size is unified + assert (speech.shape[0] == speech_lengths.shape[0] == text.shape[0] == + text_lengths.shape[0]), (speech.shape, speech_lengths.shape, + text.shape, text_lengths.shape) + # 1. Encoder + start = time.time() + encoder_out, encoder_mask = self.encoder(speech, speech_lengths) + encoder_time = time.time() - start + #logger.debug(f"encoder time: {encoder_time}") + #TODO(Hui Zhang): sum not support bool type + #encoder_out_lens = encoder_mask.squeeze(1).sum(1) #[B, 1, T] -> [B] + encoder_out_lens = encoder_mask.squeeze(1).cast(paddle.int64).sum( + 1) #[B, 1, T] -> [B] + + # 2a. Attention-decoder branch + loss_att = None + if self.ctc_weight != 1.0: + start = time.time() + loss_att, acc_att = self._calc_att_loss(encoder_out, encoder_mask, + text, text_lengths) + decoder_time = time.time() - start + #logger.debug(f"decoder time: {decoder_time}") + + # 2b. CTC branch + loss_ctc = None + if self.ctc_weight != 0.0: + start = time.time() + loss_ctc = self.ctc(encoder_out, encoder_out_lens, text, + text_lengths) + ctc_time = time.time() - start + #logger.debug(f"ctc time: {ctc_time}") + + if loss_ctc is None: + loss = loss_att + elif loss_att is None: + loss = loss_ctc + else: + loss = self.ctc_weight * loss_ctc + (1 - self.ctc_weight) * loss_att + return loss, loss_att, loss_ctc + + def _calc_att_loss( + self, + encoder_out: paddle.Tensor, + encoder_mask: paddle.Tensor, + ys_pad: paddle.Tensor, + ys_pad_lens: paddle.Tensor, ) -> Tuple[paddle.Tensor, float]: + """Calc attention loss. + + Args: + encoder_out (paddle.Tensor): [B, Tmax, D] + encoder_mask (paddle.Tensor): [B, 1, Tmax] + ys_pad (paddle.Tensor): [B, Umax] + ys_pad_lens (paddle.Tensor): [B] + + Returns: + Tuple[paddle.Tensor, float]: attention_loss, accuracy rate + """ + ys_in_pad, ys_out_pad = add_sos_eos(ys_pad, self.sos, self.eos, + self.ignore_id) + ys_in_lens = ys_pad_lens + 1 + + # 1. Forward decoder + decoder_out, _ = self.decoder(encoder_out, encoder_mask, ys_in_pad, + ys_in_lens) + + # 2. Compute attention loss + loss_att = self.criterion_att(decoder_out, ys_out_pad) + acc_att = th_accuracy( + decoder_out.view(-1, self.vocab_size), + ys_out_pad, + ignore_label=self.ignore_id, ) + return loss_att, acc_att + + def _forward_encoder( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Encoder pass. + + Args: + speech (paddle.Tensor): [B, Tmax, D] + speech_lengths (paddle.Tensor): [B] + decoding_chunk_size (int, optional): chuck size. Defaults to -1. + num_decoding_left_chunks (int, optional): nums chunks. Defaults to -1. + simulate_streaming (bool, optional): streaming or not. Defaults to False. + + Returns: + Tuple[paddle.Tensor, paddle.Tensor]: + encoder hiddens (B, Tmax, D), + encoder hiddens mask (B, 1, Tmax). + """ + # Let's assume B = batch_size + # 1. Encoder + if simulate_streaming and decoding_chunk_size > 0: + encoder_out, encoder_mask = self.encoder.forward_chunk_by_chunk( + speech, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks + ) # (B, maxlen, encoder_dim) + else: + encoder_out, encoder_mask = self.encoder( + speech, + speech_lengths, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks + ) # (B, maxlen, encoder_dim) + return encoder_out, encoder_mask + + def recognize( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + beam_size: int=10, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, ) -> paddle.Tensor: + """ Apply beam search on attention decoder + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + paddle.Tensor: decoding result, (batch, max_result_len) + """ + assert speech.shape[0] == speech_lengths.shape[0] + assert decoding_chunk_size != 0 + device = speech.place + batch_size = speech.shape[0] + + # Let's assume B = batch_size and N = beam_size + # 1. Encoder + encoder_out, encoder_mask = self._forward_encoder( + speech, speech_lengths, decoding_chunk_size, + num_decoding_left_chunks, + simulate_streaming) # (B, maxlen, encoder_dim) + maxlen = encoder_out.shape[1] + encoder_dim = encoder_out.shape[2] + running_size = batch_size * beam_size + encoder_out = encoder_out.unsqueeze(1).repeat(1, beam_size, 1, 1).view( + running_size, maxlen, encoder_dim) # (B*N, maxlen, encoder_dim) + encoder_mask = encoder_mask.unsqueeze(1).repeat( + 1, beam_size, 1, 1).view(running_size, 1, + maxlen) # (B*N, 1, max_len) + + hyps = paddle.ones( + [running_size, 1], dtype=paddle.long).fill_(self.sos) # (B*N, 1) + # log scale score + scores = paddle.to_tensor( + [0.0] + [-float('inf')] * (beam_size - 1), dtype=paddle.float) + scores = scores.to(device).repeat(batch_size).unsqueeze(1).to( + device) # (B*N, 1) + end_flag = paddle.zeros_like(scores, dtype=paddle.bool) # (B*N, 1) + cache: Optional[List[paddle.Tensor]] = None + # 2. Decoder forward step by step + for i in range(1, maxlen + 1): + # Stop if all batch and all beam produce eos + # TODO(Hui Zhang): if end_flag.sum() == running_size: + if end_flag.cast(paddle.int64).sum() == running_size: + break + + # 2.1 Forward decoder step + hyps_mask = subsequent_mask(i).unsqueeze(0).repeat( + running_size, 1, 1).to(device) # (B*N, i, i) + # logp: (B*N, vocab) + logp, cache = self.decoder.forward_one_step( + encoder_out, encoder_mask, hyps, hyps_mask, cache) + + # 2.2 First beam prune: select topk best prob at current time + top_k_logp, top_k_index = logp.topk(beam_size) # (B*N, N) + top_k_logp = mask_finished_scores(top_k_logp, end_flag) + top_k_index = mask_finished_preds(top_k_index, end_flag, self.eos) + + # 2.3 Seconde beam prune: select topk score with history + scores = scores + top_k_logp # (B*N, N), broadcast add + scores = scores.view(batch_size, beam_size * beam_size) # (B, N*N) + scores, offset_k_index = scores.topk(k=beam_size) # (B, N) + scores = scores.view(-1, 1) # (B*N, 1) + + # 2.4. Compute base index in top_k_index, + # regard top_k_index as (B*N*N),regard offset_k_index as (B*N), + # then find offset_k_index in top_k_index + base_k_index = paddle.arange(batch_size).view(-1, 1).repeat( + 1, beam_size) # (B, N) + base_k_index = base_k_index * beam_size * beam_size + best_k_index = base_k_index.view(-1) + offset_k_index.view( + -1) # (B*N) + + # 2.5 Update best hyps + best_k_pred = paddle.index_select( + top_k_index.view(-1), index=best_k_index, axis=0) # (B*N) + best_hyps_index = best_k_index // beam_size + last_best_k_hyps = paddle.index_select( + hyps, index=best_hyps_index, axis=0) # (B*N, i) + hyps = paddle.cat( + (last_best_k_hyps, best_k_pred.view(-1, 1)), + dim=1) # (B*N, i+1) + + # 2.6 Update end flag + end_flag = paddle.eq(hyps[:, -1], self.eos).view(-1, 1) + + # 3. Select best of best + scores = scores.view(batch_size, beam_size) + # TODO: length normalization + best_index = paddle.argmax(scores, axis=-1).long() # (B) + best_hyps_index = best_index + paddle.arange( + batch_size, dtype=paddle.long) * beam_size + best_hyps = paddle.index_select(hyps, index=best_hyps_index, axis=0) + best_hyps = best_hyps[:, 1:] + return best_hyps + + def ctc_greedy_search( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, ) -> List[List[int]]: + """ Apply CTC greedy search + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + List[List[int]]: best path result + """ + assert speech.shape[0] == speech_lengths.shape[0] + assert decoding_chunk_size != 0 + batch_size = speech.shape[0] + + # Let's assume B = batch_size + # encoder_out: (B, maxlen, encoder_dim) + # encoder_mask: (B, 1, Tmax) + encoder_out, encoder_mask = self._forward_encoder( + speech, speech_lengths, decoding_chunk_size, + num_decoding_left_chunks, simulate_streaming) + maxlen = encoder_out.shape[1] + # (TODO Hui Zhang): bool no support reduce_sum + # encoder_out_lens = encoder_mask.squeeze(1).sum(1) + encoder_out_lens = encoder_mask.squeeze(1).astype(paddle.int).sum(1) + ctc_probs = self.ctc.log_softmax(encoder_out) # (B, maxlen, vocab_size) + + topk_prob, topk_index = ctc_probs.topk(1, axis=2) # (B, maxlen, 1) + topk_index = topk_index.view(batch_size, maxlen) # (B, maxlen) + pad_mask = make_pad_mask(encoder_out_lens) # (B, maxlen) + topk_index = topk_index.masked_fill_(pad_mask, self.eos) # (B, maxlen) + + hyps = [hyp.tolist() for hyp in topk_index] + hyps = [remove_duplicates_and_blank(hyp) for hyp in hyps] + return hyps + + def _ctc_prefix_beam_search( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + beam_size: int, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, + blank_id: int=0, ) -> Tuple[List[Tuple[int, float]], paddle.Tensor]: + """ CTC prefix beam search inner implementation + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + List[Tuple[int, float]]: nbest results, (N,1), (text, likelihood) + paddle.Tensor: encoder output, (1, max_len, encoder_dim), + it will be used for rescoring in attention rescoring mode + """ + assert speech.shape[0] == speech_lengths.shape[0] + assert decoding_chunk_size != 0 + batch_size = speech.shape[0] + # For CTC prefix beam search, we only support batch_size=1 + assert batch_size == 1 + + # Let's assume B = batch_size and N = beam_size + # 1. Encoder forward and get CTC score + encoder_out, encoder_mask = self._forward_encoder( + speech, speech_lengths, decoding_chunk_size, + num_decoding_left_chunks, + simulate_streaming) # (B, maxlen, encoder_dim) + maxlen = encoder_out.shape[1] + ctc_probs = self.ctc.log_softmax(encoder_out) # (1, maxlen, vocab_size) + ctc_probs = ctc_probs.squeeze(0) + + # cur_hyps: (prefix, (blank_ending_score, none_blank_ending_score)) + # blank_ending_score and none_blank_ending_score in ln domain + cur_hyps = [(tuple(), (0.0, -float('inf')))] + # 2. CTC beam search step by step + for t in range(0, maxlen): + logp = ctc_probs[t] # (vocab_size,) + # key: prefix, value (pb, pnb), default value(-inf, -inf) + next_hyps = defaultdict(lambda: (-float('inf'), -float('inf'))) + # 2.1 First beam prune: select topk best + top_k_logp, top_k_index = logp.topk(beam_size) # (beam_size,) + for s in top_k_index: + s = s.item() + ps = logp[s].item() + for prefix, (pb, pnb) in cur_hyps: + last = prefix[-1] if len(prefix) > 0 else None + if s == blank_id: # blank + n_pb, n_pnb = next_hyps[prefix] + n_pb = log_add([n_pb, pb + ps, pnb + ps]) + next_hyps[prefix] = (n_pb, n_pnb) + elif s == last: + # Update *ss -> *s; + n_pb, n_pnb = next_hyps[prefix] + n_pnb = log_add([n_pnb, pnb + ps]) + next_hyps[prefix] = (n_pb, n_pnb) + # Update *s-s -> *ss, - is for blank + n_prefix = prefix + (s, ) + n_pb, n_pnb = next_hyps[n_prefix] + n_pnb = log_add([n_pnb, pb + ps]) + next_hyps[n_prefix] = (n_pb, n_pnb) + else: + n_prefix = prefix + (s, ) + n_pb, n_pnb = next_hyps[n_prefix] + n_pnb = log_add([n_pnb, pb + ps, pnb + ps]) + next_hyps[n_prefix] = (n_pb, n_pnb) + + # 2.2 Second beam prune + next_hyps = sorted( + next_hyps.items(), + key=lambda x: log_add(list(x[1])), + reverse=True) + cur_hyps = next_hyps[:beam_size] + + hyps = [(y[0], log_add([y[1][0], y[1][1]])) for y in cur_hyps] + return hyps, encoder_out + + def ctc_prefix_beam_search( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + beam_size: int, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, ) -> List[int]: + """ Apply CTC prefix beam search + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + List[int]: CTC prefix beam search nbest results + """ + hyps, _ = self._ctc_prefix_beam_search( + speech, speech_lengths, beam_size, decoding_chunk_size, + num_decoding_left_chunks, simulate_streaming) + return hyps[0][0] + + def attention_rescoring( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + beam_size: int, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + ctc_weight: float=0.0, + simulate_streaming: bool=False, ) -> List[int]: + """ Apply attention rescoring decoding, CTC prefix beam search + is applied first to get nbest, then we resoring the nbest on + attention decoder with corresponding encoder out + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + List[int]: Attention rescoring result + """ + assert speech.shape[0] == speech_lengths.shape[0] + assert decoding_chunk_size != 0 + device = speech.place + batch_size = speech.shape[0] + # For attention rescoring we only support batch_size=1 + assert batch_size == 1 + + # len(hyps) = beam_size, encoder_out: (1, maxlen, encoder_dim) + hyps, encoder_out = self._ctc_prefix_beam_search( + speech, speech_lengths, beam_size, decoding_chunk_size, + num_decoding_left_chunks, simulate_streaming) + assert len(hyps) == beam_size + + hyp_list = [] + for hyp in hyps: + hyp_content = hyp[0] + # Prevent the hyp is empty + if len(hyp_content) == 0: + hyp_content = (self.ctc.blank_id, ) + hyp_content = paddle.to_tensor( + hyp_content, place=device, dtype=paddle.long) + hyp_list.append(hyp_content) + hyps_pad = pad_sequence(hyp_list, True, self.ignore_id) + hyps_lens = paddle.to_tensor( + [len(hyp[0]) for hyp in hyps], place=device, + dtype=paddle.long) # (beam_size,) + hyps_pad, _ = add_sos_eos(hyps_pad, self.sos, self.eos, self.ignore_id) + hyps_lens = hyps_lens + 1 # Add at begining + + encoder_out = encoder_out.repeat(beam_size, 1, 1) + encoder_mask = paddle.ones( + (beam_size, 1, encoder_out.shape[1]), dtype=paddle.bool) + decoder_out, _ = self.decoder( + encoder_out, encoder_mask, hyps_pad, + hyps_lens) # (beam_size, max_hyps_len, vocab_size) + # ctc score in ln domain + decoder_out = paddle.nn.functional.log_softmax(decoder_out, axis=-1) + decoder_out = decoder_out.numpy() + + # Only use decoder score for rescoring + best_score = -float('inf') + best_index = 0 + # hyps is List[(Text=List[int], Score=float)], len(hyps)=beam_size + for i, hyp in enumerate(hyps): + score = 0.0 + for j, w in enumerate(hyp[0]): + score += decoder_out[i][j][w] + # last decoder output token is `eos`, for laste decoder input token. + score += decoder_out[i][len(hyp[0])][self.eos] + # add ctc score (which in ln domain) + score += hyp[1] * ctc_weight + if score > best_score: + best_score = score + best_index = i + return hyps[best_index][0] + + #@jit.to_static + def subsampling_rate(self) -> int: + """ Export interface for c++ call, return subsampling_rate of the + model + """ + return self.encoder.embed.subsampling_rate + + #@jit.to_static + def right_context(self) -> int: + """ Export interface for c++ call, return right_context of the model + """ + return self.encoder.embed.right_context + + #@jit.to_static + def sos_symbol(self) -> int: + """ Export interface for c++ call, return sos symbol id of the model + """ + return self.sos + + #@jit.to_static + def eos_symbol(self) -> int: + """ Export interface for c++ call, return eos symbol id of the model + """ + return self.eos + + @jit.to_static + def forward_encoder_chunk( + self, + xs: paddle.Tensor, + offset: int, + required_cache_size: int, + subsampling_cache: Optional[paddle.Tensor]=None, + elayers_output_cache: Optional[List[paddle.Tensor]]=None, + conformer_cnn_cache: Optional[List[paddle.Tensor]]=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, List[paddle.Tensor], List[ + paddle.Tensor]]: + """ Export interface for c++ call, give input chunk xs, and return + output from time 0 to current chunk. + Args: + xs (paddle.Tensor): chunk input + subsampling_cache (Optional[paddle.Tensor]): subsampling cache + elayers_output_cache (Optional[List[paddle.Tensor]]): + transformer/conformer encoder layers output cache + conformer_cnn_cache (Optional[List[paddle.Tensor]]): conformer + cnn cache + Returns: + paddle.Tensor: output, it ranges from time 0 to current chunk. + paddle.Tensor: subsampling cache + List[paddle.Tensor]: attention cache + List[paddle.Tensor]: conformer cnn cache + """ + return self.encoder.forward_chunk( + xs, offset, required_cache_size, subsampling_cache, + elayers_output_cache, conformer_cnn_cache) + + # @jit.to_static + def ctc_activation(self, xs: paddle.Tensor) -> paddle.Tensor: + """ Export interface for c++ call, apply linear transform and log + softmax before ctc + Args: + xs (paddle.Tensor): encoder output, (B, T, D) + Returns: + paddle.Tensor: activation before ctc + """ + return self.ctc.log_softmax(xs) + + @jit.to_static + def forward_attention_decoder( + self, + hyps: paddle.Tensor, + hyps_lens: paddle.Tensor, + encoder_out: paddle.Tensor, ) -> paddle.Tensor: + """ Export interface for c++ call, forward decoder with multiple + hypothesis from ctc prefix beam search and one encoder output + Args: + hyps (paddle.Tensor): hyps from ctc prefix beam search, already + pad sos at the begining, (B, T) + hyps_lens (paddle.Tensor): length of each hyp in hyps, (B) + encoder_out (paddle.Tensor): corresponding encoder output, (B=1, T, D) + Returns: + paddle.Tensor: decoder output, (B, L) + """ + assert encoder_out.shape[0] == 1 + num_hyps = hyps.shape[0] + assert hyps_lens.shape[0] == num_hyps + encoder_out = encoder_out.repeat(num_hyps, 1, 1) + # (B, 1, T) + encoder_mask = paddle.ones( + [num_hyps, 1, encoder_out.shape[1]], dtype=paddle.bool) + # (num_hyps, max_hyps_len, vocab_size) + decoder_out, _ = self.decoder(encoder_out, encoder_mask, hyps, + hyps_lens) + decoder_out = paddle.nn.functional.log_softmax(decoder_out, axis=-1) + return decoder_out + + @paddle.no_grad() + def decode(self, + feats: paddle.Tensor, + feats_lengths: paddle.Tensor, + text_feature: Dict[str, int], + decoding_method: str, + beam_size: int, + ctc_weight: float=0.0, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False): + """u2 decoding. + + Args: + feats (Tensor): audio features, (B, T, D) + feats_lengths (Tensor): (B) + text_feature (TextFeaturizer): text feature object. + decoding_method (str): decoding mode, e.g. + 'attention', 'ctc_greedy_search', + 'ctc_prefix_beam_search', 'attention_rescoring' + beam_size (int): beam size for search + ctc_weight (float, optional): ctc weight for attention rescoring decode mode. Defaults to 0.0. + decoding_chunk_size (int, optional): decoding chunk size. Defaults to -1. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here. + num_decoding_left_chunks (int, optional): + number of left chunks for decoding. Defaults to -1. + simulate_streaming (bool, optional): simulate streaming inference. Defaults to False. + + Raises: + ValueError: when not support decoding_method. + + Returns: + List[List[int]]: transcripts. + """ + batch_size = feats.shape[0] + if decoding_method in ['ctc_prefix_beam_search', + 'attention_rescoring'] and batch_size > 1: + logger.fatal( + f'decoding mode {decoding_method} must be running with batch_size == 1' + ) + sys.exit(1) + + if decoding_method == 'attention': + hyps = self.recognize( + feats, + feats_lengths, + beam_size=beam_size, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) + hyps = [hyp.tolist() for hyp in hyps] + elif decoding_method == 'ctc_greedy_search': + hyps = self.ctc_greedy_search( + feats, + feats_lengths, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) + # ctc_prefix_beam_search and attention_rescoring only return one + # result in List[int], change it to List[List[int]] for compatible + # with other batch decoding mode + elif decoding_method == 'ctc_prefix_beam_search': + assert feats.shape[0] == 1 + hyp = self.ctc_prefix_beam_search( + feats, + feats_lengths, + beam_size, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) + hyps = [hyp] + elif decoding_method == 'attention_rescoring': + assert feats.shape[0] == 1 + hyp = self.attention_rescoring( + feats, + feats_lengths, + beam_size, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + ctc_weight=ctc_weight, + simulate_streaming=simulate_streaming) + hyps = [hyp] + else: + raise ValueError(f"Not support decoding method: {decoding_method}") + + res = [text_feature.defeaturize(hyp) for hyp in hyps] + res_tokenids = [hyp for hyp in hyps] + return res, res_tokenids + + +class U2DecodeModel(U2BaseModel): + def scorers(self): + """Scorers.""" + return dict( + decoder=self.decoder, ctc=CTCPrefixScorer(self.ctc, self.eos)) + + def encode(self, x): + """Encode acoustic features. + + :param ndarray x: source acoustic feature (T, D) + :return: encoder outputs + :rtype: paddle.Tensor + """ + self.eval() + x = paddle.to_tensor(x).unsqueeze(0) + ilen = x.size(1) + enc_output, _ = self._forward_encoder(x, ilen) + return enc_output.squeeze(0) + + +class U2Model(U2DecodeModel): + def __init__(self, configs: dict): + model_conf = configs.get('model_conf', dict()) + init_type = model_conf.get("init_type", None) + with DefaultInitializerContext(init_type): + vocab_size, encoder, decoder, ctc = U2Model._init_from_config( + configs) + + super().__init__( + vocab_size=vocab_size, + encoder=encoder, + decoder=decoder, + ctc=ctc, + **model_conf) + + @classmethod + def _init_from_config(cls, configs: dict): + """init sub module for model. + + Args: + configs (dict): config dict. + + Raises: + ValueError: raise when using not support encoder type. + + Returns: + int, nn.Layer, nn.Layer, nn.Layer: vocab size, encoder, decoder, ctc + """ + # cmvn + if 'cmvn_file' in configs and configs['cmvn_file']: + mean, istd = load_cmvn(configs['cmvn_file'], + configs['cmvn_file_type']) + global_cmvn = GlobalCMVN( + paddle.to_tensor(mean, dtype=paddle.float), + paddle.to_tensor(istd, dtype=paddle.float)) + else: + global_cmvn = None + + # input & output dim + input_dim = configs['input_dim'] + vocab_size = configs['output_dim'] + assert input_dim != 0, input_dim + assert vocab_size != 0, vocab_size + + # encoder + encoder_type = configs.get('encoder', 'transformer') + logger.info(f"U2 Encoder type: {encoder_type}") + if encoder_type == 'transformer': + encoder = TransformerEncoder( + input_dim, global_cmvn=global_cmvn, **configs['encoder_conf']) + elif encoder_type == 'conformer': + encoder = ConformerEncoder( + input_dim, global_cmvn=global_cmvn, **configs['encoder_conf']) + else: + raise ValueError(f"not support encoder type:{encoder_type}") + + # decoder + decoder = TransformerDecoder(vocab_size, + encoder.output_size(), + **configs['decoder_conf']) + + # ctc decoder and ctc loss + model_conf = configs.get('model_conf', dict()) + dropout_rate = model_conf.get('ctc_dropout_rate', 0.0) + grad_norm_type = model_conf.get('ctc_grad_norm_type', None) + ctc = CTCDecoderBase( + odim=vocab_size, + enc_n_units=encoder.output_size(), + blank_id=0, + dropout_rate=dropout_rate, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=grad_norm_type) + + return vocab_size, encoder, decoder, ctc + + @classmethod + def from_config(cls, configs: dict): + """init model. + + Args: + configs (dict): config dict. + + Raises: + ValueError: raise when using not support encoder type. + + Returns: + nn.Layer: U2Model + """ + model = cls(configs) + return model + + @classmethod + def from_pretrained(cls, dataloader, config, checkpoint_path): + """Build a DeepSpeech2Model model from a pretrained model. + + Args: + dataloader (paddle.io.DataLoader): not used. + config (yacs.config.CfgNode): model configs + checkpoint_path (Path or str): the path of pretrained model checkpoint, without extension name + + Returns: + DeepSpeech2Model: The model built from pretrained result. + """ + with UpdateConfig(config): + config.input_dim = dataloader.feat_dim + config.output_dim = dataloader.vocab_size + + model = cls.from_config(config) + + if checkpoint_path: + infos = checkpoint.Checkpoint().load_parameters( + model, checkpoint_path=checkpoint_path) + logger.info(f"checkpoint info: {infos}") + layer_tools.summary(model) + return model + + +class U2InferModel(U2Model): + def __init__(self, configs: dict): + super().__init__(configs) + + def forward(self, + feats, + feats_lengths, + decoding_chunk_size=-1, + num_decoding_left_chunks=-1, + simulate_streaming=False): + """export model function + + Args: + feats (Tensor): [B, T, D] + feats_lengths (Tensor): [B] + + Returns: + List[List[int]]: best path result + """ + return self.ctc_greedy_search( + feats, + feats_lengths, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) diff --git a/ernie-sat/paddlespeech/s2t/models/u2/updater.py b/ernie-sat/paddlespeech/s2t/models/u2/updater.py new file mode 100644 index 0000000..c59090a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/u2/updater.py @@ -0,0 +1,150 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +from contextlib import nullcontext + +import paddle +from paddle import distributed as dist + +from paddlespeech.s2t.training.extensions.evaluator import StandardEvaluator +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.training.updaters.standard_updater import StandardUpdater +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + + +class U2Evaluator(StandardEvaluator): + def __init__(self, model, dataloader): + super().__init__(model, dataloader) + self.msg = "" + self.num_seen_utts = 0 + self.total_loss = 0.0 + + def evaluate_core(self, batch): + self.msg = "Valid: Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + + loss, attention_loss, ctc_loss = self.model(*batch[1:]) + if paddle.isfinite(loss): + num_utts = batch[1].shape[0] + self.num_seen_utts += num_utts + self.total_loss += float(loss) * num_utts + + losses_dict['loss'] = float(loss) + if attention_loss: + losses_dict['att_loss'] = float(attention_loss) + if ctc_loss: + losses_dict['ctc_loss'] = float(ctc_loss) + + for k, v in losses_dict.items(): + report("eval/" + k, v) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + logger.info(self.msg) + return self.total_loss, self.num_seen_utts + + +class U2Updater(StandardUpdater): + def __init__(self, + model, + optimizer, + scheduler, + dataloader, + init_state=None, + accum_grad=1, + **kwargs): + super().__init__( + model, optimizer, scheduler, dataloader, init_state=init_state) + self.accum_grad = accum_grad + self.forward_count = 0 + self.msg = "" + + def update_core(self, batch): + """One Step + + Args: + batch (List[Object]): utts, xs, xlens, ys, ylens + """ + losses_dict = {} + self.msg = "Rank: {}, ".format(dist.get_rank()) + + # forward + batch_size = batch[1].shape[0] + loss, attention_loss, ctc_loss = self.model(*batch[1:]) + # loss div by `batch_size * accum_grad` + loss /= self.accum_grad + + # loss backward + if (self.forward_count + 1) != self.accum_grad: + # Disable gradient synchronizations across DDP processes. + # Within this context, gradients will be accumulated on module + # variables, which will later be synchronized. + context = self.model.no_sync + else: + # Used for single gpu training and DDP gradient synchronization + # processes. + context = nullcontext + + with context(): + loss.backward() + layer_tools.print_grads(self.model, print_func=None) + + # loss info + losses_dict['loss'] = float(loss) * self.accum_grad + if attention_loss: + losses_dict['att_loss'] = float(attention_loss) + if ctc_loss: + losses_dict['ctc_loss'] = float(ctc_loss) + # report loss + for k, v in losses_dict.items(): + report("train/" + k, v) + # loss msg + self.msg += "batch size: {}, ".format(batch_size) + self.msg += "accum: {}, ".format(self.accum_grad) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + # Truncate the graph + loss.detach() + + # update parameters + self.forward_count += 1 + if self.forward_count != self.accum_grad: + return + self.forward_count = 0 + + self.optimizer.step() + self.optimizer.clear_grad() + self.scheduler.step() + + def update(self): + # model is default in train mode + + # training for a step is implemented here + with Timer("data time cost:{}"): + batch = self.read_batch() + with Timer("step time cost:{}"): + self.update_core(batch) + + # #iterations with accum_grad > 1 + # Ref.: https://github.com/espnet/espnet/issues/777 + if self.forward_count == 0: + self.state.iteration += 1 + if self.updates_per_epoch is not None: + if self.state.iteration % self.updates_per_epoch == 0: + self.state.epoch += 1 diff --git a/ernie-sat/paddlespeech/s2t/models/u2_st/__init__.py b/ernie-sat/paddlespeech/s2t/models/u2_st/__init__.py new file mode 100644 index 0000000..6b10b08 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/u2_st/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .u2_st import U2STInferModel +from .u2_st import U2STModel diff --git a/ernie-sat/paddlespeech/s2t/models/u2_st/u2_st.py b/ernie-sat/paddlespeech/s2t/models/u2_st/u2_st.py new file mode 100644 index 0000000..6447753 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/models/u2_st/u2_st.py @@ -0,0 +1,676 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""U2 ASR Model +Unified Streaming and Non-streaming Two-pass End-to-end Model for Speech Recognition +(https://arxiv.org/pdf/2012.05481.pdf) +""" +import time +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +import paddle +from paddle import jit +from paddle import nn + +from paddlespeech.s2t.frontend.utility import IGNORE_ID +from paddlespeech.s2t.frontend.utility import load_cmvn +from paddlespeech.s2t.modules.cmvn import GlobalCMVN +from paddlespeech.s2t.modules.ctc import CTCDecoderBase +from paddlespeech.s2t.modules.decoder import TransformerDecoder +from paddlespeech.s2t.modules.encoder import ConformerEncoder +from paddlespeech.s2t.modules.encoder import TransformerEncoder +from paddlespeech.s2t.modules.loss import LabelSmoothingLoss +from paddlespeech.s2t.modules.mask import subsequent_mask +from paddlespeech.s2t.utils import checkpoint +from paddlespeech.s2t.utils import layer_tools +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.tensor_utils import add_sos_eos +from paddlespeech.s2t.utils.tensor_utils import th_accuracy +from paddlespeech.s2t.utils.utility import UpdateConfig + +__all__ = ["U2STModel", "U2STInferModel"] + +logger = Log(__name__).getlog() + + +class U2STBaseModel(nn.Layer): + """CTC-Attention hybrid Encoder-Decoder model""" + + def __init__(self, + vocab_size: int, + encoder: TransformerEncoder, + st_decoder: TransformerDecoder, + decoder: TransformerDecoder=None, + ctc: CTCDecoderBase=None, + ctc_weight: float=0.0, + asr_weight: float=0.0, + ignore_id: int=IGNORE_ID, + lsm_weight: float=0.0, + length_normalized_loss: bool=False, + **kwargs): + assert 0.0 <= ctc_weight <= 1.0, ctc_weight + + super().__init__() + # note that eos is the same as sos (equivalent ID) + self.sos = vocab_size - 1 + self.eos = vocab_size - 1 + self.vocab_size = vocab_size + self.ignore_id = ignore_id + self.ctc_weight = ctc_weight + self.asr_weight = asr_weight + + self.encoder = encoder + self.st_decoder = st_decoder + self.decoder = decoder + self.ctc = ctc + self.criterion_att = LabelSmoothingLoss( + size=vocab_size, + padding_idx=ignore_id, + smoothing=lsm_weight, + normalize_length=length_normalized_loss, ) + + def forward( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + text: paddle.Tensor, + text_lengths: paddle.Tensor, + asr_text: paddle.Tensor=None, + asr_text_lengths: paddle.Tensor=None, + ) -> Tuple[Optional[paddle.Tensor], Optional[paddle.Tensor], Optional[ + paddle.Tensor]]: + """Frontend + Encoder + Decoder + Calc loss + Args: + speech: (Batch, Length, ...) + speech_lengths: (Batch, ) + text: (Batch, Length) + text_lengths: (Batch,) + Returns: + total_loss, attention_loss, ctc_loss + """ + assert text_lengths.dim() == 1, text_lengths.shape + # Check that batch_size is unified + assert (speech.shape[0] == speech_lengths.shape[0] == text.shape[0] == + text_lengths.shape[0]), (speech.shape, speech_lengths.shape, + text.shape, text_lengths.shape) + # 1. Encoder + start = time.time() + encoder_out, encoder_mask = self.encoder(speech, speech_lengths) + encoder_time = time.time() - start + #logger.debug(f"encoder time: {encoder_time}") + #TODO(Hui Zhang): sum not support bool type + #encoder_out_lens = encoder_mask.squeeze(1).sum(1) #[B, 1, T] -> [B] + encoder_out_lens = encoder_mask.squeeze(1).cast(paddle.int64).sum( + 1) #[B, 1, T] -> [B] + + # 2a. ST-decoder branch + start = time.time() + loss_st, acc_st = self._calc_st_loss(encoder_out, encoder_mask, text, + text_lengths) + decoder_time = time.time() - start + + loss_asr_att = None + loss_asr_ctc = None + # 2b. ASR Attention-decoder branch + if self.asr_weight > 0.: + if self.ctc_weight != 1.0: + start = time.time() + loss_asr_att, acc_att = self._calc_att_loss( + encoder_out, encoder_mask, asr_text, asr_text_lengths) + decoder_time = time.time() - start + + # 2c. CTC branch + if self.ctc_weight != 0.0: + start = time.time() + loss_asr_ctc = self.ctc(encoder_out, encoder_out_lens, asr_text, + asr_text_lengths) + ctc_time = time.time() - start + + if loss_asr_ctc is None: + loss_asr = loss_asr_att + elif loss_asr_att is None: + loss_asr = loss_asr_ctc + else: + loss_asr = self.ctc_weight * loss_asr_ctc + (1 - self.ctc_weight + ) * loss_asr_att + loss = self.asr_weight * loss_asr + (1 - self.asr_weight) * loss_st + else: + loss = loss_st + return loss, loss_st, loss_asr_att, loss_asr_ctc + + def _calc_st_loss( + self, + encoder_out: paddle.Tensor, + encoder_mask: paddle.Tensor, + ys_pad: paddle.Tensor, + ys_pad_lens: paddle.Tensor, ) -> Tuple[paddle.Tensor, float]: + """Calc attention loss. + + Args: + encoder_out (paddle.Tensor): [B, Tmax, D] + encoder_mask (paddle.Tensor): [B, 1, Tmax] + ys_pad (paddle.Tensor): [B, Umax] + ys_pad_lens (paddle.Tensor): [B] + + Returns: + Tuple[paddle.Tensor, float]: attention_loss, accuracy rate + """ + ys_in_pad, ys_out_pad = add_sos_eos(ys_pad, self.sos, self.eos, + self.ignore_id) + ys_in_lens = ys_pad_lens + 1 + + # 1. Forward decoder + decoder_out, _ = self.st_decoder(encoder_out, encoder_mask, ys_in_pad, + ys_in_lens) + + # 2. Compute attention loss + loss_att = self.criterion_att(decoder_out, ys_out_pad) + acc_att = th_accuracy( + decoder_out.view(-1, self.vocab_size), + ys_out_pad, + ignore_label=self.ignore_id, ) + return loss_att, acc_att + + def _calc_att_loss( + self, + encoder_out: paddle.Tensor, + encoder_mask: paddle.Tensor, + ys_pad: paddle.Tensor, + ys_pad_lens: paddle.Tensor, ) -> Tuple[paddle.Tensor, float]: + """Calc attention loss. + + Args: + encoder_out (paddle.Tensor): [B, Tmax, D] + encoder_mask (paddle.Tensor): [B, 1, Tmax] + ys_pad (paddle.Tensor): [B, Umax] + ys_pad_lens (paddle.Tensor): [B] + + Returns: + Tuple[paddle.Tensor, float]: attention_loss, accuracy rate + """ + ys_in_pad, ys_out_pad = add_sos_eos(ys_pad, self.sos, self.eos, + self.ignore_id) + ys_in_lens = ys_pad_lens + 1 + + # 1. Forward decoder + decoder_out, _ = self.decoder(encoder_out, encoder_mask, ys_in_pad, + ys_in_lens) + + # 2. Compute attention loss + loss_att = self.criterion_att(decoder_out, ys_out_pad) + acc_att = th_accuracy( + decoder_out.view(-1, self.vocab_size), + ys_out_pad, + ignore_label=self.ignore_id, ) + return loss_att, acc_att + + def _forward_encoder( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Encoder pass. + + Args: + speech (paddle.Tensor): [B, Tmax, D] + speech_lengths (paddle.Tensor): [B] + decoding_chunk_size (int, optional): chuck size. Defaults to -1. + num_decoding_left_chunks (int, optional): nums chunks. Defaults to -1. + simulate_streaming (bool, optional): streaming or not. Defaults to False. + + Returns: + Tuple[paddle.Tensor, paddle.Tensor]: + encoder hiddens (B, Tmax, D), + encoder hiddens mask (B, 1, Tmax). + """ + # Let's assume B = batch_size + # 1. Encoder + if simulate_streaming and decoding_chunk_size > 0: + encoder_out, encoder_mask = self.encoder.forward_chunk_by_chunk( + speech, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks + ) # (B, maxlen, encoder_dim) + else: + encoder_out, encoder_mask = self.encoder( + speech, + speech_lengths, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks + ) # (B, maxlen, encoder_dim) + return encoder_out, encoder_mask + + def translate( + self, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + beam_size: int=10, + word_reward: float=0.0, + maxlenratio: float=0.5, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False, ) -> paddle.Tensor: + """ Apply beam search on attention decoder with length penalty + Args: + speech (paddle.Tensor): (batch, max_len, feat_dim) + speech_length (paddle.Tensor): (batch, ) + beam_size (int): beam size for beam search + word_reward (float): word reward used in beam search + maxlenratio (float): max length ratio to bound the length of translated text + decoding_chunk_size (int): decoding chunk for dynamic chunk + trained model. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here + simulate_streaming (bool): whether do encoder forward in a + streaming fashion + Returns: + paddle.Tensor: decoding result, (batch, max_result_len) + """ + assert speech.shape[0] == speech_lengths.shape[0] + assert decoding_chunk_size != 0 + assert speech.shape[0] == 1 + device = speech.place + + # Let's assume B = batch_size and N = beam_size + # 1. Encoder and init hypothesis + encoder_out, encoder_mask = self._forward_encoder( + speech, speech_lengths, decoding_chunk_size, + num_decoding_left_chunks, + simulate_streaming) # (B, maxlen, encoder_dim) + + maxlen = max(int(encoder_out.shape[1] * maxlenratio), 5) + + hyp = {"score": 0.0, "yseq": [self.sos], "cache": None} + hyps = [hyp] + ended_hyps = [] + cur_best_score = -float("inf") + cache = None + + # 2. Decoder forward step by step + for i in range(1, maxlen + 1): + ys = paddle.ones((len(hyps), i), dtype=paddle.long) + + if hyps[0]["cache"] is not None: + cache = [ + paddle.ones( + (len(hyps), i - 1, hyp_cache.shape[-1]), + dtype=paddle.float32) for hyp_cache in hyps[0]["cache"] + ] + for j, hyp in enumerate(hyps): + ys[j, :] = paddle.to_tensor(hyp["yseq"]) + if hyps[0]["cache"] is not None: + for k in range(len(cache)): + cache[k][j] = hyps[j]["cache"][k] + ys_mask = subsequent_mask(i).unsqueeze(0).to(device) + + logp, cache = self.st_decoder.forward_one_step( + encoder_out.repeat(len(hyps), 1, 1), + encoder_mask.repeat(len(hyps), 1, 1), ys, ys_mask, cache) + + hyps_best_kept = [] + for j, hyp in enumerate(hyps): + top_k_logp, top_k_index = logp[j:j + 1].topk(beam_size) + + for b in range(beam_size): + new_hyp = {} + new_hyp["score"] = hyp["score"] + float(top_k_logp[0, b]) + new_hyp["yseq"] = [0] * (1 + len(hyp["yseq"])) + new_hyp["yseq"][:len(hyp["yseq"])] = hyp["yseq"] + new_hyp["yseq"][len(hyp["yseq"])] = int(top_k_index[0, b]) + new_hyp["cache"] = [cache_[j] for cache_ in cache] + # will be (2 x beam) hyps at most + hyps_best_kept.append(new_hyp) + + hyps_best_kept = sorted( + hyps_best_kept, key=lambda x: -x["score"])[:beam_size] + + # sort and get nbest + hyps = hyps_best_kept + if i == maxlen: + for hyp in hyps: + hyp["yseq"].append(self.eos) + + # finalize the ended hypotheses with word reward (by length) + remained_hyps = [] + for hyp in hyps: + if hyp["yseq"][-1] == self.eos: + hyp["score"] += (i - 1) * word_reward + cur_best_score = max(cur_best_score, hyp["score"]) + ended_hyps.append(hyp) + else: + # stop while guarantee the optimality + if hyp["score"] + maxlen * word_reward > cur_best_score: + remained_hyps.append(hyp) + + # stop predition when there is no unended hypothesis + if not remained_hyps: + break + hyps = remained_hyps + + # 3. Select best of best + best_hyp = max(ended_hyps, key=lambda x: x["score"]) + + return paddle.to_tensor([best_hyp["yseq"][1:]]) + + # @jit.to_static + def subsampling_rate(self) -> int: + """ Export interface for c++ call, return subsampling_rate of the + model + """ + return self.encoder.embed.subsampling_rate + + # @jit.to_static + def right_context(self) -> int: + """ Export interface for c++ call, return right_context of the model + """ + return self.encoder.embed.right_context + + # @jit.to_static + def sos_symbol(self) -> int: + """ Export interface for c++ call, return sos symbol id of the model + """ + return self.sos + + # @jit.to_static + def eos_symbol(self) -> int: + """ Export interface for c++ call, return eos symbol id of the model + """ + return self.eos + + @jit.to_static + def forward_encoder_chunk( + self, + xs: paddle.Tensor, + offset: int, + required_cache_size: int, + subsampling_cache: Optional[paddle.Tensor]=None, + elayers_output_cache: Optional[List[paddle.Tensor]]=None, + conformer_cnn_cache: Optional[List[paddle.Tensor]]=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, List[paddle.Tensor], List[ + paddle.Tensor]]: + """ Export interface for c++ call, give input chunk xs, and return + output from time 0 to current chunk. + Args: + xs (paddle.Tensor): chunk input + subsampling_cache (Optional[paddle.Tensor]): subsampling cache + elayers_output_cache (Optional[List[paddle.Tensor]]): + transformer/conformer encoder layers output cache + conformer_cnn_cache (Optional[List[paddle.Tensor]]): conformer + cnn cache + Returns: + paddle.Tensor: output, it ranges from time 0 to current chunk. + paddle.Tensor: subsampling cache + List[paddle.Tensor]: attention cache + List[paddle.Tensor]: conformer cnn cache + """ + return self.encoder.forward_chunk( + xs, offset, required_cache_size, subsampling_cache, + elayers_output_cache, conformer_cnn_cache) + + # @jit.to_static + def ctc_activation(self, xs: paddle.Tensor) -> paddle.Tensor: + """ Export interface for c++ call, apply linear transform and log + softmax before ctc + Args: + xs (paddle.Tensor): encoder output + Returns: + paddle.Tensor: activation before ctc + """ + return self.ctc.log_softmax(xs) + + @jit.to_static + def forward_attention_decoder( + self, + hyps: paddle.Tensor, + hyps_lens: paddle.Tensor, + encoder_out: paddle.Tensor, ) -> paddle.Tensor: + """ Export interface for c++ call, forward decoder with multiple + hypothesis from ctc prefix beam search and one encoder output + Args: + hyps (paddle.Tensor): hyps from ctc prefix beam search, already + pad sos at the begining, (B, T) + hyps_lens (paddle.Tensor): length of each hyp in hyps, (B) + encoder_out (paddle.Tensor): corresponding encoder output, (B=1, T, D) + Returns: + paddle.Tensor: decoder output, (B, L) + """ + assert encoder_out.shape[0] == 1 + num_hyps = hyps.shape[0] + assert hyps_lens.shape[0] == num_hyps + encoder_out = encoder_out.repeat(num_hyps, 1, 1) + # (B, 1, T) + encoder_mask = paddle.ones( + [num_hyps, 1, encoder_out.shape[1]], dtype=paddle.bool) + # (num_hyps, max_hyps_len, vocab_size) + decoder_out, _ = self.decoder(encoder_out, encoder_mask, hyps, + hyps_lens) + decoder_out = paddle.nn.functional.log_softmax(decoder_out, dim=-1) + return decoder_out + + @paddle.no_grad() + def decode(self, + feats: paddle.Tensor, + feats_lengths: paddle.Tensor, + text_feature: Dict[str, int], + decoding_method: str, + beam_size: int, + word_reward: float=0.0, + maxlenratio: float=0.5, + decoding_chunk_size: int=-1, + num_decoding_left_chunks: int=-1, + simulate_streaming: bool=False): + """u2 decoding. + + Args: + feats (Tensor): audio features, (B, T, D) + feats_lengths (Tensor): (B) + text_feature (TextFeaturizer): text feature object. + decoding_method (str): decoding mode, e.g. + 'fullsentence', + 'simultaneous' + beam_size (int): beam size for search + decoding_chunk_size (int, optional): decoding chunk size. Defaults to -1. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + 0: used for training, it's prohibited here. + num_decoding_left_chunks (int, optional): + number of left chunks for decoding. Defaults to -1. + simulate_streaming (bool, optional): simulate streaming inference. Defaults to False. + + Raises: + ValueError: when not support decoding_method. + + Returns: + List[List[int]]: transcripts. + """ + batch_size = feats.shape[0] + + if decoding_method == 'fullsentence': + hyps = self.translate( + feats, + feats_lengths, + beam_size=beam_size, + word_reward=word_reward, + maxlenratio=maxlenratio, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) + hyps = [hyp.tolist() for hyp in hyps] + else: + raise ValueError(f"Not support decoding method: {decoding_method}") + + res = [text_feature.defeaturize(hyp) for hyp in hyps] + return res + + +class U2STModel(U2STBaseModel): + def __init__(self, configs: dict): + vocab_size, encoder, decoder = U2STModel._init_from_config(configs) + + if isinstance(decoder, Tuple): + st_decoder, asr_decoder, ctc = decoder + super().__init__( + vocab_size=vocab_size, + encoder=encoder, + st_decoder=st_decoder, + decoder=asr_decoder, + ctc=ctc, + **configs['model_conf']) + else: + super().__init__( + vocab_size=vocab_size, + encoder=encoder, + st_decoder=decoder, + **configs['model_conf']) + + @classmethod + def _init_from_config(cls, configs: dict): + """init sub module for model. + + Args: + configs (dict): config dict. + + Raises: + ValueError: raise when using not support encoder type. + + Returns: + int, nn.Layer, nn.Layer, nn.Layer: vocab size, encoder, decoder, ctc + """ + if configs['cmvn_file'] is not None: + mean, istd = load_cmvn(configs['cmvn_file'], + configs['cmvn_file_type']) + global_cmvn = GlobalCMVN( + paddle.to_tensor(mean, dtype=paddle.float), + paddle.to_tensor(istd, dtype=paddle.float)) + else: + global_cmvn = None + + input_dim = configs['input_dim'] + vocab_size = configs['output_dim'] + assert input_dim != 0, input_dim + assert vocab_size != 0, vocab_size + + encoder_type = configs.get('encoder', 'transformer') + logger.info(f"U2 Encoder type: {encoder_type}") + if encoder_type == 'transformer': + encoder = TransformerEncoder( + input_dim, global_cmvn=global_cmvn, **configs['encoder_conf']) + elif encoder_type == 'conformer': + encoder = ConformerEncoder( + input_dim, global_cmvn=global_cmvn, **configs['encoder_conf']) + else: + raise ValueError(f"not support encoder type:{encoder_type}") + + st_decoder = TransformerDecoder(vocab_size, + encoder.output_size(), + **configs['decoder_conf']) + + asr_weight = configs['model_conf']['asr_weight'] + logger.info(f"ASR Joint Training Weight: {asr_weight}") + + if asr_weight > 0.: + decoder = TransformerDecoder(vocab_size, + encoder.output_size(), + **configs['decoder_conf']) + # ctc decoder and ctc loss + model_conf = configs['model_conf'] + dropout_rate = model_conf.get('ctc_dropout_rate', 0.0) + grad_norm_type = model_conf.get('ctc_grad_norm_type', None) + ctc = CTCDecoderBase( + odim=vocab_size, + enc_n_units=encoder.output_size(), + blank_id=0, + dropout_rate=dropout_rate, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=grad_norm_type) + + return vocab_size, encoder, (st_decoder, decoder, ctc) + else: + return vocab_size, encoder, st_decoder + + @classmethod + def from_config(cls, configs: dict): + """init model. + + Args: + configs (dict): config dict. + + Raises: + ValueError: raise when using not support encoder type. + + Returns: + nn.Layer: U2STModel + """ + model = cls(configs) + return model + + @classmethod + def from_pretrained(cls, dataloader, config, checkpoint_path): + """Build a DeepSpeech2Model model from a pretrained model. + + Args: + dataloader (paddle.io.DataLoader): not used. + config (yacs.config.CfgNode): model configs + checkpoint_path (Path or str): the path of pretrained model checkpoint, without extension name + + Returns: + DeepSpeech2Model: The model built from pretrained result. + """ + with UpdateConfig(config): + config.input_dim = dataloader.collate_fn.feature_size + config.output_dim = dataloader.collate_fn.vocab_size + + model = cls.from_config(config) + + if checkpoint_path: + infos = checkpoint.load_parameters( + model, checkpoint_path=checkpoint_path) + logger.info(f"checkpoint info: {infos}") + layer_tools.summary(model) + return model + + +class U2STInferModel(U2STModel): + def __init__(self, configs: dict): + super().__init__(configs) + + def forward(self, + feats, + feats_lengths, + decoding_chunk_size=-1, + num_decoding_left_chunks=-1, + simulate_streaming=False): + """export model function + + Args: + feats (Tensor): [B, T, D] + feats_lengths (Tensor): [B] + + Returns: + List[List[int]]: best path result + """ + return self.translate( + feats, + feats_lengths, + decoding_chunk_size=decoding_chunk_size, + num_decoding_left_chunks=num_decoding_left_chunks, + simulate_streaming=simulate_streaming) diff --git a/ernie-sat/paddlespeech/s2t/modules/__init__.py b/ernie-sat/paddlespeech/s2t/modules/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/modules/activation.py b/ernie-sat/paddlespeech/s2t/modules/activation.py new file mode 100644 index 0000000..2f387b0 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/activation.py @@ -0,0 +1,164 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import OrderedDict + +import paddle +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.s2t.modules.align import Conv2D +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["get_activation", "brelu", "LinearGLUBlock", "ConvGLUBlock", "GLU"] + + +def brelu(x, t_min=0.0, t_max=24.0, name=None): + # paddle.to_tensor is dygraph_only can not work under JIT + t_min = paddle.full(shape=[1], fill_value=t_min, dtype='float32') + t_max = paddle.full(shape=[1], fill_value=t_max, dtype='float32') + return x.maximum(t_min).minimum(t_max) + + +class GLU(nn.Layer): + """Gated Linear Units (GLU) Layer""" + + def __init__(self, dim: int=-1): + super().__init__() + self.dim = dim + + def forward(self, xs): + return F.glu(xs, axis=self.dim) + + +class LinearGLUBlock(nn.Layer): + """A linear Gated Linear Units (GLU) block.""" + + def __init__(self, idim: int): + """ GLU. + Args: + idim (int): input and output dimension + """ + super().__init__() + self.fc = Linear(idim, idim * 2) + + def forward(self, xs): + return glu(self.fc(xs), dim=-1) + + +class ConvGLUBlock(nn.Layer): + def __init__(self, kernel_size, in_ch, out_ch, bottlececk_dim=0, + dropout=0.): + """A convolutional Gated Linear Units (GLU) block. + + Args: + kernel_size (int): kernel size + in_ch (int): number of input channels + out_ch (int): number of output channels + bottlececk_dim (int): dimension of the bottleneck layers for computational efficiency. Defaults to 0. + dropout (float): dropout probability. Defaults to 0.. + """ + + super().__init__() + + self.conv_residual = None + if in_ch != out_ch: + self.conv_residual = nn.utils.weight_norm( + Conv2D( + in_channels=in_ch, out_channels=out_ch, kernel_size=(1, 1)), + name='weight', + dim=0) + self.dropout_residual = nn.Dropout(p=dropout) + + self.pad_left = nn.Pad2d((0, 0, kernel_size - 1, 0), 0) + + layers = OrderedDict() + if bottlececk_dim == 0: + layers['conv'] = nn.utils.weight_norm( + Conv2D( + in_channels=in_ch, + out_channels=out_ch * 2, + kernel_size=(kernel_size, 1)), + name='weight', + dim=0) + # TODO(hirofumi0810): padding? + layers['dropout'] = nn.Dropout(p=dropout) + layers['glu'] = GLU() + + elif bottlececk_dim > 0: + layers['conv_in'] = nn.utils.weight_norm( + nn.Conv2D( + in_channels=in_ch, + out_channels=bottlececk_dim, + kernel_size=(1, 1)), + name='weight', + dim=0) + layers['dropout_in'] = nn.Dropout(p=dropout) + layers['conv_bottleneck'] = nn.utils.weight_norm( + Conv2D( + in_channels=bottlececk_dim, + out_channels=bottlececk_dim, + kernel_size=(kernel_size, 1)), + name='weight', + dim=0) + layers['dropout'] = nn.Dropout(p=dropout) + layers['glu'] = GLU() + layers['conv_out'] = nn.utils.weight_norm( + Conv2D( + in_channels=bottlececk_dim, + out_channels=out_ch * 2, + kernel_size=(1, 1)), + name='weight', + dim=0) + layers['dropout_out'] = nn.Dropout(p=dropout) + + self.layers = nn.Sequential(layers) + + def forward(self, xs): + """Forward pass. + Args: + xs (FloatTensor): `[B, in_ch, T, feat_dim]` + Returns: + out (FloatTensor): `[B, out_ch, T, feat_dim]` + """ + residual = xs + if self.conv_residual is not None: + residual = self.dropout_residual(self.conv_residual(residual)) + xs = self.pad_left(xs) # `[B, embed_dim, T+kernel-1, 1]` + xs = self.layers(xs) # `[B, out_ch * 2, T ,1]` + xs = xs + residual + return xs + + +def get_activation(act): + """Return activation function.""" + # Lazy load to avoid unused import + activation_funcs = { + "hardshrink": paddle.nn.Hardshrink, + "hardswish": paddle.nn.Hardswish, + "hardtanh": paddle.nn.Hardtanh, + "tanh": paddle.nn.Tanh, + "relu": paddle.nn.ReLU, + "relu6": paddle.nn.ReLU6, + "leakyrelu": paddle.nn.LeakyReLU, + "selu": paddle.nn.SELU, + "swish": paddle.nn.Swish, + "gelu": paddle.nn.GELU, + "glu": GLU, + "elu": paddle.nn.ELU, + } + + return activation_funcs[act]() diff --git a/ernie-sat/paddlespeech/s2t/modules/align.py b/ernie-sat/paddlespeech/s2t/modules/align.py new file mode 100644 index 0000000..f889167 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/align.py @@ -0,0 +1,139 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + +from paddlespeech.s2t.modules.initializer import KaimingUniform +""" + To align the initializer between paddle and torch, + the API below are set defalut initializer with priority higger than global initializer. +""" +global_init_type = None + + +class LayerNorm(nn.LayerNorm): + def __init__(self, + normalized_shape, + epsilon=1e-05, + weight_attr=None, + bias_attr=None, + name=None): + if weight_attr is None: + weight_attr = paddle.ParamAttr( + initializer=nn.initializer.Constant(1.0)) + if bias_attr is None: + bias_attr = paddle.ParamAttr( + initializer=nn.initializer.Constant(0.0)) + super(LayerNorm, self).__init__(normalized_shape, epsilon, weight_attr, + bias_attr, name) + + +class BatchNorm1D(nn.BatchNorm1D): + def __init__(self, + num_features, + momentum=0.9, + epsilon=1e-05, + weight_attr=None, + bias_attr=None, + data_format='NCL', + name=None): + if weight_attr is None: + weight_attr = paddle.ParamAttr( + initializer=nn.initializer.Constant(1.0)) + if bias_attr is None: + bias_attr = paddle.ParamAttr( + initializer=nn.initializer.Constant(0.0)) + super(BatchNorm1D, + self).__init__(num_features, momentum, epsilon, weight_attr, + bias_attr, data_format, name) + + +class Embedding(nn.Embedding): + def __init__(self, + num_embeddings, + embedding_dim, + padding_idx=None, + sparse=False, + weight_attr=None, + name=None): + if weight_attr is None: + weight_attr = paddle.ParamAttr(initializer=nn.initializer.Normal()) + super(Embedding, self).__init__(num_embeddings, embedding_dim, + padding_idx, sparse, weight_attr, name) + + +class Linear(nn.Linear): + def __init__(self, + in_features, + out_features, + weight_attr=None, + bias_attr=None, + name=None): + if weight_attr is None: + if global_init_type == "kaiming_uniform": + weight_attr = paddle.ParamAttr(initializer=KaimingUniform()) + if bias_attr is None: + if global_init_type == "kaiming_uniform": + bias_attr = paddle.ParamAttr(initializer=KaimingUniform()) + super(Linear, self).__init__(in_features, out_features, weight_attr, + bias_attr, name) + + +class Conv1D(nn.Conv1D): + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + groups=1, + padding_mode='zeros', + weight_attr=None, + bias_attr=None, + data_format='NCL'): + if weight_attr is None: + if global_init_type == "kaiming_uniform": + print("set kaiming_uniform") + weight_attr = paddle.ParamAttr(initializer=KaimingUniform()) + if bias_attr is None: + if global_init_type == "kaiming_uniform": + bias_attr = paddle.ParamAttr(initializer=KaimingUniform()) + super(Conv1D, self).__init__( + in_channels, out_channels, kernel_size, stride, padding, dilation, + groups, padding_mode, weight_attr, bias_attr, data_format) + + +class Conv2D(nn.Conv2D): + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + dilation=1, + groups=1, + padding_mode='zeros', + weight_attr=None, + bias_attr=None, + data_format='NCHW'): + if weight_attr is None: + if global_init_type == "kaiming_uniform": + weight_attr = paddle.ParamAttr(initializer=KaimingUniform()) + if bias_attr is None: + if global_init_type == "kaiming_uniform": + bias_attr = paddle.ParamAttr(initializer=KaimingUniform()) + super(Conv2D, self).__init__( + in_channels, out_channels, kernel_size, stride, padding, dilation, + groups, padding_mode, weight_attr, bias_attr, data_format) diff --git a/ernie-sat/paddlespeech/s2t/modules/attention.py b/ernie-sat/paddlespeech/s2t/modules/attention.py new file mode 100644 index 0000000..438efd2 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/attention.py @@ -0,0 +1,237 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Multi-Head Attention layer definition.""" +import math +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn +from paddle.nn import initializer as I + +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["MultiHeadedAttention", "RelPositionMultiHeadedAttention"] + +# Relative Positional Encodings +# https://www.jianshu.com/p/c0608efcc26f +# https://zhuanlan.zhihu.com/p/344604604 + + +class MultiHeadedAttention(nn.Layer): + """Multi-Head Attention layer.""" + + def __init__(self, n_head: int, n_feat: int, dropout_rate: float): + """Construct an MultiHeadedAttention object. + Args: + n_head (int): The number of heads. + n_feat (int): The number of features. + dropout_rate (float): Dropout rate. + """ + super().__init__() + assert n_feat % n_head == 0 + # We assume d_v always equals d_k + self.d_k = n_feat // n_head + self.h = n_head + self.linear_q = Linear(n_feat, n_feat) + self.linear_k = Linear(n_feat, n_feat) + self.linear_v = Linear(n_feat, n_feat) + self.linear_out = Linear(n_feat, n_feat) + self.dropout = nn.Dropout(p=dropout_rate) + + def forward_qkv(self, + query: paddle.Tensor, + key: paddle.Tensor, + value: paddle.Tensor + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Transform query, key and value. + Args: + query (paddle.Tensor): Query tensor (#batch, time1, size). + key (paddle.Tensor): Key tensor (#batch, time2, size). + value (paddle.Tensor): Value tensor (#batch, time2, size). + Returns: + paddle.Tensor: Transformed query tensor, size + (#batch, n_head, time1, d_k). + paddle.Tensor: Transformed key tensor, size + (#batch, n_head, time2, d_k). + paddle.Tensor: Transformed value tensor, size + (#batch, n_head, time2, d_k). + """ + n_batch = query.shape[0] + q = self.linear_q(query).view(n_batch, -1, self.h, self.d_k) + k = self.linear_k(key).view(n_batch, -1, self.h, self.d_k) + v = self.linear_v(value).view(n_batch, -1, self.h, self.d_k) + q = q.transpose([0, 2, 1, 3]) # (batch, head, time1, d_k) + k = k.transpose([0, 2, 1, 3]) # (batch, head, time2, d_k) + v = v.transpose([0, 2, 1, 3]) # (batch, head, time2, d_k) + + return q, k, v + + def forward_attention(self, + value: paddle.Tensor, + scores: paddle.Tensor, + mask: Optional[paddle.Tensor]) -> paddle.Tensor: + """Compute attention context vector. + Args: + value (paddle.Tensor): Transformed value, size + (#batch, n_head, time2, d_k). + scores (paddle.Tensor): Attention score, size + (#batch, n_head, time1, time2). + mask (paddle.Tensor): Mask, size (#batch, 1, time2) or + (#batch, time1, time2). + Returns: + paddle.Tensor: Transformed value weighted + by the attention score, (#batch, time1, d_model). + """ + n_batch = value.shape[0] + if mask is not None: + mask = mask.unsqueeze(1).eq(0) # (batch, 1, *, time2) + scores = scores.masked_fill(mask, -float('inf')) + attn = paddle.softmax( + scores, axis=-1).masked_fill(mask, + 0.0) # (batch, head, time1, time2) + else: + attn = paddle.softmax( + scores, axis=-1) # (batch, head, time1, time2) + + p_attn = self.dropout(attn) + x = paddle.matmul(p_attn, value) # (batch, head, time1, d_k) + x = x.transpose([0, 2, 1, 3]).view(n_batch, -1, self.h * + self.d_k) # (batch, time1, d_model) + + return self.linear_out(x) # (batch, time1, d_model) + + def forward(self, + query: paddle.Tensor, + key: paddle.Tensor, + value: paddle.Tensor, + mask: Optional[paddle.Tensor]) -> paddle.Tensor: + """Compute scaled dot product attention. + Args: + query (torch.Tensor): Query tensor (#batch, time1, size). + key (torch.Tensor): Key tensor (#batch, time2, size). + value (torch.Tensor): Value tensor (#batch, time2, size). + mask (torch.Tensor): Mask tensor (#batch, 1, time2) or + (#batch, time1, time2). + Returns: + torch.Tensor: Output tensor (#batch, time1, d_model). + """ + q, k, v = self.forward_qkv(query, key, value) + scores = paddle.matmul(q, + k.transpose([0, 1, 3, 2])) / math.sqrt(self.d_k) + return self.forward_attention(v, scores, mask) + + +class RelPositionMultiHeadedAttention(MultiHeadedAttention): + """Multi-Head Attention layer with relative position encoding.""" + + def __init__(self, n_head, n_feat, dropout_rate): + """Construct an RelPositionMultiHeadedAttention object. + Paper: https://arxiv.org/abs/1901.02860 + Args: + n_head (int): The number of heads. + n_feat (int): The number of features. + dropout_rate (float): Dropout rate. + """ + super().__init__(n_head, n_feat, dropout_rate) + # linear transformation for positional encoding + self.linear_pos = Linear(n_feat, n_feat, bias_attr=False) + # these two learnable bias are used in matrix c and matrix d + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + #self.pos_bias_u = nn.Parameter(torch.Tensor(self.h, self.d_k)) + #self.pos_bias_v = nn.Parameter(torch.Tensor(self.h, self.d_k)) + #torch.nn.init.xavier_uniform_(self.pos_bias_u) + #torch.nn.init.xavier_uniform_(self.pos_bias_v) + pos_bias_u = self.create_parameter( + [self.h, self.d_k], default_initializer=I.XavierUniform()) + self.add_parameter('pos_bias_u', pos_bias_u) + pos_bias_v = self.create_parameter( + (self.h, self.d_k), default_initializer=I.XavierUniform()) + self.add_parameter('pos_bias_v', pos_bias_v) + + def rel_shift(self, x, zero_triu: bool=False): + """Compute relative positinal encoding. + Args: + x (paddle.Tensor): Input tensor (batch, head, time1, time1). + zero_triu (bool): If true, return the lower triangular part of + the matrix. + Returns: + paddle.Tensor: Output tensor. (batch, head, time1, time1) + """ + zero_pad = paddle.zeros( + (x.shape[0], x.shape[1], x.shape[2], 1), dtype=x.dtype) + x_padded = paddle.cat([zero_pad, x], dim=-1) + + x_padded = x_padded.view(x.shape[0], x.shape[1], x.shape[3] + 1, + x.shape[2]) + x = x_padded[:, :, 1:].view_as(x) # [B, H, T1, T1] + + if zero_triu: + ones = paddle.ones((x.shape[2], x.shape[3])) + x = x * paddle.tril(ones, x.shape[3] - x.shape[2])[None, None, :, :] + + return x + + def forward(self, + query: paddle.Tensor, + key: paddle.Tensor, + value: paddle.Tensor, + pos_emb: paddle.Tensor, + mask: Optional[paddle.Tensor]): + """Compute 'Scaled Dot Product Attention' with rel. positional encoding. + Args: + query (paddle.Tensor): Query tensor (#batch, time1, size). + key (paddle.Tensor): Key tensor (#batch, time2, size). + value (paddle.Tensor): Value tensor (#batch, time2, size). + pos_emb (paddle.Tensor): Positional embedding tensor + (#batch, time1, size). + mask (paddle.Tensor): Mask tensor (#batch, 1, time2) or + (#batch, time1, time2). + Returns: + paddle.Tensor: Output tensor (#batch, time1, d_model). + """ + q, k, v = self.forward_qkv(query, key, value) + q = q.transpose([0, 2, 1, 3]) # (batch, time1, head, d_k) + + n_batch_pos = pos_emb.shape[0] + p = self.linear_pos(pos_emb).view(n_batch_pos, -1, self.h, self.d_k) + p = p.transpose([0, 2, 1, 3]) # (batch, head, time1, d_k) + + # (batch, head, time1, d_k) + q_with_bias_u = (q + self.pos_bias_u).transpose([0, 2, 1, 3]) + # (batch, head, time1, d_k) + q_with_bias_v = (q + self.pos_bias_v).transpose([0, 2, 1, 3]) + + # compute attention score + # first compute matrix a and matrix c + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + # (batch, head, time1, time2) + matrix_ac = paddle.matmul(q_with_bias_u, k.transpose([0, 1, 3, 2])) + + # compute matrix b and matrix d + # (batch, head, time1, time2) + matrix_bd = paddle.matmul(q_with_bias_v, p.transpose([0, 1, 3, 2])) + # Remove rel_shift since it is useless in speech recognition, + # and it requires special attention for streaming. + # matrix_bd = self.rel_shift(matrix_bd) + + scores = (matrix_ac + matrix_bd) / math.sqrt( + self.d_k) # (batch, head, time1, time2) + + return self.forward_attention(v, scores, mask) diff --git a/ernie-sat/paddlespeech/s2t/modules/cmvn.py b/ernie-sat/paddlespeech/s2t/modules/cmvn.py new file mode 100644 index 0000000..67f71b6 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/cmvn.py @@ -0,0 +1,53 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +import paddle +from paddle import nn + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['GlobalCMVN'] + + +class GlobalCMVN(nn.Layer): + def __init__(self, + mean: paddle.Tensor, + istd: paddle.Tensor, + norm_var: bool=True): + """ + Args: + mean (paddle.Tensor): mean stats + istd (paddle.Tensor): inverse std, std which is 1.0 / std + """ + super().__init__() + assert mean.shape == istd.shape + self.norm_var = norm_var + # The buffer can be accessed from this module using self.mean + self.register_buffer("mean", mean) + self.register_buffer("istd", istd) + + def forward(self, x: paddle.Tensor): + """ + Args: + x (paddle.Tensor): (batch, max_len, feat_dim) + Returns: + (paddle.Tensor): normalized feature + """ + x = x - self.mean + if self.norm_var: + x = x * self.istd + return x diff --git a/ernie-sat/paddlespeech/s2t/modules/conformer_convolution.py b/ernie-sat/paddlespeech/s2t/modules/conformer_convolution.py new file mode 100644 index 0000000..89e6526 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/conformer_convolution.py @@ -0,0 +1,166 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""ConvolutionModule definition.""" +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.s2t.modules.align import BatchNorm1D +from paddlespeech.s2t.modules.align import Conv1D +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['ConvolutionModule'] + + +class ConvolutionModule(nn.Layer): + """ConvolutionModule in Conformer model.""" + + def __init__(self, + channels: int, + kernel_size: int=15, + activation: nn.Layer=nn.ReLU(), + norm: str="batch_norm", + causal: bool=False, + bias: bool=True): + """Construct an ConvolutionModule object. + Args: + channels (int): The number of channels of conv layers. + kernel_size (int): Kernel size of conv layers. + activation (nn.Layer): Activation Layer. + norm (str): Normalization type, 'batch_norm' or 'layer_norm' + causal (bool): Whether use causal convolution or not + bias (bool): Whether Conv with bias or not + """ + assert check_argument_types() + super().__init__() + self.pointwise_conv1 = Conv1D( + channels, + 2 * channels, + kernel_size=1, + stride=1, + padding=0, + bias_attr=None + if bias else False, # None for True, using bias as default config + ) + + # self.lorder is used to distinguish if it's a causal convolution, + # if self.lorder > 0: + # it's a causal convolution, the input will be padded with + # `self.lorder` frames on the left in forward (causal conv impl). + # else: it's a symmetrical convolution + if causal: + padding = 0 + self.lorder = kernel_size - 1 + else: + # kernel_size should be an odd number for none causal convolution + assert (kernel_size - 1) % 2 == 0 + padding = (kernel_size - 1) // 2 + self.lorder = 0 + + self.depthwise_conv = Conv1D( + channels, + channels, + kernel_size, + stride=1, + padding=padding, + groups=channels, + bias_attr=None + if bias else False, # None for True, using bias as default config + ) + + assert norm in ['batch_norm', 'layer_norm'] + if norm == "batch_norm": + self.use_layer_norm = False + self.norm = BatchNorm1D(channels) + else: + self.use_layer_norm = True + self.norm = LayerNorm(channels) + + self.pointwise_conv2 = Conv1D( + channels, + channels, + kernel_size=1, + stride=1, + padding=0, + bias_attr=None + if bias else False, # None for True, using bias as default config + ) + self.activation = activation + + def forward(self, + x: paddle.Tensor, + mask_pad: Optional[paddle.Tensor]=None, + cache: Optional[paddle.Tensor]=None + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Compute convolution module. + Args: + x (paddle.Tensor): Input tensor (#batch, time, channels). + mask_pad (paddle.Tensor): used for batch padding, (#batch, channels, time). + cache (paddle.Tensor): left context cache, it is only + used in causal convolution. (#batch, channels, time') + Returns: + paddle.Tensor: Output tensor (#batch, time, channels). + paddle.Tensor: Output cache tensor (#batch, channels, time') + """ + # exchange the temporal dimension and the feature dimension + x = x.transpose([0, 2, 1]) # [B, C, T] + + # mask batch padding + if mask_pad is not None: + x = x.masked_fill(mask_pad, 0.0) + + if self.lorder > 0: + if cache is None: + x = nn.functional.pad( + x, [self.lorder, 0], 'constant', 0.0, data_format='NCL') + else: + assert cache.shape[0] == x.shape[0] # B + assert cache.shape[1] == x.shape[1] # C + x = paddle.concat((cache, x), axis=2) + + assert (x.shape[2] > self.lorder) + new_cache = x[:, :, -self.lorder:] #[B, C, T] + else: + # It's better we just return None if no cache is requried, + # However, for JIT export, here we just fake one tensor instead of + # None. + new_cache = paddle.zeros([1], dtype=x.dtype) + + # GLU mechanism + x = self.pointwise_conv1(x) # (batch, 2*channel, dim) + x = nn.functional.glu(x, axis=1) # (batch, channel, dim) + + # 1D Depthwise Conv + x = self.depthwise_conv(x) + if self.use_layer_norm: + x = x.transpose([0, 2, 1]) # [B, T, C] + x = self.activation(self.norm(x)) + if self.use_layer_norm: + x = x.transpose([0, 2, 1]) # [B, C, T] + x = self.pointwise_conv2(x) + + # mask batch padding + if mask_pad is not None: + x = x.masked_fill(mask_pad, 0.0) + + x = x.transpose([0, 2, 1]) # [B, T, C] + return x, new_cache diff --git a/ernie-sat/paddlespeech/s2t/modules/crf.py b/ernie-sat/paddlespeech/s2t/modules/crf.py new file mode 100644 index 0000000..66f6b18 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/crf.py @@ -0,0 +1,370 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['CRF'] + + +class CRF(nn.Layer): + """ + Linear-chain Conditional Random Field (CRF). + + Args: + nb_labels (int): number of labels in your tagset, including special symbols. + bos_tag_id (int): integer representing the beginning of sentence symbol in + your tagset. + eos_tag_id (int): integer representing the end of sentence symbol in your tagset. + pad_tag_id (int, optional): integer representing the pad symbol in your tagset. + If None, the model will treat the PAD as a normal tag. Otherwise, the model + will apply constraints for PAD transitions. + batch_first (bool): Whether the first dimension represents the batch dimension. + """ + + def __init__(self, + nb_labels: int, + bos_tag_id: int, + eos_tag_id: int, + pad_tag_id: int=None, + batch_first: bool=True): + super().__init__() + + self.nb_labels = nb_labels + self.BOS_TAG_ID = bos_tag_id + self.EOS_TAG_ID = eos_tag_id + self.PAD_TAG_ID = pad_tag_id + self.batch_first = batch_first + + # initialize transitions from a random uniform distribution between -0.1 and 0.1 + self.transitions = self.create_parameter( + [self.nb_labels, self.nb_labels], + default_initializer=nn.initializer.Uniform(-0.1, 0.1)) + self.init_weights() + + def init_weights(self): + # enforce contraints (rows=from, columns=to) with a big negative number + # so exp(-10000) will tend to zero + + # no transitions allowed to the beginning of sentence + self.transitions[:, self.BOS_TAG_ID] = -10000.0 + # no transition alloed from the end of sentence + self.transitions[self.EOS_TAG_ID, :] = -10000.0 + + if self.PAD_TAG_ID is not None: + # no transitions from padding + self.transitions[self.PAD_TAG_ID, :] = -10000.0 + # no transitions to padding + self.transitions[:, self.PAD_TAG_ID] = -10000.0 + # except if the end of sentence is reached + # or we are already in a pad position + self.transitions[self.PAD_TAG_ID, self.EOS_TAG_ID] = 0.0 + self.transitions[self.PAD_TAG_ID, self.PAD_TAG_ID] = 0.0 + + def forward(self, + emissions: paddle.Tensor, + tags: paddle.Tensor, + mask: paddle.Tensor=None) -> paddle.Tensor: + """Compute the negative log-likelihood. See `log_likelihood` method.""" + nll = -self.log_likelihood(emissions, tags, mask=mask) + return nll + + def log_likelihood(self, emissions, tags, mask=None): + """Compute the probability of a sequence of tags given a sequence of + emissions scores. + + Args: + emissions (paddle.Tensor): Sequence of emissions for each label. + Shape of (batch_size, seq_len, nb_labels) if batch_first is True, + (seq_len, batch_size, nb_labels) otherwise. + tags (paddle.LongTensor): Sequence of labels. + Shape of (batch_size, seq_len) if batch_first is True, + (seq_len, batch_size) otherwise. + mask (paddle.FloatTensor, optional): Tensor representing valid positions. + If None, all positions are considered valid. + Shape of (batch_size, seq_len) if batch_first is True, + (seq_len, batch_size) otherwise. + + Returns: + paddle.Tensor: sum of the log-likelihoods for each sequence in the batch. + Shape of () + """ + # fix tensors order by setting batch as the first dimension + if not self.batch_first: + emissions = emissions.transpose(0, 1) + tags = tags.transpose(0, 1) + + if mask is None: + mask = paddle.ones(emissions.shape[:2], dtype=paddle.float) + + scores = self._compute_scores(emissions, tags, mask=mask) + partition = self._compute_log_partition(emissions, mask=mask) + return paddle.sum(scores - partition) + + def decode(self, emissions, mask=None): + """Find the most probable sequence of labels given the emissions using + the Viterbi algorithm. + + Args: + emissions (paddle.Tensor): Sequence of emissions for each label. + Shape (batch_size, seq_len, nb_labels) if batch_first is True, + (seq_len, batch_size, nb_labels) otherwise. + mask (paddle.FloatTensor, optional): Tensor representing valid positions. + If None, all positions are considered valid. + Shape (batch_size, seq_len) if batch_first is True, + (seq_len, batch_size) otherwise. + + Returns: + paddle.Tensor: the viterbi score for the for each batch. + Shape of (batch_size,) + list of lists: the best viterbi sequence of labels for each batch. [B, T] + """ + # fix tensors order by setting batch as the first dimension + if not self.batch_first: + emissions = emissions.transpose(0, 1) + tags = tags.transpose(0, 1) + + if mask is None: + mask = paddle.ones(emissions.shape[:2], dtype=paddle.float) + + scores, sequences = self._viterbi_decode(emissions, mask) + return scores, sequences + + def _compute_scores(self, emissions, tags, mask): + """Compute the scores for a given batch of emissions with their tags. + + Args: + emissions (paddle.Tensor): (batch_size, seq_len, nb_labels) + tags (Paddle.LongTensor): (batch_size, seq_len) + mask (Paddle.FloatTensor): (batch_size, seq_len) + + Returns: + paddle.Tensor: Scores for each batch. + Shape of (batch_size,) + """ + batch_size, seq_length = tags.shape + scores = paddle.zeros([batch_size]) + + # save first and last tags to be used later + first_tags = tags[:, 0] + last_valid_idx = mask.int().sum(1) - 1 + + # TODO(Hui Zhang): not support fancy index. + # last_tags = tags.gather(last_valid_idx.unsqueeze(1), axis=1).squeeze() + batch_idx = paddle.arange(batch_size, dtype=last_valid_idx.dtype) + gather_last_valid_idx = paddle.stack( + [batch_idx, last_valid_idx], axis=-1) + last_tags = tags.gather_nd(gather_last_valid_idx) + + # add the transition from BOS to the first tags for each batch + # t_scores = self.transitions[self.BOS_TAG_ID, first_tags] + t_scores = self.transitions[self.BOS_TAG_ID].gather(first_tags) + + # add the [unary] emission scores for the first tags for each batch + # for all batches, the first word, see the correspondent emissions + # for the first tags (which is a list of ids): + # emissions[:, 0, [tag_1, tag_2, ..., tag_nblabels]] + # e_scores = emissions[:, 0].gather(1, first_tags.unsqueeze(1)).squeeze() + gather_first_tags_idx = paddle.stack([batch_idx, first_tags], axis=-1) + e_scores = emissions[:, 0].gather_nd(gather_first_tags_idx) + + # the scores for a word is just the sum of both scores + scores += e_scores + t_scores + + # now lets do this for each remaining word + for i in range(1, seq_length): + + # we could: iterate over batches, check if we reached a mask symbol + # and stop the iteration, but vecotrizing is faster due to gpu, + # so instead we perform an element-wise multiplication + is_valid = mask[:, i] + + previous_tags = tags[:, i - 1] + current_tags = tags[:, i] + + # calculate emission and transition scores as we did before + # e_scores = emissions[:, i].gather(1, current_tags.unsqueeze(1)).squeeze() + gather_current_tags_idx = paddle.stack( + [batch_idx, current_tags], axis=-1) + e_scores = emissions[:, i].gather_nd(gather_current_tags_idx) + # t_scores = self.transitions[previous_tags, current_tags] + gather_transitions_idx = paddle.stack( + [previous_tags, current_tags], axis=-1) + t_scores = self.transitions.gather_nd(gather_transitions_idx) + + # apply the mask + e_scores = e_scores * is_valid + t_scores = t_scores * is_valid + + scores += e_scores + t_scores + + # add the transition from the end tag to the EOS tag for each batch + # scores += self.transitions[last_tags, self.EOS_TAG_ID] + scores += self.transitions.gather(last_tags)[:, self.EOS_TAG_ID] + + return scores + + def _compute_log_partition(self, emissions, mask): + """Compute the partition function in log-space using the forward-algorithm. + + Args: + emissions (paddle.Tensor): (batch_size, seq_len, nb_labels) + mask (Paddle.FloatTensor): (batch_size, seq_len) + + Returns: + paddle.Tensor: the partition scores for each batch. + Shape of (batch_size,) + """ + batch_size, seq_length, nb_labels = emissions.shape + + # in the first iteration, BOS will have all the scores + alphas = self.transitions[self.BOS_TAG_ID, :].unsqueeze( + 0) + emissions[:, 0] + + for i in range(1, seq_length): + # (bs, nb_labels) -> (bs, 1, nb_labels) + e_scores = emissions[:, i].unsqueeze(1) + + # (nb_labels, nb_labels) -> (bs, nb_labels, nb_labels) + t_scores = self.transitions.unsqueeze(0) + + # (bs, nb_labels) -> (bs, nb_labels, 1) + a_scores = alphas.unsqueeze(2) + + scores = e_scores + t_scores + a_scores + new_alphas = paddle.logsumexp(scores, axis=1) + + # set alphas if the mask is valid, otherwise keep the current values + is_valid = mask[:, i].unsqueeze(-1) + alphas = is_valid * new_alphas + (1 - is_valid) * alphas + + # add the scores for the final transition + last_transition = self.transitions[:, self.EOS_TAG_ID] + end_scores = alphas + last_transition.unsqueeze(0) + + # return a *log* of sums of exps + return paddle.logsumexp(end_scores, axis=1) + + def _viterbi_decode(self, emissions, mask): + """Compute the viterbi algorithm to find the most probable sequence of labels + given a sequence of emissions. + + Args: + emissions (paddle.Tensor): (batch_size, seq_len, nb_labels) + mask (Paddle.FloatTensor): (batch_size, seq_len) + + Returns: + paddle.Tensor: the viterbi score for the for each batch. + Shape of (batch_size,) + list of lists of ints: the best viterbi sequence of labels for each batch + """ + batch_size, seq_length, nb_labels = emissions.shape + + # in the first iteration, BOS will have all the scores and then, the max + alphas = self.transitions[self.BOS_TAG_ID, :].unsqueeze( + 0) + emissions[:, 0] + + backpointers = [] + + for i in range(1, seq_length): + # (bs, nb_labels) -> (bs, 1, nb_labels) + e_scores = emissions[:, i].unsqueeze(1) + + # (nb_labels, nb_labels) -> (bs, nb_labels, nb_labels) + t_scores = self.transitions.unsqueeze(0) + + # (bs, nb_labels) -> (bs, nb_labels, 1) + a_scores = alphas.unsqueeze(2) + + # combine current scores with previous alphas + scores = e_scores + t_scores + a_scores + + # so far is exactly like the forward algorithm, + # but now, instead of calculating the logsumexp, + # we will find the highest score and the tag associated with it + # max_scores, max_score_tags = paddle.max(scores, axis=1) + max_scores = paddle.max(scores, axis=1) + max_score_tags = paddle.argmax(scores, axis=1) + + # set alphas if the mask is valid, otherwise keep the current values + is_valid = mask[:, i].unsqueeze(-1) + alphas = is_valid * max_scores + (1 - is_valid) * alphas + + # add the max_score_tags for our list of backpointers + # max_scores has shape (batch_size, nb_labels) so we transpose it to + # be compatible with our previous loopy version of viterbi + backpointers.append(max_score_tags.t()) + + # add the scores for the final transition + last_transition = self.transitions[:, self.EOS_TAG_ID] + end_scores = alphas + last_transition.unsqueeze(0) + + # get the final most probable score and the final most probable tag + # max_final_scores, max_final_tags = paddle.max(end_scores, axis=1) + max_final_scores = paddle.max(end_scores, axis=1) + max_final_tags = paddle.argmax(end_scores, axis=1) + + # find the best sequence of labels for each sample in the batch + best_sequences = [] + emission_lengths = mask.int().sum(axis=1) + for i in range(batch_size): + + # recover the original sentence length for the i-th sample in the batch + sample_length = emission_lengths[i].item() + + # recover the max tag for the last timestep + sample_final_tag = max_final_tags[i].item() + + # limit the backpointers until the last but one + # since the last corresponds to the sample_final_tag + sample_backpointers = backpointers[:sample_length - 1] + + # follow the backpointers to build the sequence of labels + sample_path = self._find_best_path(i, sample_final_tag, + sample_backpointers) + + # add this path to the list of best sequences + best_sequences.append(sample_path) + + return max_final_scores, best_sequences + + def _find_best_path(self, sample_id, best_tag, backpointers): + """Auxiliary function to find the best path sequence for a specific sample. + + Args: + sample_id (int): sample index in the range [0, batch_size) + best_tag (int): tag which maximizes the final score + backpointers (list of lists of tensors): list of pointers with + shape (seq_len_i-1, nb_labels, batch_size) where seq_len_i + represents the length of the ith sample in the batch + + Returns: + list of ints: a list of tag indexes representing the bast path + """ + # add the final best_tag to our best path + best_path = [best_tag] + + # traverse the backpointers in backwards + for backpointers_t in reversed(backpointers): + + # recover the best_tag at this timestep + best_tag = backpointers_t[best_tag][sample_id].item() + + # append to the beginning of the list so we don't need to reverse it later + best_path.insert(0, best_tag) + + return best_path diff --git a/ernie-sat/paddlespeech/s2t/modules/ctc.py b/ernie-sat/paddlespeech/s2t/modules/ctc.py new file mode 100644 index 0000000..33ad472 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/ctc.py @@ -0,0 +1,470 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Union + +import paddle +from paddle import nn +from paddle.nn import functional as F +from typeguard import check_argument_types + +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.modules.loss import CTCLoss +from paddlespeech.s2t.utils import ctc_utils +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +try: + from paddlespeech.s2t.decoders.ctcdecoder import ctc_beam_search_decoding_batch # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import ctc_greedy_decoding # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import Scorer # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import CTCBeamSearchDecoder # noqa: F401 +except ImportError: + try: + from paddlespeech.s2t.utils import dynamic_pip_install + package_name = 'paddlespeech_ctcdecoders' + dynamic_pip_install.install(package_name) + from paddlespeech.s2t.decoders.ctcdecoder import ctc_beam_search_decoding_batch # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import ctc_greedy_decoding # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import Scorer # noqa: F401 + from paddlespeech.s2t.decoders.ctcdecoder import CTCBeamSearchDecoder # noqa: F401 + except Exception as e: + logger.info("paddlespeech_ctcdecoders not installed!") + +__all__ = ['CTCDecoder'] + + +class CTCDecoderBase(nn.Layer): + def __init__(self, + odim, + enc_n_units, + blank_id=0, + dropout_rate: float=0.0, + reduction: bool=True, + batch_average: bool=True, + grad_norm_type: Union[str, None]=None): + """CTC decoder + + Args: + odim ([int]): text vocabulary size + enc_n_units ([int]): encoder output dimention + dropout_rate (float): dropout rate (0.0 ~ 1.0) + reduction (bool): reduce the CTC loss into a scalar, True for 'sum' or 'none' + batch_average (bool): do batch dim wise average. + grad_norm_type (str): Default, None. one of 'instance', 'batch', 'frame', None. + """ + assert check_argument_types() + super().__init__() + + self.blank_id = blank_id + self.odim = odim + self.dropout = nn.Dropout(dropout_rate) + self.ctc_lo = Linear(enc_n_units, self.odim) + reduction_type = "sum" if reduction else "none" + self.criterion = CTCLoss( + blank=self.blank_id, + reduction=reduction_type, + batch_average=batch_average, + grad_norm_type=grad_norm_type) + + def forward(self, hs_pad, hlens, ys_pad, ys_lens): + """Calculate CTC loss. + + Args: + hs_pad (Tensor): batch of padded hidden state sequences (B, Tmax, D) + hlens (Tensor): batch of lengths of hidden state sequences (B) + ys_pad (Tensor): batch of padded character id sequence tensor (B, Lmax) + ys_lens (Tensor): batch of lengths of character sequence (B) + Returns: + loss (Tensor): ctc loss value, scalar. + """ + logits = self.ctc_lo(self.dropout(hs_pad)) + loss = self.criterion(logits, ys_pad, hlens, ys_lens) + return loss + + def softmax(self, eouts: paddle.Tensor, temperature: float=1.0): + """Get CTC probabilities. + Args: + eouts (FloatTensor): `[B, T, enc_units]` + Returns: + probs (FloatTensor): `[B, T, odim]` + """ + self.probs = F.softmax(self.ctc_lo(eouts) / temperature, axis=2) + return self.probs + + def log_softmax(self, hs_pad: paddle.Tensor, + temperature: float=1.0) -> paddle.Tensor: + """log_softmax of frame activations + Args: + Tensor hs_pad: 3d tensor (B, Tmax, eprojs) + Returns: + paddle.Tensor: log softmax applied 3d tensor (B, Tmax, odim) + """ + return F.log_softmax(self.ctc_lo(hs_pad) / temperature, axis=2) + + def argmax(self, hs_pad: paddle.Tensor) -> paddle.Tensor: + """argmax of frame activations + Args: + paddle.Tensor hs_pad: 3d tensor (B, Tmax, eprojs) + Returns: + paddle.Tensor: argmax applied 2d tensor (B, Tmax) + """ + return paddle.argmax(self.ctc_lo(hs_pad), dim=2) + + def forced_align(self, + ctc_probs: paddle.Tensor, + y: paddle.Tensor, + blank_id=0) -> list: + """ctc forced alignment. + Args: + ctc_probs (paddle.Tensor): hidden state sequence, 2d tensor (T, D) + y (paddle.Tensor): label id sequence tensor, 1d tensor (L) + blank_id (int): blank symbol index + Returns: + paddle.Tensor: best alignment result, (T). + """ + return ctc_utils.forced_align(ctc_probs, y, blank_id) + + +class CTCDecoder(CTCDecoderBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # CTCDecoder LM Score handle + self._ext_scorer = None + self.beam_search_decoder = None + + def _decode_batch_greedy_offline(self, probs_split, vocab_list): + """This function will be deprecated in future. + Decode by best path for a batch of probs matrix input. + :param probs_split: List of 2-D probability matrix, and each consists + of prob vectors for one speech utterancce. + :param probs_split: List of matrix + :param vocab_list: List of tokens in the vocabulary, for decoding. + :type vocab_list: list + :return: List of transcription texts. + :rtype: List of str + """ + results = [] + for i, probs in enumerate(probs_split): + output_transcription = ctc_greedy_decoding( + probs_seq=probs, vocabulary=vocab_list, blank_id=self.blank_id) + results.append(output_transcription) + return results + + def _init_ext_scorer(self, beam_alpha, beam_beta, language_model_path, + vocab_list): + """Initialize the external scorer. + :param beam_alpha: Parameter associated with language model. + :type beam_alpha: float + :param beam_beta: Parameter associated with word count. + :type beam_beta: float + :param language_model_path: Filepath for language model. If it is + empty, the external scorer will be set to + None, and the decoding method will be pure + beam search without scorer. + :type language_model_path: str|None + :param vocab_list: List of tokens in the vocabulary, for decoding. + :type vocab_list: list + """ + # init once + if self._ext_scorer is not None: + return + + if language_model_path != '': + logger.info("begin to initialize the external scorer " + "for decoding") + self._ext_scorer = Scorer(beam_alpha, beam_beta, + language_model_path, vocab_list) + lm_char_based = self._ext_scorer.is_character_based() + lm_max_order = self._ext_scorer.get_max_order() + lm_dict_size = self._ext_scorer.get_dict_size() + logger.info("language model: " + "is_character_based = %d," % lm_char_based + + " max_order = %d," % lm_max_order + " dict_size = %d" % + lm_dict_size) + logger.info("end initializing scorer") + else: + self._ext_scorer = None + logger.info("no language model provided, " + "decoding by pure beam search without scorer.") + + def _decode_batch_beam_search_offline( + self, probs_split, beam_alpha, beam_beta, beam_size, cutoff_prob, + cutoff_top_n, vocab_list, num_processes): + """ + This function will be deprecated in future. + Decode by beam search for a batch of probs matrix input. + :param probs_split: List of 2-D probability matrix, and each consists + of prob vectors for one speech utterancce. + :param probs_split: List of matrix + :param beam_alpha: Parameter associated with language model. + :type beam_alpha: float + :param beam_beta: Parameter associated with word count. + :type beam_beta: float + :param beam_size: Width for Beam search. + :type beam_size: int + :param cutoff_prob: Cutoff probability in pruning, + default 1.0, no pruning. + :type cutoff_prob: float + :param cutoff_top_n: Cutoff number in pruning, only top cutoff_top_n + characters with highest probs in vocabulary will be + used in beam search, default 40. + :type cutoff_top_n: int + :param vocab_list: List of tokens in the vocabulary, for decoding. + :type vocab_list: list + :param num_processes: Number of processes (CPU) for decoder. + :type num_processes: int + :return: List of transcription texts. + :rtype: List of str + """ + if self._ext_scorer is not None: + self._ext_scorer.reset_params(beam_alpha, beam_beta) + + # beam search decode + num_processes = min(num_processes, len(probs_split)) + beam_search_results = ctc_beam_search_decoding_batch( + probs_split=probs_split, + vocabulary=vocab_list, + beam_size=beam_size, + num_processes=num_processes, + ext_scoring_func=self._ext_scorer, + cutoff_prob=cutoff_prob, + cutoff_top_n=cutoff_top_n, + blank_id=self.blank_id) + + results = [result[0][1] for result in beam_search_results] + return results + + def init_decoder(self, batch_size, vocab_list, decoding_method, + lang_model_path, beam_alpha, beam_beta, beam_size, + cutoff_prob, cutoff_top_n, num_processes): + """ + init ctc decoders + Args: + batch_size(int): Batch size for input data + vocab_list (list): List of tokens in the vocabulary, for decoding + decoding_method (str): ctc_beam_search + lang_model_path (str): language model path + beam_alpha (float): beam_alpha + beam_beta (float): beam_beta + beam_size (int): beam_size + cutoff_prob (float): cutoff probability in beam search + cutoff_top_n (int): cutoff_top_n + num_processes (int): num_processes + + Raises: + ValueError: when decoding_method not support. + + Returns: + CTCBeamSearchDecoder + """ + self.batch_size = batch_size + self.vocab_list = vocab_list + self.decoding_method = decoding_method + self.beam_size = beam_size + self.cutoff_prob = cutoff_prob + self.cutoff_top_n = cutoff_top_n + self.num_processes = num_processes + if decoding_method == "ctc_beam_search": + self._init_ext_scorer(beam_alpha, beam_beta, lang_model_path, + vocab_list) + if self.beam_search_decoder is None: + self.beam_search_decoder = self.get_decoder( + vocab_list, batch_size, beam_alpha, beam_beta, beam_size, + num_processes, cutoff_prob, cutoff_top_n) + return self.beam_search_decoder + elif decoding_method == "ctc_greedy": + self._init_ext_scorer(beam_alpha, beam_beta, lang_model_path, + vocab_list) + else: + raise ValueError(f"Not support: {decoding_method}") + + def decode_probs_offline(self, probs, logits_lens, vocab_list, + decoding_method, lang_model_path, beam_alpha, + beam_beta, beam_size, cutoff_prob, cutoff_top_n, + num_processes): + """ + This function will be deprecated in future. + ctc decoding with probs. + Args: + probs (Tensor): activation after softmax + logits_lens (Tensor): audio output lens + vocab_list (list): List of tokens in the vocabulary, for decoding + decoding_method (str): ctc_beam_search + lang_model_path (str): language model path + beam_alpha (float): beam_alpha + beam_beta (float): beam_beta + beam_size (int): beam_size + cutoff_prob (float): cutoff probability in beam search + cutoff_top_n (int): cutoff_top_n + num_processes (int): num_processes + + Raises: + ValueError: when decoding_method not support. + + Returns: + List[str]: transcripts. + """ + logger.warn( + "This function will be deprecated in future: decode_probs_offline") + probs_split = [probs[i, :l, :] for i, l in enumerate(logits_lens)] + if decoding_method == "ctc_greedy": + result_transcripts = self._decode_batch_greedy_offline( + probs_split=probs_split, vocab_list=vocab_list) + elif decoding_method == "ctc_beam_search": + result_transcripts = self._decode_batch_beam_search_offline( + probs_split=probs_split, + beam_alpha=beam_alpha, + beam_beta=beam_beta, + beam_size=beam_size, + cutoff_prob=cutoff_prob, + cutoff_top_n=cutoff_top_n, + vocab_list=vocab_list, + num_processes=num_processes) + else: + raise ValueError(f"Not support: {decoding_method}") + return result_transcripts + + def get_decoder(self, vocab_list, batch_size, beam_alpha, beam_beta, + beam_size, num_processes, cutoff_prob, cutoff_top_n): + """ + init get ctc decoder + Args: + vocab_list (list): List of tokens in the vocabulary, for decoding. + batch_size(int): Batch size for input data + beam_alpha (float): beam_alpha + beam_beta (float): beam_beta + beam_size (int): beam_size + num_processes (int): num_processes + cutoff_prob (float): cutoff probability in beam search + cutoff_top_n (int): cutoff_top_n + + Raises: + ValueError: when decoding_method not support. + + Returns: + CTCBeamSearchDecoder + """ + num_processes = min(num_processes, batch_size) + if self._ext_scorer is not None: + self._ext_scorer.reset_params(beam_alpha, beam_beta) + if self.decoding_method == "ctc_beam_search": + beam_search_decoder = CTCBeamSearchDecoder( + vocab_list, batch_size, beam_size, num_processes, cutoff_prob, + cutoff_top_n, self._ext_scorer, self.blank_id) + else: + raise ValueError(f"Not support: {decoding_method}") + return beam_search_decoder + + def next(self, probs, logits_lens): + """ + Input probs into ctc decoder + Args: + probs (list(list(float))): probs for a batch of data + logits_lens (list(int)): logits lens for a batch of data + Raises: + Exception: when the ctc decoder is not initialized + ValueError: when decoding_method not support. + """ + + if self.beam_search_decoder is None: + raise Exception( + "You need to initialize the beam_search_decoder firstly") + beam_search_decoder = self.beam_search_decoder + + has_value = (logits_lens > 0).tolist() + has_value = [ + "true" if has_value[i] is True else "false" + for i in range(len(has_value)) + ] + probs_split = [ + probs[i, :l, :].tolist() if has_value[i] else probs[i].tolist() + for i, l in enumerate(logits_lens) + ] + if self.decoding_method == "ctc_beam_search": + beam_search_decoder.next(probs_split, has_value) + else: + raise ValueError(f"Not support: {decoding_method}") + + return + + def decode(self): + """ + Get the decoding result + Raises: + Exception: when the ctc decoder is not initialized + ValueError: when decoding_method not support. + Returns: + results_best (list(str)): The best result for a batch of data + results_beam (list(list(str))): The beam search result for a batch of data + """ + if self.beam_search_decoder is None: + raise Exception( + "You need to initialize the beam_search_decoder firstly") + + beam_search_decoder = self.beam_search_decoder + if self.decoding_method == "ctc_beam_search": + batch_beam_results = beam_search_decoder.decode() + batch_beam_results = [[(res[0], res[1]) for res in beam_results] + for beam_results in batch_beam_results] + results_best = [result[0][1] for result in batch_beam_results] + results_beam = [[trans[1] for trans in result] + for result in batch_beam_results] + + else: + raise ValueError(f"Not support: {decoding_method}") + + return results_best, results_beam + + def reset_decoder(self, + batch_size=-1, + beam_size=-1, + num_processes=-1, + cutoff_prob=-1.0, + cutoff_top_n=-1): + if batch_size > 0: + self.batch_size = batch_size + if beam_size > 0: + self.beam_size = beam_size + if num_processes > 0: + self.num_processes = num_processes + if cutoff_prob > 0: + self.cutoff_prob = cutoff_prob + if cutoff_top_n > 0: + self.cutoff_top_n = cutoff_top_n + """ + Reset the decoder state + Args: + batch_size(int): Batch size for input data + beam_size (int): beam_size + num_processes (int): num_processes + cutoff_prob (float): cutoff probability in beam search + cutoff_top_n (int): cutoff_top_n + Raises: + Exception: when the ctc decoder is not initialized + """ + if self.beam_search_decoder is None: + raise Exception( + "You need to initialize the beam_search_decoder firstly") + self.beam_search_decoder.reset_state( + self.batch_size, self.beam_size, self.num_processes, + self.cutoff_prob, self.cutoff_top_n) + + def del_decoder(self): + """ + Delete the decoder + """ + if self.beam_search_decoder is not None: + del self.beam_search_decoder + self.beam_search_decoder = None diff --git a/ernie-sat/paddlespeech/s2t/modules/decoder.py b/ernie-sat/paddlespeech/s2t/modules/decoder.py new file mode 100644 index 0000000..3a851ec --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/decoder.py @@ -0,0 +1,252 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Decoder definition.""" +from typing import Any +from typing import List +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.s2t.decoders.scorers.scorer_interface import BatchScorerInterface +from paddlespeech.s2t.modules.align import Embedding +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.modules.attention import MultiHeadedAttention +from paddlespeech.s2t.modules.decoder_layer import DecoderLayer +from paddlespeech.s2t.modules.embedding import PositionalEncoding +from paddlespeech.s2t.modules.mask import make_non_pad_mask +from paddlespeech.s2t.modules.mask import make_xs_mask +from paddlespeech.s2t.modules.mask import subsequent_mask +from paddlespeech.s2t.modules.positionwise_feed_forward import PositionwiseFeedForward +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["TransformerDecoder"] + + +class TransformerDecoder(BatchScorerInterface, nn.Layer): + """Base class of Transfomer decoder module. + Args: + vocab_size: output dim + encoder_output_size: dimension of attention + attention_heads: the number of heads of multi head attention + linear_units: the hidden units number of position-wise feedforward + num_blocks: the number of decoder blocks + dropout_rate: dropout rate + self_attention_dropout_rate: dropout rate for attention + input_layer: input layer type, `embed` + use_output_layer: whether to use output layer + pos_enc_class: PositionalEncoding module + normalize_before: + True: use layer_norm before each sub-block of a layer. + False: use layer_norm after each sub-block of a layer. + concat_after: whether to concat attention layer's input and output + True: x -> x + linear(concat(x, att(x))) + False: x -> x + att(x) + """ + + def __init__( + self, + vocab_size: int, + encoder_output_size: int, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + self_attention_dropout_rate: float=0.0, + src_attention_dropout_rate: float=0.0, + input_layer: str="embed", + use_output_layer: bool=True, + normalize_before: bool=True, + concat_after: bool=False, ): + + assert check_argument_types() + + nn.Layer.__init__(self) + self.selfattention_layer_type = 'selfattn' + attention_dim = encoder_output_size + + if input_layer == "embed": + self.embed = nn.Sequential( + Embedding(vocab_size, attention_dim), + PositionalEncoding(attention_dim, positional_dropout_rate), ) + else: + raise ValueError(f"only 'embed' is supported: {input_layer}") + + self.normalize_before = normalize_before + self.after_norm = LayerNorm(attention_dim, epsilon=1e-12) + self.use_output_layer = use_output_layer + self.output_layer = Linear(attention_dim, vocab_size) + + self.decoders = nn.LayerList([ + DecoderLayer( + size=attention_dim, + self_attn=MultiHeadedAttention(attention_heads, attention_dim, + self_attention_dropout_rate), + src_attn=MultiHeadedAttention(attention_heads, attention_dim, + src_attention_dropout_rate), + feed_forward=PositionwiseFeedForward( + attention_dim, linear_units, dropout_rate), + dropout_rate=dropout_rate, + normalize_before=normalize_before, + concat_after=concat_after, ) for _ in range(num_blocks) + ]) + + def forward( + self, + memory: paddle.Tensor, + memory_mask: paddle.Tensor, + ys_in_pad: paddle.Tensor, + ys_in_lens: paddle.Tensor, ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Forward decoder. + Args: + memory: encoded memory, float32 (batch, maxlen_in, feat) + memory_mask: encoder memory mask, (batch, 1, maxlen_in) + ys_in_pad: padded input token ids, int64 (batch, maxlen_out) + ys_in_lens: input lengths of this batch (batch) + Returns: + (tuple): tuple containing: + x: decoded token score before softmax (batch, maxlen_out, vocab_size) + if use_output_layer is True, + olens: (batch, ) + """ + tgt = ys_in_pad + # tgt_mask: (B, 1, L) + tgt_mask = (make_non_pad_mask(ys_in_lens).unsqueeze(1)) + # m: (1, L, L) + m = subsequent_mask(tgt_mask.shape[-1]).unsqueeze(0) + # tgt_mask: (B, L, L) + # TODO(Hui Zhang): not support & for tensor + # tgt_mask = tgt_mask & m + tgt_mask = tgt_mask.logical_and(m) + + x, _ = self.embed(tgt) + for layer in self.decoders: + x, tgt_mask, memory, memory_mask = layer(x, tgt_mask, memory, + memory_mask) + if self.normalize_before: + x = self.after_norm(x) + if self.use_output_layer: + x = self.output_layer(x) + + # TODO(Hui Zhang): reduce_sum not support bool type + # olens = tgt_mask.sum(1) + olens = tgt_mask.astype(paddle.int).sum(1) + return x, olens + + def forward_one_step( + self, + memory: paddle.Tensor, + memory_mask: paddle.Tensor, + tgt: paddle.Tensor, + tgt_mask: paddle.Tensor, + cache: Optional[List[paddle.Tensor]]=None, + ) -> Tuple[paddle.Tensor, List[paddle.Tensor]]: + """Forward one step. + This is only used for decoding. + Args: + memory: encoded memory, float32 (batch, maxlen_in, feat) + memory_mask: encoded memory mask, (batch, 1, maxlen_in) + tgt: input token ids, int64 (batch, maxlen_out) + tgt_mask: input token mask, (batch, maxlen_out, maxlen_out) + dtype=paddle.bool + cache: cached output list of (batch, max_time_out-1, size) + Returns: + y, cache: NN output value and cache per `self.decoders`. + y.shape` is (batch, token) + """ + x, _ = self.embed(tgt) + new_cache = [] + for i, decoder in enumerate(self.decoders): + if cache is None: + c = None + else: + c = cache[i] + x, tgt_mask, memory, memory_mask = decoder( + x, tgt_mask, memory, memory_mask, cache=c) + new_cache.append(x) + if self.normalize_before: + y = self.after_norm(x[:, -1]) + else: + y = x[:, -1] + if self.use_output_layer: + y = paddle.log_softmax(self.output_layer(y), axis=-1) + return y, new_cache + + # beam search API (see ScorerInterface) + def score(self, ys, state, x): + """Score. + ys: (ylen,) + x: (xlen, n_feat) + """ + ys_mask = subsequent_mask(len(ys)).unsqueeze(0) # (B,L,L) + x_mask = make_xs_mask(x.unsqueeze(0)).unsqueeze(1) # (B,1,T) + if self.selfattention_layer_type != "selfattn": + # TODO(karita): implement cache + logging.warning( + f"{self.selfattention_layer_type} does not support cached decoding." + ) + state = None + logp, state = self.forward_one_step( + x.unsqueeze(0), x_mask, ys.unsqueeze(0), ys_mask, cache=state) + return logp.squeeze(0), state + + # batch beam search API (see BatchScorerInterface) + def batch_score(self, + ys: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor) -> Tuple[paddle.Tensor, List[Any]]: + """Score new token batch (required). + + Args: + ys (paddle.Tensor): paddle.int64 prefix tokens (n_batch, ylen). + states (List[Any]): Scorer states for prefix tokens. + xs (paddle.Tensor): + The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[paddle.Tensor, List[Any]]: Tuple of + batchfied scores for next token with shape of `(n_batch, n_vocab)` + and next state list for ys. + + """ + # merge states + n_batch = len(ys) + n_layers = len(self.decoders) + if states[0] is None: + batch_state = None + else: + # transpose state of [batch, layer] into [layer, batch] + batch_state = [ + paddle.stack([states[b][i] for b in range(n_batch)]) + for i in range(n_layers) + ] + + # batch decoding + ys_mask = subsequent_mask(ys.size(-1)).unsqueeze(0) # (B,L,L) + xs_mask = make_xs_mask(xs).unsqueeze(1) # (B,1,T) + logp, states = self.forward_one_step( + xs, xs_mask, ys, ys_mask, cache=batch_state) + + # transpose state of [layer, batch] into [batch, layer] + state_list = [[states[i][b] for i in range(n_layers)] + for b in range(n_batch)] + return logp, state_list diff --git a/ernie-sat/paddlespeech/s2t/modules/decoder_layer.py b/ernie-sat/paddlespeech/s2t/modules/decoder_layer.py new file mode 100644 index 0000000..b7f8694 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/decoder_layer.py @@ -0,0 +1,155 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Decoder self-attention layer definition.""" +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn + +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["DecoderLayer"] + + +class DecoderLayer(nn.Layer): + """Single decoder layer module. + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` instance can be used as the argument. + src_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` instance can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward` instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): + True: use layer_norm before each sub-block. + False: to use layer_norm after each sub-block. + concat_after (bool): Whether to concat attention layer's input + and output. + True: x -> x + linear(concat(x, att(x))) + False: x -> x + att(x) + """ + + def __init__( + self, + size: int, + self_attn: nn.Layer, + src_attn: nn.Layer, + feed_forward: nn.Layer, + dropout_rate: float, + normalize_before: bool=True, + concat_after: bool=False, ): + """Construct an DecoderLayer object.""" + super().__init__() + self.size = size + self.self_attn = self_attn + self.src_attn = src_attn + self.feed_forward = feed_forward + self.norm1 = LayerNorm(size, epsilon=1e-12) + self.norm2 = LayerNorm(size, epsilon=1e-12) + self.norm3 = LayerNorm(size, epsilon=1e-12) + self.dropout = nn.Dropout(dropout_rate) + self.normalize_before = normalize_before + self.concat_after = concat_after + self.concat_linear1 = Linear(size + size, size) + self.concat_linear2 = Linear(size + size, size) + + def forward( + self, + tgt: paddle.Tensor, + tgt_mask: paddle.Tensor, + memory: paddle.Tensor, + memory_mask: paddle.Tensor, + cache: Optional[paddle.Tensor]=None + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Compute decoded features. + Args: + tgt (paddle.Tensor): Input tensor (#batch, maxlen_out, size). + tgt_mask (paddle.Tensor): Mask for input tensor + (#batch, maxlen_out). + memory (paddle.Tensor): Encoded memory + (#batch, maxlen_in, size). + memory_mask (paddle.Tensor): Encoded memory mask + (#batch, maxlen_in). + cache (paddle.Tensor): cached tensors. + (#batch, maxlen_out - 1, size). + Returns: + paddle.Tensor: Output tensor (#batch, maxlen_out, size). + paddle.Tensor: Mask for output tensor (#batch, maxlen_out). + paddle.Tensor: Encoded memory (#batch, maxlen_in, size). + paddle.Tensor: Encoded memory mask (#batch, maxlen_in). + """ + residual = tgt + if self.normalize_before: + tgt = self.norm1(tgt) + + if cache is None: + tgt_q = tgt + tgt_q_mask = tgt_mask + else: + # compute only the last frame query keeping dim: max_time_out -> 1 + assert cache.shape == [ + tgt.shape[0], + tgt.shape[1] - 1, + self.size, + ], f"{cache.shape} == {[tgt.shape[0], tgt.shape[1] - 1, self.size]}" + tgt_q = tgt[:, -1:, :] + residual = residual[:, -1:, :] + # TODO(Hui Zhang): slice not support bool type + # tgt_q_mask = tgt_mask[:, -1:, :] + tgt_q_mask = tgt_mask.cast(paddle.int64)[:, -1:, :].cast( + paddle.bool) + + if self.concat_after: + tgt_concat = paddle.cat( + (tgt_q, self.self_attn(tgt_q, tgt, tgt, tgt_q_mask)), dim=-1) + x = residual + self.concat_linear1(tgt_concat) + else: + x = residual + self.dropout( + self.self_attn(tgt_q, tgt, tgt, tgt_q_mask)) + if not self.normalize_before: + x = self.norm1(x) + + residual = x + if self.normalize_before: + x = self.norm2(x) + if self.concat_after: + x_concat = paddle.cat( + (x, self.src_attn(x, memory, memory, memory_mask)), dim=-1) + x = residual + self.concat_linear2(x_concat) + else: + x = residual + self.dropout( + self.src_attn(x, memory, memory, memory_mask)) + if not self.normalize_before: + x = self.norm2(x) + + residual = x + if self.normalize_before: + x = self.norm3(x) + x = residual + self.dropout(self.feed_forward(x)) + if not self.normalize_before: + x = self.norm3(x) + + if cache is not None: + x = paddle.cat([cache, x], dim=1) + + return x, tgt_mask, memory, memory_mask diff --git a/ernie-sat/paddlespeech/s2t/modules/embedding.py b/ernie-sat/paddlespeech/s2t/modules/embedding.py new file mode 100644 index 0000000..5d4e917 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/embedding.py @@ -0,0 +1,165 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Positonal Encoding Module.""" +import math +from typing import Tuple + +import paddle +from paddle import nn + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "PositionalEncodingInterface", "NoPositionalEncoding", "PositionalEncoding", + "RelPositionalEncoding" +] + + +class PositionalEncodingInterface: + def forward(self, x: paddle.Tensor, + offset: int=0) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Compute positional encoding. + Args: + x (paddle.Tensor): Input tensor (batch, time, `*`). + Returns: + paddle.Tensor: Encoded tensor (batch, time, `*`). + paddle.Tensor: Positional embedding tensor (1, time, `*`). + """ + raise NotImplementedError("forward method is not implemented") + + def position_encoding(self, offset: int, size: int) -> paddle.Tensor: + """ For getting encoding in a streaming fashion + Args: + offset (int): start offset + size (int): requried size of position encoding + Returns: + paddle.Tensor: Corresponding position encoding + """ + raise NotImplementedError("position_encoding method is not implemented") + + +class NoPositionalEncoding(nn.Layer, PositionalEncodingInterface): + def __init__(self, + d_model: int, + dropout_rate: float, + max_len: int=5000, + reverse: bool=False): + nn.Layer.__init__(self) + + def forward(self, x: paddle.Tensor, + offset: int=0) -> Tuple[paddle.Tensor, paddle.Tensor]: + return x, None + + def position_encoding(self, offset: int, size: int) -> paddle.Tensor: + return None + + +class PositionalEncoding(nn.Layer, PositionalEncodingInterface): + def __init__(self, + d_model: int, + dropout_rate: float, + max_len: int=5000, + reverse: bool=False): + """Positional encoding. + PE(pos, 2i) = sin(pos/(10000^(2i/dmodel))) + PE(pos, 2i+1) = cos(pos/(10000^(2i/dmodel))) + Args: + d_model (int): embedding dim. + dropout_rate (float): dropout rate. + max_len (int, optional): maximum input length. Defaults to 5000. + reverse (bool, optional): Not used. Defaults to False. + """ + nn.Layer.__init__(self) + self.d_model = d_model + self.max_len = max_len + self.xscale = paddle.to_tensor(math.sqrt(self.d_model)) + self.dropout = nn.Dropout(p=dropout_rate) + self.pe = paddle.zeros([self.max_len, self.d_model]) #[T,D] + + position = paddle.arange( + 0, self.max_len, dtype=paddle.float32).unsqueeze(1) #[T, 1] + div_term = paddle.exp( + paddle.arange(0, self.d_model, 2, dtype=paddle.float32) * + -(math.log(10000.0) / self.d_model)) + + self.pe[:, 0::2] = paddle.sin(position * div_term) + self.pe[:, 1::2] = paddle.cos(position * div_term) + self.pe = self.pe.unsqueeze(0) #[1, T, D] + + def forward(self, x: paddle.Tensor, + offset: int=0) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Add positional encoding. + Args: + x (paddle.Tensor): Input. Its shape is (batch, time, ...) + offset (int): position offset + Returns: + paddle.Tensor: Encoded tensor. Its shape is (batch, time, ...) + paddle.Tensor: for compatibility to RelPositionalEncoding, (batch=1, time, ...) + """ + T = x.shape[1] + assert offset + x.shape[1] < self.max_len + #TODO(Hui Zhang): using T = x.size(1), __getitem__ not support Tensor + pos_emb = self.pe[:, offset:offset + T] + x = x * self.xscale + pos_emb + return self.dropout(x), self.dropout(pos_emb) + + def position_encoding(self, offset: int, size: int) -> paddle.Tensor: + """ For getting encoding in a streaming fashion + Attention!!!!! + we apply dropout only once at the whole utterance level in a none + streaming way, but will call this function several times with + increasing input size in a streaming scenario, so the dropout will + be applied several times. + Args: + offset (int): start offset + size (int): requried size of position encoding + Returns: + paddle.Tensor: Corresponding position encoding + """ + assert offset + size < self.max_len + return self.dropout(self.pe[:, offset:offset + size]) + + +class RelPositionalEncoding(PositionalEncoding): + """Relative positional encoding module. + See : Appendix B in https://arxiv.org/abs/1901.02860 + """ + + def __init__(self, d_model: int, dropout_rate: float, max_len: int=5000): + """ + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int, optional): [Maximum input length.]. Defaults to 5000. + """ + super().__init__(d_model, dropout_rate, max_len, reverse=True) + + def forward(self, x: paddle.Tensor, + offset: int=0) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Compute positional encoding. + Args: + x (paddle.Tensor): Input tensor (batch, time, `*`). + Returns: + paddle.Tensor: Encoded tensor (batch, time, `*`). + paddle.Tensor: Positional embedding tensor (1, time, `*`). + """ + assert offset + x.shape[1] < self.max_len + x = x * self.xscale + #TODO(Hui Zhang): using x.size(1), __getitem__ not support Tensor + pos_emb = self.pe[:, offset:offset + x.shape[1]] + return self.dropout(x), self.dropout(pos_emb) diff --git a/ernie-sat/paddlespeech/s2t/modules/encoder.py b/ernie-sat/paddlespeech/s2t/modules/encoder.py new file mode 100644 index 0000000..c843c0e --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/encoder.py @@ -0,0 +1,495 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Encoder definition.""" +from typing import List +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.s2t.modules.activation import get_activation +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.modules.attention import MultiHeadedAttention +from paddlespeech.s2t.modules.attention import RelPositionMultiHeadedAttention +from paddlespeech.s2t.modules.conformer_convolution import ConvolutionModule +from paddlespeech.s2t.modules.embedding import NoPositionalEncoding +from paddlespeech.s2t.modules.embedding import PositionalEncoding +from paddlespeech.s2t.modules.embedding import RelPositionalEncoding +from paddlespeech.s2t.modules.encoder_layer import ConformerEncoderLayer +from paddlespeech.s2t.modules.encoder_layer import TransformerEncoderLayer +from paddlespeech.s2t.modules.mask import add_optional_chunk_mask +from paddlespeech.s2t.modules.mask import make_non_pad_mask +from paddlespeech.s2t.modules.positionwise_feed_forward import PositionwiseFeedForward +from paddlespeech.s2t.modules.subsampling import Conv2dSubsampling4 +from paddlespeech.s2t.modules.subsampling import Conv2dSubsampling6 +from paddlespeech.s2t.modules.subsampling import Conv2dSubsampling8 +from paddlespeech.s2t.modules.subsampling import LinearNoSubsampling +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["BaseEncoder", 'TransformerEncoder', "ConformerEncoder"] + + +class BaseEncoder(nn.Layer): + def __init__( + self, + input_size: int, + output_size: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + pos_enc_layer_type: str="abs_pos", + normalize_before: bool=True, + concat_after: bool=False, + static_chunk_size: int=0, + use_dynamic_chunk: bool=False, + global_cmvn: paddle.nn.Layer=None, + use_dynamic_left_chunk: bool=False, ): + """ + Args: + input_size (int): input dim, d_feature + output_size (int): dimension of attention, d_model + attention_heads (int): the number of heads of multi head attention + linear_units (int): the hidden units number of position-wise feed + forward + num_blocks (int): the number of encoder blocks + dropout_rate (float): dropout rate + attention_dropout_rate (float): dropout rate in attention + positional_dropout_rate (float): dropout rate after adding + positional encoding + input_layer (str): input layer type. + optional [linear, conv2d, conv2d6, conv2d8] + pos_enc_layer_type (str): Encoder positional encoding layer type. + opitonal [abs_pos, scaled_abs_pos, rel_pos, no_pos] + normalize_before (bool): + True: use layer_norm before each sub-block of a layer. + False: use layer_norm after each sub-block of a layer. + concat_after (bool): whether to concat attention layer's input + and output. + True: x -> x + linear(concat(x, att(x))) + False: x -> x + att(x) + static_chunk_size (int): chunk size for static chunk training and + decoding + use_dynamic_chunk (bool): whether use dynamic chunk size for + training or not, You can only use fixed chunk(chunk_size > 0) + or dyanmic chunk size(use_dynamic_chunk = True) + global_cmvn (Optional[paddle.nn.Layer]): Optional GlobalCMVN layer + use_dynamic_left_chunk (bool): whether use dynamic left chunk in + dynamic chunk training + """ + assert check_argument_types() + super().__init__() + self._output_size = output_size + + if pos_enc_layer_type == "abs_pos": + pos_enc_class = PositionalEncoding + elif pos_enc_layer_type == "rel_pos": + pos_enc_class = RelPositionalEncoding + elif pos_enc_layer_type == "no_pos": + pos_enc_class = NoPositionalEncoding + else: + raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type) + + if input_layer == "linear": + subsampling_class = LinearNoSubsampling + elif input_layer == "conv2d": + subsampling_class = Conv2dSubsampling4 + elif input_layer == "conv2d6": + subsampling_class = Conv2dSubsampling6 + elif input_layer == "conv2d8": + subsampling_class = Conv2dSubsampling8 + else: + raise ValueError("unknown input_layer: " + input_layer) + + self.global_cmvn = global_cmvn + self.embed = subsampling_class( + idim=input_size, + odim=output_size, + dropout_rate=dropout_rate, + pos_enc_class=pos_enc_class( + d_model=output_size, dropout_rate=positional_dropout_rate), ) + + self.normalize_before = normalize_before + self.after_norm = LayerNorm(output_size, epsilon=1e-12) + self.static_chunk_size = static_chunk_size + self.use_dynamic_chunk = use_dynamic_chunk + self.use_dynamic_left_chunk = use_dynamic_left_chunk + + def output_size(self) -> int: + return self._output_size + + def forward( + self, + xs: paddle.Tensor, + xs_lens: paddle.Tensor, + decoding_chunk_size: int=0, + num_decoding_left_chunks: int=-1, + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Embed positions in tensor. + Args: + xs: padded input tensor (B, L, D) + xs_lens: input length (B) + decoding_chunk_size: decoding chunk size for dynamic chunk + 0: default for training, use random dynamic chunk. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + num_decoding_left_chunks: number of left chunks, this is for decoding, + the chunk size is decoding_chunk_size. + >=0: use num_decoding_left_chunks + <0: use all left chunks + Returns: + encoder output tensor, lens and mask + """ + masks = make_non_pad_mask(xs_lens).unsqueeze(1) # (B, 1, L) + + if self.global_cmvn is not None: + xs = self.global_cmvn(xs) + #TODO(Hui Zhang): self.embed(xs, masks, offset=0), stride_slice not support bool tensor + xs, pos_emb, masks = self.embed(xs, masks.astype(xs.dtype), offset=0) + #TODO(Hui Zhang): remove mask.astype, stride_slice not support bool tensor + masks = masks.astype(paddle.bool) + #TODO(Hui Zhang): mask_pad = ~masks + mask_pad = masks.logical_not() + chunk_masks = add_optional_chunk_mask( + xs, masks, self.use_dynamic_chunk, self.use_dynamic_left_chunk, + decoding_chunk_size, self.static_chunk_size, + num_decoding_left_chunks) + for layer in self.encoders: + xs, chunk_masks, _ = layer(xs, chunk_masks, pos_emb, mask_pad) + if self.normalize_before: + xs = self.after_norm(xs) + # Here we assume the mask is not changed in encoder layers, so just + # return the masks before encoder layers, and the masks will be used + # for cross attention with decoder later + return xs, masks + + def forward_chunk( + self, + xs: paddle.Tensor, + offset: int, + required_cache_size: int, + subsampling_cache: Optional[paddle.Tensor]=None, + elayers_output_cache: Optional[List[paddle.Tensor]]=None, + conformer_cnn_cache: Optional[List[paddle.Tensor]]=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, List[paddle.Tensor], List[ + paddle.Tensor]]: + """ Forward just one chunk + Args: + xs (paddle.Tensor): chunk input, [B=1, T, D] + offset (int): current offset in encoder output time stamp + required_cache_size (int): cache size required for next chunk + compuation + >=0: actual cache size + <0: means all history cache is required + subsampling_cache (Optional[paddle.Tensor]): subsampling cache + elayers_output_cache (Optional[List[paddle.Tensor]]): + transformer/conformer encoder layers output cache + conformer_cnn_cache (Optional[List[paddle.Tensor]]): conformer + cnn cache + Returns: + paddle.Tensor: output of current input xs + paddle.Tensor: subsampling cache required for next chunk computation + List[paddle.Tensor]: encoder layers output cache required for next + chunk computation + List[paddle.Tensor]: conformer cnn cache + """ + assert xs.shape[0] == 1 # batch size must be one + # tmp_masks is just for interface compatibility + # TODO(Hui Zhang): stride_slice not support bool tensor + # tmp_masks = paddle.ones([1, xs.size(1)], dtype=paddle.bool) + tmp_masks = paddle.ones([1, xs.shape[1]], dtype=paddle.int32) + tmp_masks = tmp_masks.unsqueeze(1) #[B=1, C=1, T] + + if self.global_cmvn is not None: + xs = self.global_cmvn(xs) + + xs, pos_emb, _ = self.embed( + xs, tmp_masks, offset=offset) #xs=(B, T, D), pos_emb=(B=1, T, D) + + if subsampling_cache is not None: + cache_size = subsampling_cache.shape[1] #T + xs = paddle.cat((subsampling_cache, xs), dim=1) + else: + cache_size = 0 + + # only used when using `RelPositionMultiHeadedAttention` + pos_emb = self.embed.position_encoding( + offset=offset - cache_size, size=xs.shape[1]) + + if required_cache_size < 0: + next_cache_start = 0 + elif required_cache_size == 0: + next_cache_start = xs.shape[1] + else: + next_cache_start = xs.shape[1] - required_cache_size + r_subsampling_cache = xs[:, next_cache_start:, :] + + # Real mask for transformer/conformer layers + masks = paddle.ones([1, xs.shape[1]], dtype=paddle.bool) + masks = masks.unsqueeze(1) #[B=1, L'=1, T] + r_elayers_output_cache = [] + r_conformer_cnn_cache = [] + for i, layer in enumerate(self.encoders): + attn_cache = None if elayers_output_cache is None else elayers_output_cache[ + i] + cnn_cache = None if conformer_cnn_cache is None else conformer_cnn_cache[ + i] + xs, _, new_cnn_cache = layer( + xs, + masks, + pos_emb, + output_cache=attn_cache, + cnn_cache=cnn_cache) + r_elayers_output_cache.append(xs[:, next_cache_start:, :]) + r_conformer_cnn_cache.append(new_cnn_cache) + if self.normalize_before: + xs = self.after_norm(xs) + + return (xs[:, cache_size:, :], r_subsampling_cache, + r_elayers_output_cache, r_conformer_cnn_cache) + + def forward_chunk_by_chunk( + self, + xs: paddle.Tensor, + decoding_chunk_size: int, + num_decoding_left_chunks: int=-1, + ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """ Forward input chunk by chunk with chunk_size like a streaming + fashion + Here we should pay special attention to computation cache in the + streaming style forward chunk by chunk. Three things should be taken + into account for computation in the current network: + 1. transformer/conformer encoder layers output cache + 2. convolution in conformer + 3. convolution in subsampling + However, we don't implement subsampling cache for: + 1. We can control subsampling module to output the right result by + overlapping input instead of cache left context, even though it + wastes some computation, but subsampling only takes a very + small fraction of computation in the whole model. + 2. Typically, there are several covolution layers with subsampling + in subsampling module, it is tricky and complicated to do cache + with different convolution layers with different subsampling + rate. + 3. Currently, nn.Sequential is used to stack all the convolution + layers in subsampling, we need to rewrite it to make it work + with cache, which is not prefered. + Args: + xs (paddle.Tensor): (1, max_len, dim) + chunk_size (int): decoding chunk size. + num_left_chunks (int): decoding with num left chunks. + """ + assert decoding_chunk_size > 0 + # The model is trained by static or dynamic chunk + assert self.static_chunk_size > 0 or self.use_dynamic_chunk + + # feature stride and window for `subsampling` module + subsampling = self.embed.subsampling_rate + context = self.embed.right_context + 1 # Add current frame + stride = subsampling * decoding_chunk_size + decoding_window = (decoding_chunk_size - 1) * subsampling + context + + num_frames = xs.shape[1] + required_cache_size = decoding_chunk_size * num_decoding_left_chunks + subsampling_cache: Optional[paddle.Tensor] = None + elayers_output_cache: Optional[List[paddle.Tensor]] = None + conformer_cnn_cache: Optional[List[paddle.Tensor]] = None + outputs = [] + offset = 0 + # Feed forward overlap input step by step + for cur in range(0, num_frames - context + 1, stride): + end = min(cur + decoding_window, num_frames) + chunk_xs = xs[:, cur:end, :] + (y, subsampling_cache, elayers_output_cache, + conformer_cnn_cache) = self.forward_chunk( + chunk_xs, offset, required_cache_size, subsampling_cache, + elayers_output_cache, conformer_cnn_cache) + outputs.append(y) + offset += y.shape[1] + ys = paddle.cat(outputs, 1) + # fake mask, just for jit script and compatibility with `forward` api + masks = paddle.ones([1, ys.shape[1]], dtype=paddle.bool) + masks = masks.unsqueeze(1) + return ys, masks + + +class TransformerEncoder(BaseEncoder): + """Transformer encoder module.""" + + def __init__( + self, + input_size: int, + output_size: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + pos_enc_layer_type: str="abs_pos", + normalize_before: bool=True, + concat_after: bool=False, + static_chunk_size: int=0, + use_dynamic_chunk: bool=False, + global_cmvn: nn.Layer=None, + use_dynamic_left_chunk: bool=False, ): + """ Construct TransformerEncoder + See Encoder for the meaning of each parameter. + """ + assert check_argument_types() + super().__init__(input_size, output_size, attention_heads, linear_units, + num_blocks, dropout_rate, positional_dropout_rate, + attention_dropout_rate, input_layer, + pos_enc_layer_type, normalize_before, concat_after, + static_chunk_size, use_dynamic_chunk, global_cmvn, + use_dynamic_left_chunk) + self.encoders = nn.LayerList([ + TransformerEncoderLayer( + size=output_size, + self_attn=MultiHeadedAttention(attention_heads, output_size, + attention_dropout_rate), + feed_forward=PositionwiseFeedForward(output_size, linear_units, + dropout_rate), + dropout_rate=dropout_rate, + normalize_before=normalize_before, + concat_after=concat_after) for _ in range(num_blocks) + ]) + + def forward_one_step( + self, + xs: paddle.Tensor, + masks: paddle.Tensor, + cache=None, ) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Encode input frame. + + Args: + xs (paddle.Tensor): (Prefix) Input tensor. (B, T, D) + masks (paddle.Tensor): Mask tensor. (B, T, T) + cache (List[paddle.Tensor]): List of cache tensors. + + Returns: + paddle.Tensor: Output tensor. + paddle.Tensor: Mask tensor. + List[paddle.Tensor]: List of new cache tensors. + """ + if self.global_cmvn is not None: + xs = self.global_cmvn(xs) + + #TODO(Hui Zhang): self.embed(xs, masks, offset=0), stride_slice not support bool tensor + xs, pos_emb, masks = self.embed(xs, masks.astype(xs.dtype), offset=0) + #TODO(Hui Zhang): remove mask.astype, stride_slice not support bool tensor + masks = masks.astype(paddle.bool) + + if cache is None: + cache = [None for _ in range(len(self.encoders))] + new_cache = [] + for c, e in zip(cache, self.encoders): + xs, masks, _ = e(xs, masks, output_cache=c) + new_cache.append(xs) + if self.normalize_before: + xs = self.after_norm(xs) + return xs, masks, new_cache + + +class ConformerEncoder(BaseEncoder): + """Conformer encoder module.""" + + def __init__( + self, + input_size: int, + output_size: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + pos_enc_layer_type: str="rel_pos", + normalize_before: bool=True, + concat_after: bool=False, + static_chunk_size: int=0, + use_dynamic_chunk: bool=False, + global_cmvn: nn.Layer=None, + use_dynamic_left_chunk: bool=False, + positionwise_conv_kernel_size: int=1, + macaron_style: bool=True, + selfattention_layer_type: str="rel_selfattn", + activation_type: str="swish", + use_cnn_module: bool=True, + cnn_module_kernel: int=15, + causal: bool=False, + cnn_module_norm: str="batch_norm", ): + """Construct ConformerEncoder + Args: + input_size to use_dynamic_chunk, see in BaseEncoder + positionwise_conv_kernel_size (int): Kernel size of positionwise + conv1d layer. + macaron_style (bool): Whether to use macaron style for + positionwise layer. + selfattention_layer_type (str): Encoder attention layer type, + the parameter has no effect now, it's just for configure + compatibility. + activation_type (str): Encoder activation function type. + use_cnn_module (bool): Whether to use convolution module. + cnn_module_kernel (int): Kernel size of convolution module. + causal (bool): whether to use causal convolution or not. + cnn_module_norm (str): cnn conv norm type, Optional['batch_norm','layer_norm'] + """ + assert check_argument_types() + + super().__init__(input_size, output_size, attention_heads, linear_units, + num_blocks, dropout_rate, positional_dropout_rate, + attention_dropout_rate, input_layer, + pos_enc_layer_type, normalize_before, concat_after, + static_chunk_size, use_dynamic_chunk, global_cmvn, + use_dynamic_left_chunk) + activation = get_activation(activation_type) + + # self-attention module definition + encoder_selfattn_layer = RelPositionMultiHeadedAttention + encoder_selfattn_layer_args = (attention_heads, output_size, + attention_dropout_rate) + # feed-forward module definition + positionwise_layer = PositionwiseFeedForward + positionwise_layer_args = (output_size, linear_units, dropout_rate, + activation) + # convolution module definition + convolution_layer = ConvolutionModule + convolution_layer_args = (output_size, cnn_module_kernel, activation, + cnn_module_norm, causal) + + self.encoders = nn.LayerList([ + ConformerEncoderLayer( + size=output_size, + self_attn=encoder_selfattn_layer(*encoder_selfattn_layer_args), + feed_forward=positionwise_layer(*positionwise_layer_args), + feed_forward_macaron=positionwise_layer( + *positionwise_layer_args) if macaron_style else None, + conv_module=convolution_layer(*convolution_layer_args) + if use_cnn_module else None, + dropout_rate=dropout_rate, + normalize_before=normalize_before, + concat_after=concat_after) for _ in range(num_blocks) + ]) diff --git a/ernie-sat/paddlespeech/s2t/modules/encoder_layer.py b/ernie-sat/paddlespeech/s2t/modules/encoder_layer.py new file mode 100644 index 0000000..e80a298 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/encoder_layer.py @@ -0,0 +1,288 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Encoder self-attention layer definition.""" +from typing import Optional +from typing import Tuple + +import paddle +from paddle import nn + +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["TransformerEncoderLayer", "ConformerEncoderLayer"] + + +class TransformerEncoderLayer(nn.Layer): + """Encoder layer module.""" + + def __init__( + self, + size: int, + self_attn: nn.Layer, + feed_forward: nn.Layer, + dropout_rate: float, + normalize_before: bool=True, + concat_after: bool=False, ): + """Construct an EncoderLayer object. + + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` or `RelPositionMultiHeadedAttention` + instance can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward`, instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): + True: use layer_norm before each sub-block. + False: to use layer_norm after each sub-block. + concat_after (bool): Whether to concat attention layer's input and + output. + True: x -> x + linear(concat(x, att(x))) + False: x -> x + att(x) + """ + super().__init__() + self.self_attn = self_attn + self.feed_forward = feed_forward + self.norm1 = LayerNorm(size, epsilon=1e-12) + self.norm2 = LayerNorm(size, epsilon=1e-12) + self.dropout = nn.Dropout(dropout_rate) + self.size = size + self.normalize_before = normalize_before + self.concat_after = concat_after + # concat_linear may be not used in forward fuction, + # but will be saved in the *.pt + self.concat_linear = Linear(size + size, size) + + def forward( + self, + x: paddle.Tensor, + mask: paddle.Tensor, + pos_emb: Optional[paddle.Tensor]=None, + mask_pad: Optional[paddle.Tensor]=None, + output_cache: Optional[paddle.Tensor]=None, + cnn_cache: Optional[paddle.Tensor]=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Compute encoded features. + Args: + x (paddle.Tensor): Input tensor (#batch, time, size). + mask (paddle.Tensor): Mask tensor for the input (#batch, time). + pos_emb (paddle.Tensor): just for interface compatibility + to ConformerEncoderLayer + mask_pad (paddle.Tensor): not used here, it's for interface + compatibility to ConformerEncoderLayer + output_cache (paddle.Tensor): Cache tensor of the output + (#batch, time2, size), time2 < time in x. + cnn_cache (paddle.Tensor): not used here, it's for interface + compatibility to ConformerEncoderLayer + Returns: + paddle.Tensor: Output tensor (#batch, time, size). + paddle.Tensor: Mask tensor (#batch, time). + paddle.Tensor: Fake cnn cache tensor for api compatibility with Conformer (#batch, channels, time'). + """ + residual = x + if self.normalize_before: + x = self.norm1(x) + + if output_cache is None: + x_q = x + else: + assert output_cache.shape[0] == x.shape[0] + assert output_cache.shape[1] < x.shape[1] + assert output_cache.shape[2] == self.size + chunk = x.shape[1] - output_cache.shape[1] + x_q = x[:, -chunk:, :] + residual = residual[:, -chunk:, :] + mask = mask[:, -chunk:, :] + + if self.concat_after: + x_concat = paddle.concat( + (x, self.self_attn(x_q, x, x, mask)), axis=-1) + x = residual + self.concat_linear(x_concat) + else: + x = residual + self.dropout(self.self_attn(x_q, x, x, mask)) + if not self.normalize_before: + x = self.norm1(x) + + residual = x + if self.normalize_before: + x = self.norm2(x) + x = residual + self.dropout(self.feed_forward(x)) + if not self.normalize_before: + x = self.norm2(x) + + if output_cache is not None: + x = paddle.concat([output_cache, x], axis=1) + + fake_cnn_cache = paddle.zeros([1], dtype=x.dtype) + return x, mask, fake_cnn_cache + + +class ConformerEncoderLayer(nn.Layer): + """Encoder layer module.""" + + def __init__( + self, + size: int, + self_attn: nn.Layer, + feed_forward: Optional[nn.Layer]=None, + feed_forward_macaron: Optional[nn.Layer]=None, + conv_module: Optional[nn.Layer]=None, + dropout_rate: float=0.1, + normalize_before: bool=True, + concat_after: bool=False, ): + """Construct an EncoderLayer object. + + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` or `RelPositionMultiHeadedAttention` + instance can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward` instance can be used as the argument. + feed_forward_macaron (nn.Layer): Additional feed-forward module + instance. + `PositionwiseFeedForward` instance can be used as the argument. + conv_module (nn.Layer): Convolution module instance. + `ConvlutionModule` instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): + True: use layer_norm before each sub-block. + False: use layer_norm after each sub-block. + concat_after (bool): Whether to concat attention layer's input and + output. + True: x -> x + linear(concat(x, att(x))) + False: x -> x + att(x) + """ + super().__init__() + self.self_attn = self_attn + self.feed_forward = feed_forward + self.feed_forward_macaron = feed_forward_macaron + self.conv_module = conv_module + self.norm_ff = LayerNorm(size, epsilon=1e-12) # for the FNN module + self.norm_mha = LayerNorm(size, epsilon=1e-12) # for the MHA module + if feed_forward_macaron is not None: + self.norm_ff_macaron = LayerNorm(size, epsilon=1e-12) + self.ff_scale = 0.5 + else: + self.ff_scale = 1.0 + if self.conv_module is not None: + self.norm_conv = LayerNorm( + size, epsilon=1e-12) # for the CNN module + self.norm_final = LayerNorm( + size, epsilon=1e-12) # for the final output of the block + self.dropout = nn.Dropout(dropout_rate) + self.size = size + self.normalize_before = normalize_before + self.concat_after = concat_after + self.concat_linear = Linear(size + size, size) + + def forward( + self, + x: paddle.Tensor, + mask: paddle.Tensor, + pos_emb: paddle.Tensor, + mask_pad: Optional[paddle.Tensor]=None, + output_cache: Optional[paddle.Tensor]=None, + cnn_cache: Optional[paddle.Tensor]=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Compute encoded features. + Args: + x (paddle.Tensor): (#batch, time, size) + mask (paddle.Tensor): Mask tensor for the input (#batch, time,time). + pos_emb (paddle.Tensor): positional encoding, must not be None + for ConformerEncoderLayer. + mask_pad (paddle.Tensor): batch padding mask used for conv module, (B, 1, T). + output_cache (paddle.Tensor): Cache tensor of the encoder output + (#batch, time2, size), time2 < time in x. + cnn_cache (paddle.Tensor): Convolution cache in conformer layer + Returns: + paddle.Tensor: Output tensor (#batch, time, size). + paddle.Tensor: Mask tensor (#batch, time). + paddle.Tensor: New cnn cache tensor (#batch, channels, time'). + """ + # whether to use macaron style FFN + if self.feed_forward_macaron is not None: + residual = x + if self.normalize_before: + x = self.norm_ff_macaron(x) + x = residual + self.ff_scale * self.dropout( + self.feed_forward_macaron(x)) + if not self.normalize_before: + x = self.norm_ff_macaron(x) + + # multi-headed self-attention module + residual = x + if self.normalize_before: + x = self.norm_mha(x) + + if output_cache is None: + x_q = x + else: + assert output_cache.shape[0] == x.shape[0] + assert output_cache.shape[1] < x.shape[1] + assert output_cache.shape[2] == self.size + chunk = x.shape[1] - output_cache.shape[1] + x_q = x[:, -chunk:, :] + residual = residual[:, -chunk:, :] + mask = mask[:, -chunk:, :] + + x_att = self.self_attn(x_q, x, x, pos_emb, mask) + + if self.concat_after: + x_concat = paddle.concat((x, x_att), axis=-1) + x = residual + self.concat_linear(x_concat) + else: + x = residual + self.dropout(x_att) + + if not self.normalize_before: + x = self.norm_mha(x) + + # convolution module + # Fake new cnn cache here, and then change it in conv_module + new_cnn_cache = paddle.zeros([1], dtype=x.dtype) + if self.conv_module is not None: + residual = x + if self.normalize_before: + x = self.norm_conv(x) + + x, new_cnn_cache = self.conv_module(x, mask_pad, cnn_cache) + x = residual + self.dropout(x) + + if not self.normalize_before: + x = self.norm_conv(x) + + # feed forward module + residual = x + if self.normalize_before: + x = self.norm_ff(x) + + x = residual + self.ff_scale * self.dropout(self.feed_forward(x)) + + if not self.normalize_before: + x = self.norm_ff(x) + + if self.conv_module is not None: + x = self.norm_final(x) + + if output_cache is not None: + x = paddle.concat([output_cache, x], axis=1) + + return x, mask, new_cnn_cache diff --git a/ernie-sat/paddlespeech/s2t/modules/initializer.py b/ernie-sat/paddlespeech/s2t/modules/initializer.py new file mode 100644 index 0000000..30a04e4 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/initializer.py @@ -0,0 +1,172 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from paddle.fluid import framework +from paddle.fluid import unique_name +from paddle.fluid.core import VarDesc +from paddle.fluid.initializer import MSRAInitializer + +__all__ = ['KaimingUniform'] + + +class KaimingUniform(MSRAInitializer): + r"""Implements the Kaiming Uniform initializer + + This class implements the weight initialization from the paper + `Delving Deep into Rectifiers: Surpassing Human-Level Performance on + ImageNet Classification `_ + by Kaiming He, Xiangyu Zhang, Shaoqing Ren and Jian Sun. This is a + robust initialization method that particularly considers the rectifier + nonlinearities. + + In case of Uniform distribution, the range is [-x, x], where + + .. math:: + + x = \sqrt{\frac{1.0}{fan\_in}} + + In case of Normal distribution, the mean is 0 and the standard deviation + is + + .. math:: + + \sqrt{\\frac{2.0}{fan\_in}} + + Args: + fan_in (float32|None): fan_in for Kaiming uniform Initializer. If None, it is\ + inferred from the variable. default is None. + + Note: + It is recommended to set fan_in to None for most cases. + + Examples: + .. code-block:: python + + import paddle + import paddle.nn as nn + + linear = nn.Linear(2, + 4, + weight_attr=nn.initializer.KaimingUniform()) + data = paddle.rand([30, 10, 2], dtype='float32') + res = linear(data) + + """ + + def __init__(self, fan_in=None): + super(KaimingUniform, self).__init__( + uniform=True, fan_in=fan_in, seed=0) + + def __call__(self, var, block=None): + """Initialize the input tensor with MSRA initialization. + + Args: + var(Tensor): Tensor that needs to be initialized. + block(Block, optional): The block in which initialization ops + should be added. Used in static graph only, default None. + + Returns: + The initialization op + """ + block = self._check_block(block) + + assert isinstance(var, framework.Variable) + assert isinstance(block, framework.Block) + f_in, f_out = self._compute_fans(var) + + # If fan_in is passed, use it + fan_in = f_in if self._fan_in is None else self._fan_in + + if self._seed == 0: + self._seed = block.program.random_seed + + # to be compatible of fp16 initalizers + if var.dtype == VarDesc.VarType.FP16 or ( + var.dtype == VarDesc.VarType.BF16 and not self._uniform): + out_dtype = VarDesc.VarType.FP32 + out_var = block.create_var( + name=unique_name.generate( + ".".join(['masra_init', var.name, 'tmp'])), + shape=var.shape, + dtype=out_dtype, + type=VarDesc.VarType.LOD_TENSOR, + persistable=False) + else: + out_dtype = var.dtype + out_var = var + + if self._uniform: + limit = np.sqrt(1.0 / float(fan_in)) + op = block.append_op( + type="uniform_random", + inputs={}, + outputs={"Out": out_var}, + attrs={ + "shape": out_var.shape, + "dtype": int(out_dtype), + "min": -limit, + "max": limit, + "seed": self._seed + }, + stop_gradient=True) + + else: + std = np.sqrt(2.0 / float(fan_in)) + op = block.append_op( + type="gaussian_random", + outputs={"Out": out_var}, + attrs={ + "shape": out_var.shape, + "dtype": int(out_dtype), + "mean": 0.0, + "std": std, + "seed": self._seed + }, + stop_gradient=True) + + if var.dtype == VarDesc.VarType.FP16 or ( + var.dtype == VarDesc.VarType.BF16 and not self._uniform): + block.append_op( + type="cast", + inputs={"X": out_var}, + outputs={"Out": var}, + attrs={"in_dtype": out_var.dtype, + "out_dtype": var.dtype}) + + if not framework.in_dygraph_mode(): + var.op = op + return op + + +class DefaultInitializerContext(object): + """ + egs: + with DefaultInitializerContext("kaiming_uniform"): + code for setup_model + """ + + def __init__(self, init_type=None): + self.init_type = init_type + + def __enter__(self): + if self.init_type is None: + return + else: + from paddlespeech.s2t.modules import align + align.global_init_type = self.init_type + return + + def __exit__(self, exc_type, exc_val, exc_tb): + from paddlespeech.s2t.modules import align + align.global_init_type = None diff --git a/ernie-sat/paddlespeech/s2t/modules/loss.py b/ernie-sat/paddlespeech/s2t/modules/loss.py new file mode 100644 index 0000000..c7d9bd4 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/loss.py @@ -0,0 +1,185 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +import inspect + +import paddle +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ['CTCLoss', "LabelSmoothingLoss"] + + +class CTCLoss(nn.Layer): + def __init__(self, + blank=0, + reduction='sum', + batch_average=False, + grad_norm_type=None): + super().__init__() + # last token id as blank id + self.loss = nn.CTCLoss(blank=blank, reduction=reduction) + self.batch_average = batch_average + + logger.info( + f"CTCLoss Loss reduction: {reduction}, div-bs: {batch_average}") + logger.info(f"CTCLoss Grad Norm Type: {grad_norm_type}") + + assert grad_norm_type in ('instance', 'batch', 'frame', None) + self.norm_by_times = False + self.norm_by_batchsize = False + self.norm_by_total_logits_len = False + if grad_norm_type is None: + # no grad norm + pass + elif grad_norm_type == 'instance': + self.norm_by_times = True + elif grad_norm_type == 'batch': + self.norm_by_batchsize = True + elif grad_norm_type == 'frame': + self.norm_by_total_logits_len = True + else: + raise ValueError(f"CTCLoss Grad Norm no support {grad_norm_type}") + kwargs = { + "norm_by_times": self.norm_by_times, + "norm_by_batchsize": self.norm_by_batchsize, + "norm_by_total_logits_len": self.norm_by_total_logits_len, + } + + # Derive only the args which the func has + try: + param = inspect.signature(self.loss.forward).parameters + except ValueError: + # Some function, e.g. built-in function, are failed + param = {} + self._kwargs = {k: v for k, v in kwargs.items() if k in param} + _notin = {k: v for k, v in kwargs.items() if k not in param} + logger.info(f"{self.loss} kwargs:{self._kwargs}, not support: {_notin}") + + def forward(self, logits, ys_pad, hlens, ys_lens): + """Compute CTC loss. + + Args: + logits ([paddle.Tensor]): [B, Tmax, D] + ys_pad ([paddle.Tensor]): [B, Tmax] + hlens ([paddle.Tensor]): [B] + ys_lens ([paddle.Tensor]): [B] + + Returns: + [paddle.Tensor]: scalar. If reduction is 'none', then (N), where N = \text{batch size}. + """ + B = paddle.shape(logits)[0] + # warp-ctc need logits, and do softmax on logits by itself + # warp-ctc need activation with shape [T, B, V + 1] + # logits: (B, L, D) -> (L, B, D) + logits = logits.transpose([1, 0, 2]) + ys_pad = ys_pad.astype(paddle.int32) + loss = self.loss(logits, ys_pad, hlens, ys_lens, **self._kwargs) + if self.batch_average: + # Batch-size average + loss = loss / B + return loss + + +class LabelSmoothingLoss(nn.Layer): + """Label-smoothing loss. + In a standard CE loss, the label's data distribution is: + [0,1,2] -> + [ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + In the smoothing version CE Loss,some probabilities + are taken from the true label prob (1.0) and are divided + among other labels. + e.g. + smoothing=0.1 + [0,1,2] -> + [ + [0.9, 0.05, 0.05], + [0.05, 0.9, 0.05], + [0.05, 0.05, 0.9], + ] + + """ + + def __init__(self, + size: int, + padding_idx: int, + smoothing: float, + normalize_length: bool=False): + """Label-smoothing loss. + + Args: + size (int): the number of class + padding_idx (int): padding class id which will be ignored for loss + smoothing (float): smoothing rate (0.0 means the conventional CE) + normalize_length (bool): + True, normalize loss by sequence length; + False, normalize loss by batch size. + Defaults to False. + """ + super().__init__() + self.size = size + self.padding_idx = padding_idx + self.smoothing = smoothing + self.confidence = 1.0 - smoothing + self.normalize_length = normalize_length + self.criterion = nn.KLDivLoss(reduction="none") + + def forward(self, x: paddle.Tensor, target: paddle.Tensor) -> paddle.Tensor: + """Compute loss between x and target. + The model outputs and data labels tensors are flatten to + (batch*seqlen, class) shape and a mask is applied to the + padding part which should not be calculated for loss. + + Args: + x (paddle.Tensor): prediction (batch, seqlen, class) + target (paddle.Tensor): + target signal masked with self.padding_id (batch, seqlen) + Returns: + loss (paddle.Tensor) : The KL loss, scalar float value + """ + B, T, D = paddle.shape(x) + assert D == self.size + x = x.reshape((-1, self.size)) + target = target.reshape([-1]) + + # use zeros_like instead of torch.no_grad() for true_dist, + # since no_grad() can not be exported by JIT + true_dist = paddle.full_like(x, self.smoothing / (self.size - 1)) + ignore = target == self.padding_idx # (B,) + + #TODO(Hui Zhang): target = target * (1 - ignore) # avoid -1 index + target = target.masked_fill(ignore, 0) # avoid -1 index + # true_dist.scatter_(1, target.unsqueeze(1), self.confidence) + target_mask = F.one_hot(target, self.size) + true_dist *= (1 - target_mask) + true_dist += target_mask * self.confidence + + kl = self.criterion(F.log_softmax(x, axis=1), true_dist) + + #TODO(Hui Zhang): sum not support bool type + #total = len(target) - int(ignore.sum()) + total = len(target) - int(ignore.type_as(target).sum()) + denom = total if self.normalize_length else B + #numer = (kl * (1 - ignore)).sum() + numer = kl.masked_fill(ignore.unsqueeze(1), 0).sum() + return numer / denom diff --git a/ernie-sat/paddlespeech/s2t/modules/mask.py b/ernie-sat/paddlespeech/s2t/modules/mask.py new file mode 100644 index 0000000..1f66c01 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/mask.py @@ -0,0 +1,277 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +import paddle + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "make_xs_mask", "make_pad_mask", "make_non_pad_mask", "subsequent_mask", + "subsequent_chunk_mask", "add_optional_chunk_mask", "mask_finished_scores", + "mask_finished_preds" +] + + +def make_xs_mask(xs: paddle.Tensor, pad_value=0.0) -> paddle.Tensor: + """Maks mask tensor containing indices of non-padded part. + Args: + xs (paddle.Tensor): (B, T, D), zeros for pad. + Returns: + paddle.Tensor: Mask Tensor indices of non-padded part. (B, T) + """ + pad_frame = paddle.full([1, 1, xs.shape[-1]], pad_value, dtype=xs.dtype) + mask = xs != pad_frame + mask = mask.all(axis=-1) + return mask + + +def make_pad_mask(lengths: paddle.Tensor) -> paddle.Tensor: + """Make mask tensor containing indices of padded part. + See description of make_non_pad_mask. + Args: + lengths (paddle.Tensor): Batch of lengths (B,). + Returns: + paddle.Tensor: Mask tensor containing indices of padded part. + (B, T) + Examples: + >>> lengths = [5, 3, 2] + >>> make_pad_mask(lengths) + masks = [[0, 0, 0, 0 ,0], + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]] + """ + # (TODO: Hui Zhang): jit not support Tensor.dim() and Tensor.ndim + # assert lengths.dim() == 1 + batch_size = int(lengths.shape[0]) + max_len = int(lengths.max()) + seq_range = paddle.arange(0, max_len, dtype=paddle.int64) + seq_range_expand = seq_range.unsqueeze(0).expand([batch_size, max_len]) + seq_length_expand = lengths.unsqueeze(-1) + mask = seq_range_expand >= seq_length_expand + return mask + + +def make_non_pad_mask(lengths: paddle.Tensor) -> paddle.Tensor: + """Make mask tensor containing indices of non-padded part. + The sequences in a batch may have different lengths. To enable + batch computing, padding is need to make all sequence in same + size. To avoid the padding part pass value to context dependent + block such as attention or convolution , this padding part is + masked. + This pad_mask is used in both encoder and decoder. + 1 for non-padded part and 0 for padded part. + Args: + lengths (paddle.Tensor): Batch of lengths (B,). + Returns: + paddle.Tensor: mask tensor containing indices of padded part. + (B, T) + Examples: + >>> lengths = [5, 3, 2] + >>> make_non_pad_mask(lengths) + masks = [[1, 1, 1, 1 ,1], + [1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]] + """ + #return ~make_pad_mask(lengths) + return make_pad_mask(lengths).logical_not() + + +def subsequent_mask(size: int) -> paddle.Tensor: + """Create mask for subsequent steps (size, size). + This mask is used only in decoder which works in an auto-regressive mode. + This means the current step could only do attention with its left steps. + In encoder, fully attention is used when streaming is not necessary and + the sequence is not long. In this case, no attention mask is needed. + When streaming is need, chunk-based attention is used in encoder. See + subsequent_chunk_mask for the chunk-based attention mask. + Args: + size (int): size of mask + Returns: + paddle.Tensor: mask, [size, size] + Examples: + >>> subsequent_mask(3) + [[1, 0, 0], + [1, 1, 0], + [1, 1, 1]] + """ + ret = paddle.ones([size, size], dtype=paddle.bool) + #TODO(Hui Zhang): tril not support bool + #return paddle.tril(ret) + ret = ret.astype(paddle.float) + ret = paddle.tril(ret) + ret = ret.astype(paddle.bool) + return ret + + +def subsequent_chunk_mask( + size: int, + chunk_size: int, + num_left_chunks: int=-1, ) -> paddle.Tensor: + """Create mask for subsequent steps (size, size) with chunk size, + this is for streaming encoder + Args: + size (int): size of mask + chunk_size (int): size of chunk + num_left_chunks (int): number of left chunks + <0: use full chunk + >=0: use num_left_chunks + Returns: + paddle.Tensor: mask, [size, size] + Examples: + >>> subsequent_chunk_mask(4, 2) + [[1, 1, 0, 0], + [1, 1, 0, 0], + [1, 1, 1, 1], + [1, 1, 1, 1]] + """ + ret = paddle.zeros([size, size], dtype=paddle.bool) + for i in range(size): + if num_left_chunks < 0: + start = 0 + else: + start = max(0, (i // chunk_size - num_left_chunks) * chunk_size) + ending = min(size, (i // chunk_size + 1) * chunk_size) + ret[i, start:ending] = True + return ret + + +def add_optional_chunk_mask(xs: paddle.Tensor, + masks: paddle.Tensor, + use_dynamic_chunk: bool, + use_dynamic_left_chunk: bool, + decoding_chunk_size: int, + static_chunk_size: int, + num_decoding_left_chunks: int): + """ Apply optional mask for encoder. + Args: + xs (paddle.Tensor): padded input, (B, L, D), L for max length + mask (paddle.Tensor): mask for xs, (B, 1, L) + use_dynamic_chunk (bool): whether to use dynamic chunk or not + use_dynamic_left_chunk (bool): whether to use dynamic left chunk for + training. + decoding_chunk_size (int): decoding chunk size for dynamic chunk, it's + 0: default for training, use random dynamic chunk. + <0: for decoding, use full chunk. + >0: for decoding, use fixed chunk size as set. + static_chunk_size (int): chunk size for static chunk training/decoding + if it's greater than 0, if use_dynamic_chunk is true, + this parameter will be ignored + num_decoding_left_chunks (int): number of left chunks, this is for decoding, + the chunk size is decoding_chunk_size. + >=0: use num_decoding_left_chunks + <0: use all left chunks + Returns: + paddle.Tensor: chunk mask of the input xs. + """ + # Whether to use chunk mask or not + if use_dynamic_chunk: + max_len = xs.shape[1] + if decoding_chunk_size < 0: + chunk_size = max_len + num_left_chunks = -1 + elif decoding_chunk_size > 0: + chunk_size = decoding_chunk_size + num_left_chunks = num_decoding_left_chunks + else: + # chunk size is either [1, 25] or full context(max_len). + # Since we use 4 times subsampling and allow up to 1s(100 frames) + # delay, the maximum frame is 100 / 4 = 25. + chunk_size = int(paddle.randint(1, max_len, (1, ))) + num_left_chunks = -1 + if chunk_size > max_len // 2: + chunk_size = max_len + else: + chunk_size = chunk_size % 25 + 1 + if use_dynamic_left_chunk: + max_left_chunks = (max_len - 1) // chunk_size + num_left_chunks = int( + paddle.randint(0, max_left_chunks, (1, ))) + chunk_masks = subsequent_chunk_mask(xs.shape[1], chunk_size, + num_left_chunks) # (L, L) + chunk_masks = chunk_masks.unsqueeze(0) # (1, L, L) + # chunk_masks = masks & chunk_masks # (B, L, L) + chunk_masks = masks.logical_and(chunk_masks) # (B, L, L) + elif static_chunk_size > 0: + num_left_chunks = num_decoding_left_chunks + chunk_masks = subsequent_chunk_mask(xs.shape[1], static_chunk_size, + num_left_chunks) # (L, L) + chunk_masks = chunk_masks.unsqueeze(0) # (1, L, L) + # chunk_masks = masks & chunk_masks # (B, L, L) + chunk_masks = masks.logical_and(chunk_masks) # (B, L, L) + else: + chunk_masks = masks + return chunk_masks + + +def mask_finished_scores(score: paddle.Tensor, + flag: paddle.Tensor) -> paddle.Tensor: + """ + If a sequence is finished, we only allow one alive branch. This function + aims to give one branch a zero score and the rest -inf score. + Args: + score (paddle.Tensor): A real value array with shape + (batch_size * beam_size, beam_size). + flag (paddle.Tensor): A bool array with shape + (batch_size * beam_size, 1). + Returns: + paddle.Tensor: (batch_size * beam_size, beam_size). + Examples: + flag: tensor([[ True], + [False]]) + score: tensor([[-0.3666, -0.6664, 0.6019], + [-1.1490, -0.2948, 0.7460]]) + unfinished: tensor([[False, True, True], + [False, False, False]]) + finished: tensor([[ True, False, False], + [False, False, False]]) + return: tensor([[ 0.0000, -inf, -inf], + [-1.1490, -0.2948, 0.7460]]) + """ + beam_size = score.shape[-1] + zero_mask = paddle.zeros_like(flag, dtype=paddle.bool) + if beam_size > 1: + unfinished = paddle.concat( + (zero_mask, flag.tile([1, beam_size - 1])), axis=1) + finished = paddle.concat( + (flag, zero_mask.tile([1, beam_size - 1])), axis=1) + else: + unfinished = zero_mask + finished = flag + + # infs = paddle.ones_like(score) * -float('inf') + # score = paddle.where(unfinished, infs, score) + # score = paddle.where(finished, paddle.zeros_like(score), score) + score.masked_fill_(unfinished, -float('inf')) + score.masked_fill_(finished, 0) + return score + + +def mask_finished_preds(pred: paddle.Tensor, flag: paddle.Tensor, + eos: int) -> paddle.Tensor: + """ + If a sequence is finished, all of its branch should be + Args: + pred (paddle.Tensor): A int array with shape + (batch_size * beam_size, beam_size). + flag (paddle.Tensor): A bool array with shape + (batch_size * beam_size, 1). + Returns: + paddle.Tensor: (batch_size * beam_size). + """ + beam_size = pred.shape[-1] + finished = flag.repeat(1, beam_size) + return pred.masked_fill_(finished, eos) diff --git a/ernie-sat/paddlespeech/s2t/modules/positionwise_feed_forward.py b/ernie-sat/paddlespeech/s2t/modules/positionwise_feed_forward.py new file mode 100644 index 0000000..c2725dc --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/positionwise_feed_forward.py @@ -0,0 +1,60 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Positionwise feed forward layer definition.""" +import paddle +from paddle import nn + +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["PositionwiseFeedForward"] + + +class PositionwiseFeedForward(nn.Layer): + """Positionwise feed forward layer.""" + + def __init__(self, + idim: int, + hidden_units: int, + dropout_rate: float, + activation: nn.Layer=nn.ReLU()): + """Construct a PositionwiseFeedForward object. + + FeedForward are appied on each position of the sequence. + The output dim is same with the input dim. + + Args: + idim (int): Input dimenstion. + hidden_units (int): The number of hidden units. + dropout_rate (float): Dropout rate. + activation (paddle.nn.Layer): Activation function + """ + super().__init__() + self.w_1 = Linear(idim, hidden_units) + self.activation = activation + self.dropout = nn.Dropout(dropout_rate) + self.w_2 = Linear(hidden_units, idim) + + def forward(self, xs: paddle.Tensor) -> paddle.Tensor: + """Forward function. + Args: + xs: input tensor (B, Lmax, D) + Returns: + output tensor, (B, Lmax, D) + """ + return self.w_2(self.dropout(self.activation(self.w_1(xs)))) diff --git a/ernie-sat/paddlespeech/s2t/modules/subsampling.py b/ernie-sat/paddlespeech/s2t/modules/subsampling.py new file mode 100644 index 0000000..88451dd --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/modules/subsampling.py @@ -0,0 +1,250 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# Copyright 2019 Mobvoi Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +"""Subsampling layer definition.""" +from typing import Tuple + +import paddle +from paddle import nn + +from paddlespeech.s2t.modules.align import Conv2D +from paddlespeech.s2t.modules.align import LayerNorm +from paddlespeech.s2t.modules.align import Linear +from paddlespeech.s2t.modules.embedding import PositionalEncoding +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "LinearNoSubsampling", "Conv2dSubsampling4", "Conv2dSubsampling6", + "Conv2dSubsampling8" +] + + +class BaseSubsampling(nn.Layer): + def __init__(self, pos_enc_class: nn.Layer=PositionalEncoding): + super().__init__() + self.pos_enc = pos_enc_class + # window size = (1 + right_context) + (chunk_size -1) * subsampling_rate + self.right_context = 0 + # stride = subsampling_rate * chunk_size + self.subsampling_rate = 1 + + def position_encoding(self, offset: int, size: int) -> paddle.Tensor: + return self.pos_enc.position_encoding(offset, size) + + +class LinearNoSubsampling(BaseSubsampling): + """Linear transform the input without subsampling.""" + + def __init__(self, + idim: int, + odim: int, + dropout_rate: float, + pos_enc_class: nn.Layer=PositionalEncoding): + """Construct an linear object. + Args: + idim (int): Input dimension. + odim (int): Output dimension. + dropout_rate (float): Dropout rate. + pos_enc_class (PositionalEncoding): position encoding class + """ + super().__init__(pos_enc_class) + self.out = nn.Sequential( + Linear(idim, odim), + LayerNorm(odim, epsilon=1e-12), + nn.Dropout(dropout_rate), + nn.ReLU(), ) + self.right_context = 0 + self.subsampling_rate = 1 + + def forward(self, x: paddle.Tensor, x_mask: paddle.Tensor, offset: int=0 + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Input x. + Args: + x (paddle.Tensor): Input tensor (#batch, time, idim). + x_mask (paddle.Tensor): Input mask (#batch, 1, time). + offset (int): position encoding offset. + Returns: + paddle.Tensor: linear input tensor (#batch, time', odim), + where time' = time . + paddle.Tensor: positional encoding + paddle.Tensor: linear input mask (#batch, 1, time'), + where time' = time . + """ + x = self.out(x) + x, pos_emb = self.pos_enc(x, offset) + return x, pos_emb, x_mask + + +class Conv2dSubsampling(BaseSubsampling): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class Conv2dSubsampling4(Conv2dSubsampling): + """Convolutional 2D subsampling (to 1/4 length).""" + + def __init__(self, + idim: int, + odim: int, + dropout_rate: float, + pos_enc_class: nn.Layer=PositionalEncoding): + """Construct an Conv2dSubsampling4 object. + + Args: + idim (int): Input dimension. + odim (int): Output dimension. + dropout_rate (float): Dropout rate. + """ + super().__init__(pos_enc_class) + self.conv = nn.Sequential( + Conv2D(1, odim, 3, 2), + nn.ReLU(), + Conv2D(odim, odim, 3, 2), + nn.ReLU(), ) + self.out = nn.Sequential( + Linear(odim * (((idim - 1) // 2 - 1) // 2), odim)) + self.subsampling_rate = 4 + # The right context for every conv layer is computed by: + # (kernel_size - 1) * frame_rate_of_this_layer + # 6 = (3 - 1) * 1 + (3 - 1) * 2 + self.right_context = 6 + + def forward(self, x: paddle.Tensor, x_mask: paddle.Tensor, offset: int=0 + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Subsample x. + Args: + x (paddle.Tensor): Input tensor (#batch, time, idim). + x_mask (paddle.Tensor): Input mask (#batch, 1, time). + offset (int): position encoding offset. + Returns: + paddle.Tensor: Subsampled tensor (#batch, time', odim), + where time' = time // 4. + paddle.Tensor: positional encoding + paddle.Tensor: Subsampled mask (#batch, 1, time'), + where time' = time // 4. + """ + x = x.unsqueeze(1) # (b, c=1, t, f) + x = self.conv(x) + b, c, t, f = paddle.shape(x) + x = self.out(x.transpose([0, 2, 1, 3]).reshape([b, t, c * f])) + x, pos_emb = self.pos_enc(x, offset) + return x, pos_emb, x_mask[:, :, :-2:2][:, :, :-2:2] + + +class Conv2dSubsampling6(Conv2dSubsampling): + """Convolutional 2D subsampling (to 1/6 length).""" + + def __init__(self, + idim: int, + odim: int, + dropout_rate: float, + pos_enc_class: nn.Layer=PositionalEncoding): + """Construct an Conv2dSubsampling6 object. + + Args: + idim (int): Input dimension. + odim (int): Output dimension. + dropout_rate (float): Dropout rate. + pos_enc (PositionalEncoding): Custom position encoding layer. + """ + super().__init__(pos_enc_class) + self.conv = nn.Sequential( + Conv2D(1, odim, 3, 2), + nn.ReLU(), + Conv2D(odim, odim, 5, 3), + nn.ReLU(), ) + # O = (I - F + Pstart + Pend) // S + 1 + # when Padding == 0, O = (I - F - S) // S + self.linear = Linear(odim * (((idim - 1) // 2 - 2) // 3), odim) + # The right context for every conv layer is computed by: + # (kernel_size - 1) * frame_rate_of_this_layer + # 10 = (3 - 1) * 1 + (5 - 1) * 2 + self.subsampling_rate = 6 + self.right_context = 10 + + def forward(self, x: paddle.Tensor, x_mask: paddle.Tensor, offset: int=0 + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Subsample x. + Args: + x (paddle.Tensor): Input tensor (#batch, time, idim). + x_mask (paddle.Tensor): Input mask (#batch, 1, time). + offset (int): position encoding offset. + Returns: + paddle.Tensor: Subsampled tensor (#batch, time', odim), + where time' = time // 6. + paddle.Tensor: positional encoding + paddle.Tensor: Subsampled mask (#batch, 1, time'), + where time' = time // 6. + """ + x = x.unsqueeze(1) # (b, c, t, f) + x = self.conv(x) + b, c, t, f = paddle.shape(x) + x = self.linear(x.transpose([0, 2, 1, 3]).reshape([b, t, c * f])) + x, pos_emb = self.pos_enc(x, offset) + return x, pos_emb, x_mask[:, :, :-2:2][:, :, :-4:3] + + +class Conv2dSubsampling8(Conv2dSubsampling): + """Convolutional 2D subsampling (to 1/8 length).""" + + def __init__(self, + idim: int, + odim: int, + dropout_rate: float, + pos_enc_class: nn.Layer=PositionalEncoding): + """Construct an Conv2dSubsampling8 object. + + Args: + idim (int): Input dimension. + odim (int): Output dimension. + dropout_rate (float): Dropout rate. + """ + super().__init__(pos_enc_class) + self.conv = nn.Sequential( + Conv2D(1, odim, 3, 2), + nn.ReLU(), + Conv2D(odim, odim, 3, 2), + nn.ReLU(), + Conv2D(odim, odim, 3, 2), + nn.ReLU(), ) + self.linear = Linear(odim * ((((idim - 1) // 2 - 1) // 2 - 1) // 2), + odim) + self.subsampling_rate = 8 + # The right context for every conv layer is computed by: + # (kernel_size - 1) * frame_rate_of_this_layer + # 14 = (3 - 1) * 1 + (3 - 1) * 2 + (3 - 1) * 4 + self.right_context = 14 + + def forward(self, x: paddle.Tensor, x_mask: paddle.Tensor, offset: int=0 + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Subsample x. + Args: + x (paddle.Tensor): Input tensor (#batch, time, idim). + x_mask (paddle.Tensor): Input mask (#batch, 1, time). + offset (int): position encoding offset. + Returns: + paddle.Tensor: Subsampled tensor (#batch, time', odim), + where time' = time // 8. + paddle.Tensor: positional encoding + paddle.Tensor: Subsampled mask (#batch, 1, time'), + where time' = time // 8. + """ + x = x.unsqueeze(1) # (b, c, t, f) + x = self.conv(x) + x = self.linear(x.transpose([0, 2, 1, 3]).reshape([b, t, c * f])) + x, pos_emb = self.pos_enc(x, offset) + return x, pos_emb, x_mask[:, :, :-2:2][:, :, :-2:2][:, :, :-2:2] diff --git a/ernie-sat/paddlespeech/s2t/training/__init__.py b/ernie-sat/paddlespeech/s2t/training/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/training/cli.py b/ernie-sat/paddlespeech/s2t/training/cli.py new file mode 100644 index 0000000..bb85732 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/cli.py @@ -0,0 +1,127 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse + + +class ExtendAction(argparse.Action): + """ + [Since Python 3.8, the "extend" is available directly in stdlib] + (https://docs.python.org/3.8/library/argparse.html#action). + If you only have to support 3.8+ then defining it yourself is no longer required. + Usage of stdlib "extend" action is exactly the same way as this answer originally described: + """ + + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest) or [] + items.extend(values) + setattr(namespace, self.dest, items) + + +class LoadFromFile(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + with values as f: + # parse arguments in the file and store them in the target namespace + parser.parse_args(f.read().split(), namespace) + + +def default_argument_parser(parser=None): + r"""A simple yet genral argument parser for experiments with t2s. + + This is used in examples with t2s. And it is intended to be used by + other experiments with t2s. It requires a minimal set of command line + arguments to start a training script. + + The ``--config`` and ``--opts`` are used for overwrite the deault + configuration. + + The ``--data`` and ``--output`` specifies the data path and output path. + Resuming training from existing progress at the output directory is the + intended default behavior. + + The ``--checkpoint_path`` specifies the checkpoint to load from. + + The ``--ngpu`` specifies how to run the training. + + + See Also + -------- + paddlespeech.t2s.training.experiment + Returns + ------- + argparse.ArgumentParser + the parser + """ + if parser is None: + parser = argparse.ArgumentParser() + + parser.register('action', 'extend', ExtendAction) + parser.add_argument( + '--conf', type=open, action=LoadFromFile, help="config file.") + + train_group = parser.add_argument_group( + title='Train Options', description=None) + train_group.add_argument( + "--seed", + type=int, + default=None, + help="seed to use for paddle, np and random. None or 0 for random, else set seed." + ) + train_group.add_argument( + "--ngpu", + type=int, + default=1, + help="number of parallel processes. 0 for cpu.") + train_group.add_argument( + "--config", metavar="CONFIG_FILE", help="config file.") + train_group.add_argument( + "--output", metavar="CKPT_DIR", help="path to save checkpoint.") + train_group.add_argument( + "--checkpoint_path", type=str, help="path to load checkpoint") + train_group.add_argument( + "--opts", + action='extend', + nargs=2, + metavar=('key', 'val'), + help="overwrite --config field, passing (KEY VALUE) pairs") + train_group.add_argument( + "--dump-config", metavar="FILE", help="dump config to `this` file.") + + test_group = parser.add_argument_group( + title='Test Options', description=None) + + test_group.add_argument( + "--decode_cfg", + metavar="DECODE_CONFIG_FILE", + help="decode config file.") + + profile_group = parser.add_argument_group( + title='Benchmark Options', description=None) + profile_group.add_argument( + '--profiler-options', + type=str, + default=None, + help='The option of profiler, which should be in format \"key1=value1;key2=value2;key3=value3\".' + ) + profile_group.add_argument( + '--benchmark-batch-size', + type=int, + default=None, + help='batch size for benchmark.') + profile_group.add_argument( + '--benchmark-max-step', + type=int, + default=None, + help='max iteration for benchmark.') + + return parser diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/__init__.py b/ernie-sat/paddlespeech/s2t/training/extensions/__init__.py new file mode 100644 index 0000000..6ad0415 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/__init__.py @@ -0,0 +1,41 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Callable + +from .extension import Extension + + +def make_extension(trigger: Callable=None, + default_name: str=None, + priority: int=None, + finalizer: Callable=None, + initializer: Callable=None, + on_error: Callable=None): + """Make an Extension-like object by injecting required attributes to it. + """ + if trigger is None: + trigger = Extension.trigger + if priority is None: + priority = Extension.priority + + def decorator(ext): + ext.trigger = trigger + ext.default_name = default_name or ext.__name__ + ext.priority = priority + ext.finalize = finalizer + ext.on_error = on_error + ext.initialize = initializer + return ext + + return decorator diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/evaluator.py b/ernie-sat/paddlespeech/s2t/training/extensions/evaluator.py new file mode 100644 index 0000000..b96a481 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/evaluator.py @@ -0,0 +1,102 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from typing import Dict + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer + +from . import extension +from ..reporter import DictSummary +from ..reporter import ObsScope +from ..reporter import report +from ..timer import Timer +from paddlespeech.s2t.utils.log import Log +logger = Log(__name__).getlog() + + +class StandardEvaluator(extension.Extension): + + trigger = (1, 'epoch') + default_name = 'validation' + priority = extension.PRIORITY_WRITER + + name = None + + def __init__(self, model: Layer, dataloader: DataLoader): + # it is designed to hold multiple models + models = {"main": model} + self.models: Dict[str, Layer] = models + self.model = model + + # dataloaders + self.dataloader = dataloader + + def evaluate_core(self, batch): + # compute + self.model(batch) # you may report here + return + + def evaluate_sync(self, data): + # dist sync `evaluate_core` outputs + if data is None: + return + + numerator, denominator = data + if dist.get_world_size() > 1: + numerator = paddle.to_tensor(numerator) + denominator = paddle.to_tensor(denominator) + # the default operator in all_reduce function is sum. + dist.all_reduce(numerator) + dist.all_reduce(denominator) + value = numerator / denominator + value = float(value) + else: + value = numerator / denominator + # used for `snapshort` to do kbest save. + report("VALID/LOSS", value) + logger.info(f"Valid: all-reduce loss {value}") + + def evaluate(self): + # switch to eval mode + for model in self.models.values(): + model.eval() + + # to average evaluation metrics + summary = DictSummary() + for batch in self.dataloader: + observation = {} + with ObsScope(observation): + # main evaluation computation here. + with paddle.no_grad(): + self.evaluate_sync(self.evaluate_core(batch)) + summary.add(observation) + summary = summary.compute_mean() + + # switch to train mode + for model in self.models.values(): + model.train() + return summary + + def __call__(self, trainer=None): + # evaluate and report the averaged metric to current observation + # if it is used to extend a trainer, the metrics is reported to + # to observation of the trainer + # or otherwise, you can use your own observation + with Timer("Eval Time Cost: {}"): + summary = self.evaluate() + for k, v in summary.items(): + report(k, v) diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/extension.py b/ernie-sat/paddlespeech/s2t/training/extensions/extension.py new file mode 100644 index 0000000..7493213 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/extension.py @@ -0,0 +1,53 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +PRIORITY_WRITER = 300 +PRIORITY_EDITOR = 200 +PRIORITY_READER = 100 + + +class Extension(): + """Extension to customize the behavior of Trainer.""" + trigger = (1, 'iteration') + priority = PRIORITY_READER + name = None + + @property + def default_name(self): + """Default name of the extension, class name by default.""" + return type(self).__name__ + + def __call__(self, trainer): + """Main action of the extention. After each update, it is executed + when the trigger fires.""" + raise NotImplementedError( + 'Extension implementation must override __call__.') + + def initialize(self, trainer): + """Action that is executed once to get the corect trainer state. + It is called before training normally, but if the trainer restores + states with an Snapshot extension, this method should also be called. + """ + pass + + def on_error(self, trainer, exc, tb): + """Handles the error raised during training before finalization. + """ + pass + + def finalize(self, trainer): + """Action that is executed when training is done. + For example, visualizers would need to be closed. + """ + pass diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/plot.py b/ernie-sat/paddlespeech/s2t/training/extensions/plot.py new file mode 100644 index 0000000..7782b95 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/plot.py @@ -0,0 +1,419 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import copy +import os + +import numpy as np + +from . import extension + + +class PlotAttentionReport(extension.Extension): + """Plot attention reporter. + + Args: + att_vis_fn (espnet.nets.*_backend.e2e_asr.E2E.calculate_all_attentions): + Function of attention visualization. + data (list[tuple(str, dict[str, list[Any]])]): List json utt key items. + outdir (str): Directory to save figures. + converter (espnet.asr.*_backend.asr.CustomConverter): + Function to convert data. + device (int | torch.device): Device. + reverse (bool): If True, input and output length are reversed. + ikey (str): Key to access input + (for ASR/ST ikey="input", for MT ikey="output".) + iaxis (int): Dimension to access input + (for ASR/ST iaxis=0, for MT iaxis=1.) + okey (str): Key to access output + (for ASR/ST okey="input", MT okay="output".) + oaxis (int): Dimension to access output + (for ASR/ST oaxis=0, for MT oaxis=0.) + subsampling_factor (int): subsampling factor in encoder + + """ + + def __init__( + self, + att_vis_fn, + data, + outdir, + converter, + transform, + device, + reverse=False, + ikey="input", + iaxis=0, + okey="output", + oaxis=0, + subsampling_factor=1, ): + self.att_vis_fn = att_vis_fn + self.data = copy.deepcopy(data) + self.data_dict = {k: v for k, v in copy.deepcopy(data)} + # key is utterance ID + self.outdir = outdir + self.converter = converter + self.transform = transform + self.device = device + self.reverse = reverse + self.ikey = ikey + self.iaxis = iaxis + self.okey = okey + self.oaxis = oaxis + self.factor = subsampling_factor + if not os.path.exists(self.outdir): + os.makedirs(self.outdir) + + def __call__(self, trainer): + """Plot and save image file of att_ws matrix.""" + att_ws, uttid_list = self.get_attention_weights() + if isinstance(att_ws, list): # multi-encoder case + num_encs = len(att_ws) - 1 + # atts + for i in range(num_encs): + for idx, att_w in enumerate(att_ws[i]): + filename = "%s/%s.ep.{.updater.epoch}.att%d.png" % ( + self.outdir, uttid_list[idx], i + 1, ) + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + np_filename = "%s/%s.ep.{.updater.epoch}.att%d.npy" % ( + self.outdir, uttid_list[idx], i + 1, ) + np.save(np_filename.format(trainer), att_w) + self._plot_and_save_attention(att_w, + filename.format(trainer)) + # han + for idx, att_w in enumerate(att_ws[num_encs]): + filename = "%s/%s.ep.{.updater.epoch}.han.png" % ( + self.outdir, uttid_list[idx], ) + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + np_filename = "%s/%s.ep.{.updater.epoch}.han.npy" % ( + self.outdir, uttid_list[idx], ) + np.save(np_filename.format(trainer), att_w) + self._plot_and_save_attention( + att_w, filename.format(trainer), han_mode=True) + else: + for idx, att_w in enumerate(att_ws): + filename = "%s/%s.ep.{.updater.epoch}.png" % (self.outdir, + uttid_list[idx], ) + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + np_filename = "%s/%s.ep.{.updater.epoch}.npy" % ( + self.outdir, uttid_list[idx], ) + np.save(np_filename.format(trainer), att_w) + self._plot_and_save_attention(att_w, filename.format(trainer)) + + def log_attentions(self, logger, step): + """Add image files of att_ws matrix to the tensorboard.""" + att_ws, uttid_list = self.get_attention_weights() + if isinstance(att_ws, list): # multi-encoder case + num_encs = len(att_ws) - 1 + # atts + for i in range(num_encs): + for idx, att_w in enumerate(att_ws[i]): + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + plot = self.draw_attention_plot(att_w) + logger.add_figure( + "%s_att%d" % (uttid_list[idx], i + 1), + plot.gcf(), + step, ) + # han + for idx, att_w in enumerate(att_ws[num_encs]): + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + plot = self.draw_han_plot(att_w) + logger.add_figure( + "%s_han" % (uttid_list[idx]), + plot.gcf(), + step, ) + else: + for idx, att_w in enumerate(att_ws): + att_w = self.trim_attention_weight(uttid_list[idx], att_w) + plot = self.draw_attention_plot(att_w) + logger.add_figure("%s" % (uttid_list[idx]), plot.gcf(), step) + + def get_attention_weights(self): + """Return attention weights. + + Returns: + numpy.ndarray: attention weights. float. Its shape would be + differ from backend. + * pytorch-> 1) multi-head case => (B, H, Lmax, Tmax), 2) + other case => (B, Lmax, Tmax). + * chainer-> (B, Lmax, Tmax) + + """ + return_batch, uttid_list = self.transform(self.data, return_uttid=True) + batch = self.converter([return_batch], self.device) + if isinstance(batch, tuple): + att_ws = self.att_vis_fn(*batch) + else: + att_ws = self.att_vis_fn(**batch) + return att_ws, uttid_list + + def trim_attention_weight(self, uttid, att_w): + """Transform attention matrix with regard to self.reverse.""" + if self.reverse: + enc_key, enc_axis = self.okey, self.oaxis + dec_key, dec_axis = self.ikey, self.iaxis + else: + enc_key, enc_axis = self.ikey, self.iaxis + dec_key, dec_axis = self.okey, self.oaxis + dec_len = int(self.data_dict[uttid][dec_key][dec_axis]["shape"][0]) + enc_len = int(self.data_dict[uttid][enc_key][enc_axis]["shape"][0]) + if self.factor > 1: + enc_len //= self.factor + if len(att_w.shape) == 3: + att_w = att_w[:, :dec_len, :enc_len] + else: + att_w = att_w[:dec_len, :enc_len] + return att_w + + def draw_attention_plot(self, att_w): + """Plot the att_w matrix. + + Returns: + matplotlib.pyplot: pyplot object with attention matrix image. + + """ + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.clf() + att_w = att_w.astype(np.float32) + if len(att_w.shape) == 3: + for h, aw in enumerate(att_w, 1): + plt.subplot(1, len(att_w), h) + plt.imshow(aw, aspect="auto") + plt.xlabel("Encoder Index") + plt.ylabel("Decoder Index") + else: + plt.imshow(att_w, aspect="auto") + plt.xlabel("Encoder Index") + plt.ylabel("Decoder Index") + plt.tight_layout() + return plt + + def draw_han_plot(self, att_w): + """Plot the att_w matrix for hierarchical attention. + + Returns: + matplotlib.pyplot: pyplot object with attention matrix image. + + """ + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + plt.clf() + if len(att_w.shape) == 3: + for h, aw in enumerate(att_w, 1): + legends = [] + plt.subplot(1, len(att_w), h) + for i in range(aw.shape[1]): + plt.plot(aw[:, i]) + legends.append("Att{}".format(i)) + plt.ylim([0, 1.0]) + plt.xlim([0, aw.shape[0]]) + plt.grid(True) + plt.ylabel("Attention Weight") + plt.xlabel("Decoder Index") + plt.legend(legends) + else: + legends = [] + for i in range(att_w.shape[1]): + plt.plot(att_w[:, i]) + legends.append("Att{}".format(i)) + plt.ylim([0, 1.0]) + plt.xlim([0, att_w.shape[0]]) + plt.grid(True) + plt.ylabel("Attention Weight") + plt.xlabel("Decoder Index") + plt.legend(legends) + plt.tight_layout() + return plt + + def _plot_and_save_attention(self, att_w, filename, han_mode=False): + if han_mode: + plt = self.draw_han_plot(att_w) + else: + plt = self.draw_attention_plot(att_w) + plt.savefig(filename) + plt.close() + + +class PlotCTCReport(extension.Extension): + """Plot CTC reporter. + + Args: + ctc_vis_fn (espnet.nets.*_backend.e2e_asr.E2E.calculate_all_ctc_probs): + Function of CTC visualization. + data (list[tuple(str, dict[str, list[Any]])]): List json utt key items. + outdir (str): Directory to save figures. + converter (espnet.asr.*_backend.asr.CustomConverter): + Function to convert data. + device (int | torch.device): Device. + reverse (bool): If True, input and output length are reversed. + ikey (str): Key to access input + (for ASR/ST ikey="input", for MT ikey="output".) + iaxis (int): Dimension to access input + (for ASR/ST iaxis=0, for MT iaxis=1.) + okey (str): Key to access output + (for ASR/ST okey="input", MT okay="output".) + oaxis (int): Dimension to access output + (for ASR/ST oaxis=0, for MT oaxis=0.) + subsampling_factor (int): subsampling factor in encoder + + """ + + def __init__( + self, + ctc_vis_fn, + data, + outdir, + converter, + transform, + device, + reverse=False, + ikey="input", + iaxis=0, + okey="output", + oaxis=0, + subsampling_factor=1, ): + self.ctc_vis_fn = ctc_vis_fn + self.data = copy.deepcopy(data) + self.data_dict = {k: v for k, v in copy.deepcopy(data)} + # key is utterance ID + self.outdir = outdir + self.converter = converter + self.transform = transform + self.device = device + self.reverse = reverse + self.ikey = ikey + self.iaxis = iaxis + self.okey = okey + self.oaxis = oaxis + self.factor = subsampling_factor + if not os.path.exists(self.outdir): + os.makedirs(self.outdir) + + def __call__(self, trainer): + """Plot and save image file of ctc prob.""" + ctc_probs, uttid_list = self.get_ctc_probs() + if isinstance(ctc_probs, list): # multi-encoder case + num_encs = len(ctc_probs) - 1 + for i in range(num_encs): + for idx, ctc_prob in enumerate(ctc_probs[i]): + filename = "%s/%s.ep.{.updater.epoch}.ctc%d.png" % ( + self.outdir, uttid_list[idx], i + 1, ) + ctc_prob = self.trim_ctc_prob(uttid_list[idx], ctc_prob) + np_filename = "%s/%s.ep.{.updater.epoch}.ctc%d.npy" % ( + self.outdir, uttid_list[idx], i + 1, ) + np.save(np_filename.format(trainer), ctc_prob) + self._plot_and_save_ctc(ctc_prob, filename.format(trainer)) + else: + for idx, ctc_prob in enumerate(ctc_probs): + filename = "%s/%s.ep.{.updater.epoch}.png" % (self.outdir, + uttid_list[idx], ) + ctc_prob = self.trim_ctc_prob(uttid_list[idx], ctc_prob) + np_filename = "%s/%s.ep.{.updater.epoch}.npy" % ( + self.outdir, uttid_list[idx], ) + np.save(np_filename.format(trainer), ctc_prob) + self._plot_and_save_ctc(ctc_prob, filename.format(trainer)) + + def log_ctc_probs(self, logger, step): + """Add image files of ctc probs to the tensorboard.""" + ctc_probs, uttid_list = self.get_ctc_probs() + if isinstance(ctc_probs, list): # multi-encoder case + num_encs = len(ctc_probs) - 1 + for i in range(num_encs): + for idx, ctc_prob in enumerate(ctc_probs[i]): + ctc_prob = self.trim_ctc_prob(uttid_list[idx], ctc_prob) + plot = self.draw_ctc_plot(ctc_prob) + logger.add_figure( + "%s_ctc%d" % (uttid_list[idx], i + 1), + plot.gcf(), + step, ) + else: + for idx, ctc_prob in enumerate(ctc_probs): + ctc_prob = self.trim_ctc_prob(uttid_list[idx], ctc_prob) + plot = self.draw_ctc_plot(ctc_prob) + logger.add_figure("%s" % (uttid_list[idx]), plot.gcf(), step) + + def get_ctc_probs(self): + """Return CTC probs. + + Returns: + numpy.ndarray: CTC probs. float. Its shape would be + differ from backend. (B, Tmax, vocab). + + """ + return_batch, uttid_list = self.transform(self.data, return_uttid=True) + batch = self.converter([return_batch], self.device) + if isinstance(batch, tuple): + probs = self.ctc_vis_fn(*batch) + else: + probs = self.ctc_vis_fn(**batch) + return probs, uttid_list + + def trim_ctc_prob(self, uttid, prob): + """Trim CTC posteriors accoding to input lengths.""" + enc_len = int(self.data_dict[uttid][self.ikey][self.iaxis]["shape"][0]) + if self.factor > 1: + enc_len //= self.factor + prob = prob[:enc_len] + return prob + + def draw_ctc_plot(self, ctc_prob): + """Plot the ctc_prob matrix. + + Returns: + matplotlib.pyplot: pyplot object with CTC prob matrix image. + + """ + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + ctc_prob = ctc_prob.astype(np.float32) + + plt.clf() + topk_ids = np.argsort(ctc_prob, axis=1) + n_frames, vocab = ctc_prob.shape + times_probs = np.arange(n_frames) + + plt.figure(figsize=(20, 8)) + + # NOTE: index 0 is reserved for blank + for idx in set(topk_ids.reshape(-1).tolist()): + if idx == 0: + plt.plot( + times_probs, + ctc_prob[:, 0], + ":", + label="", + color="grey") + else: + plt.plot(times_probs, ctc_prob[:, idx]) + plt.xlabel(u"Input [frame]", fontsize=12) + plt.ylabel("Posteriors", fontsize=12) + plt.xticks(list(range(0, int(n_frames) + 1, 10))) + plt.yticks(list(range(0, 2, 1))) + plt.tight_layout() + return plt + + def _plot_and_save_ctc(self, ctc_prob, filename): + plt = self.draw_ctc_plot(ctc_prob) + plt.savefig(filename) + plt.close() diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/snapshot.py b/ernie-sat/paddlespeech/s2t/training/extensions/snapshot.py new file mode 100644 index 0000000..426bf72 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/snapshot.py @@ -0,0 +1,134 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import os +from datetime import datetime +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines + +from . import extension +from ..reporter import get_observations +from ..updaters.trainer import Trainer +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.mp_tools import rank_zero_only + +logger = Log(__name__).getlog() + + +def load_records(records_fp): + """Load record files (json lines.)""" + with jsonlines.open(records_fp, 'r') as reader: + records = list(reader) + return records + + +class Snapshot(extension.Extension): + """An extension to make snapshot of the updater object inside + the trainer. It is done by calling the updater's `save` method. + An Updater save its state_dict by default, which contains the + updater state, (i.e. epoch and iteration) and all the model + parameters and optimizer states. If the updater inside the trainer + subclasses StandardUpdater, everything is good to go. + Parameters + ---------- + checkpoint_dir : Union[str, Path] + The directory to save checkpoints into. + """ + + trigger = (1, 'epoch') + priority = -100 + default_name = "snapshot" + + def __init__(self, + mode='latest', + max_size: int=5, + indicator=None, + less_better=True, + snapshot_on_error: bool=False): + self.records: List[Dict[str, Any]] = [] + assert mode in ('latest', 'kbest'), mode + if mode == 'kbest': + assert indicator is not None + self.mode = mode + self.indicator = indicator + self.less_is_better = less_better + self.max_size = max_size + self._snapshot_on_error = snapshot_on_error + self._save_all = (max_size == -1) + self.checkpoint_dir = None + + def initialize(self, trainer: Trainer): + """Setting up this extention.""" + self.checkpoint_dir = trainer.out / "checkpoints" + + # load existing records + record_path: Path = self.checkpoint_dir / "records.jsonl" + if record_path.exists(): + self.records = load_records(record_path) + ckpt_path = self.records[-1]['path'] + logger.info(f"Loading from an existing checkpoint {ckpt_path}") + trainer.updater.load(ckpt_path) + + def on_error(self, trainer, exc, tb): + if self._snapshot_on_error: + self.save_checkpoint_and_update(trainer, 'latest') + + def __call__(self, trainer: Trainer): + self.save_checkpoint_and_update(trainer, self.mode) + + def full(self): + """Whether the number of snapshots it keeps track of is greater + than the max_size.""" + return (not self._save_all) and len(self.records) > self.max_size + + @rank_zero_only + def save_checkpoint_and_update(self, trainer: Trainer, mode: str): + """Saving new snapshot and remove the oldest snapshot if needed.""" + iteration = trainer.updater.state.iteration + epoch = trainer.updater.state.epoch + num = epoch if self.trigger[1] == 'epoch' else iteration + path = self.checkpoint_dir / f"{num}.np" + + # add the new one + trainer.updater.save(path) + record = { + "time": str(datetime.now()), + 'path': str(path.resolve()), # use absolute path + 'iteration': iteration, + 'epoch': epoch, + 'indicator': get_observations()[self.indicator] + } + self.records.append(record) + + # remove the earist + if self.full(): + if mode == 'kbest': + self.records = sorted( + self.records, + key=lambda record: record['indicator'], + reverse=not self.less_is_better) + eariest_record = self.records[0] + os.remove(eariest_record["path"]) + self.records.pop(0) + + # update the record file + record_path = self.checkpoint_dir / "records.jsonl" + with jsonlines.open(record_path, 'w') as writer: + for record in self.records: + # jsonlines.open may return a Writer or a Reader + writer.write(record) # pylint: disable=no-member diff --git a/ernie-sat/paddlespeech/s2t/training/extensions/visualizer.py b/ernie-sat/paddlespeech/s2t/training/extensions/visualizer.py new file mode 100644 index 0000000..e5f456c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/extensions/visualizer.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from visualdl import LogWriter + +from . import extension +from ..updaters.trainer import Trainer + + +class VisualDL(extension.Extension): + """A wrapper of visualdl log writer. It assumes that the metrics to be visualized + are all scalars which are recorded into the `.observation` dictionary of the + trainer object. The dictionary is created for each step, thus the visualdl log + writer uses the iteration from the updater's `iteration` as the global step to + add records. + """ + trigger = (1, 'iteration') + default_name = 'visualdl' + priority = extension.PRIORITY_READER + + def __init__(self, output_dir): + self.writer = LogWriter(str(output_dir)) + + def __call__(self, trainer: Trainer): + for k, v in trainer.observation.items(): + self.writer.add_scalar(k, v, step=trainer.updater.state.iteration) + + def finalize(self, trainer): + self.writer.close() diff --git a/ernie-sat/paddlespeech/s2t/training/gradclip.py b/ernie-sat/paddlespeech/s2t/training/gradclip.py new file mode 100644 index 0000000..26ac501 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/gradclip.py @@ -0,0 +1,85 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle.fluid import core +from paddle.fluid import layers +from paddle.fluid.dygraph import base as imperative_base + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["ClipGradByGlobalNormWithLog"] + +logger = Log(__name__).getlog() + + +class ClipGradByGlobalNormWithLog(paddle.nn.ClipGradByGlobalNorm): + def __init__(self, clip_norm): + super().__init__(clip_norm) + + def __repr__(self): + return f"{self.__class__.__name__}(global_clip_norm={self.clip_norm})" + + @imperative_base.no_grad + def _dygraph_clip(self, params_grads): + params_and_grads = [] + sum_square_list = [] + for i, (p, g) in enumerate(params_grads): + if g is None: + continue + if getattr(p, 'need_clip', True) is False: + continue + merge_grad = g + if g.type == core.VarDesc.VarType.SELECTED_ROWS: + merge_grad = layers.merge_selected_rows(g) + merge_grad = layers.get_tensor_from_selected_rows(merge_grad) + square = layers.square(merge_grad) + sum_square = layers.reduce_sum(square) + sum_square_list.append(sum_square) + + # debug log, not dump all since slow down train process + if i < 10: + logger.debug( + f"Grad Before Clip: {p.name}: {float(sum_square.sqrt()) }") + + # all parameters have been filterd out + if len(sum_square_list) == 0: + return params_grads + + global_norm_var = layers.concat(sum_square_list) + global_norm_var = layers.reduce_sum(global_norm_var) + global_norm_var = layers.sqrt(global_norm_var) + # debug log + logger.debug(f"Grad Global Norm: {float(global_norm_var)}!!!!") + + max_global_norm = layers.fill_constant( + shape=[1], dtype=global_norm_var.dtype, value=self.clip_norm) + clip_var = layers.elementwise_div( + x=max_global_norm, + y=layers.elementwise_max(x=global_norm_var, y=max_global_norm)) + for i, (p, g) in enumerate(params_grads): + if g is None: + continue + if getattr(p, 'need_clip', True) is False: + params_and_grads.append((p, g)) + continue + new_grad = layers.elementwise_mul(x=g, y=clip_var) + params_and_grads.append((p, new_grad)) + + # debug log, not dump all since slow down train process + if i < 10: + logger.debug( + f"Grad After Clip: {p.name}: {float(new_grad.square().sum().sqrt())}" + ) + + return params_and_grads diff --git a/ernie-sat/paddlespeech/s2t/training/optimizer.py b/ernie-sat/paddlespeech/s2t/training/optimizer.py new file mode 100644 index 0000000..f7f70c5 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/optimizer.py @@ -0,0 +1,122 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from typing import Any +from typing import Dict +from typing import Text + +import paddle +from paddle.optimizer import Optimizer +from paddle.regularizer import L2Decay + +from paddlespeech.s2t.training.gradclip import ClipGradByGlobalNormWithLog +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.dynamic_import import instance_class +from paddlespeech.s2t.utils.log import Log + +__all__ = ["OptimizerFactory"] + +logger = Log(__name__).getlog() + +OPTIMIZER_DICT = { + "sgd": "paddle.optimizer:SGD", + "momentum": "paddle.optimizer:Momentum", + "adadelta": "paddle.optimizer:Adadelta", + "adam": "paddle.optimizer:Adam", + "adamw": "paddle.optimizer:AdamW", +} + + +def register_optimizer(cls): + """Register optimizer.""" + alias = cls.__name__.lower() + OPTIMIZER_DICT[cls.__name__.lower()] = cls.__module__ + ":" + cls.__name__ + return cls + + +@register_optimizer +class Noam(paddle.optimizer.Adam): + """Seem to: espnet/nets/pytorch_backend/transformer/optimizer.py """ + + def __init__(self, + learning_rate=0, + beta1=0.9, + beta2=0.98, + epsilon=1e-9, + parameters=None, + weight_decay=None, + grad_clip=None, + lazy_mode=False, + multi_precision=False, + name=None): + super().__init__( + learning_rate=learning_rate, + beta1=beta1, + beta2=beta2, + epsilon=epsilon, + parameters=parameters, + weight_decay=weight_decay, + grad_clip=grad_clip, + lazy_mode=lazy_mode, + multi_precision=multi_precision, + name=name) + + def __repr__(self): + echo = f"<{self.__class__.__module__}.{self.__class__.__name__} object at {hex(id(self))}> " + echo += f"learning_rate: {self._learning_rate}, " + echo += f"(beta1: {self._beta1} beta2: {self._beta2}), " + echo += f"epsilon: {self._epsilon}" + + +def dynamic_import_optimizer(module): + """Import Optimizer class dynamically. + + Args: + module (str): module_name:class_name or alias in `OPTIMIZER_DICT` + + Returns: + type: Optimizer class + + """ + module_class = dynamic_import(module, OPTIMIZER_DICT) + assert issubclass(module_class, + Optimizer), f"{module} does not implement Optimizer" + return module_class + + +class OptimizerFactory(): + @classmethod + def from_args(cls, name: str, args: Dict[Text, Any]): + assert "parameters" in args, "parameters not in args." + assert "learning_rate" in args, "learning_rate not in args." + + grad_clip = ClipGradByGlobalNormWithLog( + args['grad_clip']) if "grad_clip" in args else None + weight_decay = L2Decay( + args['weight_decay']) if "weight_decay" in args else None + if weight_decay: + logger.info(f'') + if grad_clip: + logger.info(f'') + + module_class = dynamic_import_optimizer(name.lower()) + args.update({"grad_clip": grad_clip, "weight_decay": weight_decay}) + opt = instance_class(module_class, args) + if "__repr__" in vars(opt): + logger.info(f"{opt}") + else: + logger.info( + f" LR: {args['learning_rate']}" + ) + return opt diff --git a/ernie-sat/paddlespeech/s2t/training/reporter.py b/ernie-sat/paddlespeech/s2t/training/reporter.py new file mode 100644 index 0000000..4d8eb2a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/reporter.py @@ -0,0 +1,145 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import contextlib +import math +from collections import defaultdict + +OBSERVATIONS = None + + +@contextlib.contextmanager +def ObsScope(observations): + # make `observation` the target to report to. + # it is basically a dictionary that stores temporary observations + global OBSERVATIONS + old = OBSERVATIONS + OBSERVATIONS = observations + + try: + yield + finally: + OBSERVATIONS = old + + +def get_observations(): + global OBSERVATIONS + return OBSERVATIONS + + +def report(name, value): + # a simple function to report named value + # you can use it everywhere, it will get the default target and writ to it + # you can think of it as std.out + observations = get_observations() + if observations is None: + return + else: + observations[name] = value + + +class Summary(): + """Online summarization of a sequence of scalars. + Summary computes the statistics of given scalars online. + """ + + def __init__(self): + self._x = 0.0 + self._x2 = 0.0 + self._n = 0 + + def add(self, value, weight=1): + """Adds a scalar value. + Args: + value: Scalar value to accumulate. It is either a NumPy scalar or + a zero-dimensional array (on CPU or GPU). + weight: An optional weight for the value. It is a NumPy scalar or + a zero-dimensional array (on CPU or GPU). + Default is 1 (integer). + """ + self._x += weight * value + self._x2 += weight * value * value + self._n += weight + + def compute_mean(self): + """Computes the mean.""" + x, n = self._x, self._n + return x / n + + def make_statistics(self): + """Computes and returns the mean and standard deviation values. + Returns: + tuple: Mean and standard deviation values. + """ + x, n = self._x, self._n + mean = x / n + var = self._x2 / n - mean * mean + std = math.sqrt(var) + return mean, std + + +class DictSummary(): + """Online summarization of a sequence of dictionaries. + ``DictSummary`` computes the statistics of a given set of scalars online. + It only computes the statistics for scalar values and variables of scalar + values in the dictionaries. + """ + + def __init__(self): + self._summaries = defaultdict(Summary) + + def add(self, d): + """Adds a dictionary of scalars. + Args: + d (dict): Dictionary of scalars to accumulate. Only elements of + scalars, zero-dimensional arrays, and variables of + zero-dimensional arrays are accumulated. When the value + is a tuple, the second element is interpreted as a weight. + """ + summaries = self._summaries + for k, v in d.items(): + w = 1 + if isinstance(v, tuple): + v = v[0] + w = v[1] + summaries[k].add(v, weight=w) + + def compute_mean(self): + """Creates a dictionary of mean values. + It returns a single dictionary that holds a mean value for each entry + added to the summary. + Returns: + dict: Dictionary of mean values. + """ + return { + name: summary.compute_mean() + for name, summary in self._summaries.items() + } + + def make_statistics(self): + """Creates a dictionary of statistics. + It returns a single dictionary that holds mean and standard deviation + values for every entry added to the summary. For an entry of name + ``'key'``, these values are added to the dictionary by names ``'key'`` + and ``'key.std'``, respectively. + Returns: + dict: Dictionary of statistics of all entries. + """ + stats = {} + for name, summary in self._summaries.items(): + mean, std = summary.make_statistics() + stats[name] = mean + stats[name + '.std'] = std + + return stats diff --git a/ernie-sat/paddlespeech/s2t/training/scheduler.py b/ernie-sat/paddlespeech/s2t/training/scheduler.py new file mode 100644 index 0000000..b22f7ef --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/scheduler.py @@ -0,0 +1,130 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from typing import Any +from typing import Dict +from typing import Text +from typing import Union + +from paddle.optimizer.lr import LRScheduler +from typeguard import check_argument_types + +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.s2t.utils.dynamic_import import instance_class +from paddlespeech.s2t.utils.log import Log + +__all__ = ["WarmupLR", "LRSchedulerFactory"] + +logger = Log(__name__).getlog() + +SCHEDULER_DICT = { + "noam": "paddle.optimizer.lr:NoamDecay", + "expdecaylr": "paddle.optimizer.lr:ExponentialDecay", + "piecewisedecay": "paddle.optimizer.lr:PiecewiseDecay", +} + + +def register_scheduler(cls): + """Register scheduler.""" + alias = cls.__name__.lower() + SCHEDULER_DICT[cls.__name__.lower()] = cls.__module__ + ":" + cls.__name__ + return cls + + +@register_scheduler +class WarmupLR(LRScheduler): + """The WarmupLR scheduler + This scheduler is almost same as NoamLR Scheduler except for following + difference: + NoamLR: + lr = optimizer.lr * model_size ** -0.5 + * min(step ** -0.5, step * warmup_step ** -1.5) + WarmupLR: + lr = optimizer.lr * warmup_step ** 0.5 + * min(step ** -0.5, step * warmup_step ** -1.5) + Note that the maximum lr equals to optimizer.lr in this scheduler. + """ + + def __init__(self, + warmup_steps: Union[int, float]=25000, + learning_rate=1.0, + last_epoch=-1, + verbose=False, + **kwargs): + assert check_argument_types() + self.warmup_steps = warmup_steps + super().__init__(learning_rate, last_epoch, verbose) + + def __repr__(self): + return f"{self.__class__.__name__}(warmup_steps={self.warmup_steps}, lr={self.base_lr}, last_epoch={self.last_epoch})" + + def get_lr(self): + # self.last_epoch start from zero + step_num = self.last_epoch + 1 + return self.base_lr * self.warmup_steps**0.5 * min( + step_num**-0.5, step_num * self.warmup_steps**-1.5) + + def set_step(self, step: int=None): + ''' + It will update the learning rate in optimizer according to current ``epoch`` . + The new learning rate will take effect on next ``optimizer.step`` . + + Args: + step (int, None): specify current epoch. Default: None. Auto-increment from last_epoch=-1. + Returns: + None + ''' + self.step(epoch=step) + + +@register_scheduler +class ConstantLR(LRScheduler): + """ + Args: + learning_rate (float): The initial learning rate. It is a python float number. + last_epoch (int, optional): The index of last epoch. Can be set to restart training. Default: -1, means initial learning rate. + verbose (bool, optional): If ``True``, prints a message to stdout for each update. Default: ``False`` . + + Returns: + ``ConstantLR`` instance to schedule learning rate. + """ + + def __init__(self, learning_rate, last_epoch=-1, verbose=False): + super().__init__(learning_rate, last_epoch, verbose) + + def get_lr(self): + return self.base_lr + + +def dynamic_import_scheduler(module): + """Import Scheduler class dynamically. + + Args: + module (str): module_name:class_name or alias in `SCHEDULER_DICT` + + Returns: + type: Scheduler class + + """ + module_class = dynamic_import(module, SCHEDULER_DICT) + assert issubclass(module_class, + LRScheduler), f"{module} does not implement LRScheduler" + return module_class + + +class LRSchedulerFactory(): + @classmethod + def from_args(cls, name: str, args: Dict[Text, Any]): + module_class = dynamic_import_scheduler(name.lower()) + return instance_class(module_class, args) diff --git a/ernie-sat/paddlespeech/s2t/training/timer.py b/ernie-sat/paddlespeech/s2t/training/timer.py new file mode 100644 index 0000000..271ffff --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/timer.py @@ -0,0 +1,50 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import datetime +import time + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["Timer"] + +logger = Log(__name__).getlog() + + +class Timer(): + """To be used like this: + with Timer("Message") as value: + do some thing + """ + + def __init__(self, message=None): + self.message = message + + def duration(self) -> str: + elapsed_time = time.time() - self.start + time_str = str(datetime.timedelta(seconds=elapsed_time)) + return time_str + + def __enter__(self): + self.start = time.time() + return self + + def __exit__(self, type, value, traceback): + if self.message: + logger.info(self.message.format(self.duration())) + + def __call__(self) -> float: + return time.time() - self.start + + def __str__(self): + return self.duration() diff --git a/ernie-sat/paddlespeech/s2t/training/trainer.py b/ernie-sat/paddlespeech/s2t/training/trainer.py new file mode 100644 index 0000000..de90c9e --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/trainer.py @@ -0,0 +1,492 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import time +from collections import OrderedDict +from contextlib import contextmanager +from pathlib import Path + +import paddle +from paddle import distributed as dist +from visualdl import LogWriter + +from paddlespeech.s2t.training.reporter import ObsScope +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.timer import Timer +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils import profiler +from paddlespeech.s2t.utils.checkpoint import Checkpoint +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.utility import all_version +from paddlespeech.s2t.utils.utility import seed_all +from paddlespeech.s2t.utils.utility import UpdateConfig + +__all__ = ["Trainer"] + +logger = Log(__name__).getlog() + + +class Trainer(): + """ + An experiment template in order to structure the training code and take + care of saving, loading, logging, visualization stuffs. It's intended to + be flexible and simple. + + So it only handles output directory (create directory for the output, + create a checkpoint directory, dump the config in use and create + visualizer and logger) in a standard way without enforcing any + input-output protocols to the model and dataloader. It leaves the main + part for the user to implement their own (setup the model, criterion, + optimizer, define a training step, define a validation function and + customize all the text and visual logs). + It does not save too much boilerplate code. The users still have to write + the forward/backward/update mannually, but they are free to add + non-standard behaviors if needed. + We have some conventions to follow. + 1. Experiment should have ``model``, ``optimizer``, ``train_loader`` and + ``valid_loader``, ``config`` and ``args`` attributes. + 2. The config should have a ``training`` field, which has + ``valid_interval``, ``save_interval`` and ``max_iteration`` keys. It is + used as the trigger to invoke validation, checkpointing and stop of the + experiment. + 3. There are four methods, namely ``train_batch``, ``valid``, + ``setup_model`` and ``setup_dataloader`` that should be implemented. + Feel free to add/overwrite other methods and standalone functions if you + need. + + Parameters + ---------- + config: yacs.config.CfgNode + The configuration used for the experiment. + + args: argparse.Namespace + The parsed command line arguments. + Examples + -------- + >>> def main_sp(config, args): + >>> exp = Trainer(config, args) + >>> exp.setup() + >>> exp.run() + >>> + >>> config = get_cfg_defaults() + >>> parser = default_argument_parser() + >>> args = parser.parse_args() + >>> if args.config: + >>> config.merge_from_file(args.config) + >>> if args.opts: + >>> config.merge_from_list(args.opts) + >>> config.freeze() + >>> + >>> if args.ngpu > 1: + >>> dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + >>> else: + >>> main_sp(config, args) + """ + + def __init__(self, config, args): + self.config = config + self.args = args + self.optimizer = None + self.visualizer = None + self.output_dir = None + self.checkpoint_dir = None + self.iteration = 0 + self.epoch = 0 + self.rank = dist.get_rank() + self.world_size = dist.get_world_size() + self._train = True + + # print deps version + all_version() + logger.info(f"Rank: {self.rank}/{self.world_size}") + + # set device + paddle.set_device('gpu' if self.args.ngpu > 0 else 'cpu') + if self.parallel: + self.init_parallel() + + self.checkpoint = Checkpoint( + kbest_n=self.config.checkpoint.kbest_n, + latest_n=self.config.checkpoint.latest_n) + + # set random seed if needed + if args.seed: + seed_all(args.seed) + logger.info(f"Set seed {args.seed}") + + # profiler and benchmark options + if hasattr(self.args, + "benchmark_batch_size") and self.args.benchmark_batch_size: + with UpdateConfig(self.config): + self.config.batch_size = self.args.benchmark_batch_size + self.config.log_interval = 1 + logger.info( + f"Benchmark reset batch-size: {self.args.benchmark_batch_size}") + + @property + def train(self): + return self._train + + @contextmanager + def eval(self): + self._train = False + yield + self._train = True + + def setup(self): + """Setup the experiment. + """ + self.setup_output_dir() + self.dump_config() + self.setup_visualizer() + + self.setup_dataloader() + self.setup_model() + + self.iteration = 0 + self.epoch = 0 + + @property + def parallel(self): + """A flag indicating whether the experiment should run with + multiprocessing. + """ + return self.args.ngpu > 1 + + def init_parallel(self): + """Init environment for multiprocess training. + """ + dist.init_parallel_env() + + @mp_tools.rank_zero_only + def save(self, tag=None, infos: dict=None): + """Save checkpoint (model parameters and optimizer states). + + Args: + tag (int or str, optional): None for step, else using tag, e.g epoch. Defaults to None. + infos (dict, optional): meta data to save. Defaults to None. + """ + + infos = infos if infos else dict() + infos.update({ + "step": self.iteration, + "epoch": self.epoch, + "lr": self.optimizer.get_lr() + }) + self.checkpoint.save_parameters(self.checkpoint_dir, self.iteration + if tag is None else tag, self.model, + self.optimizer, infos) + + def resume_or_scratch(self): + """Resume from latest checkpoint at checkpoints in the output + directory or load a specified checkpoint. + + If ``args.checkpoint_path`` is not None, load the checkpoint, else + resume training. + """ + scratch = None + infos = self.checkpoint.load_latest_parameters( + self.model, + self.optimizer, + checkpoint_dir=self.checkpoint_dir, + checkpoint_path=self.args.checkpoint_path) + if infos: + # just restore ckpt + # lr will resotre from optimizer ckpt + self.iteration = infos["step"] + self.epoch = infos["epoch"] + scratch = False + logger.info( + f"Restore ckpt: epoch {self.epoch }, step {self.iteration}!") + else: + self.iteration = 0 + self.epoch = 0 + scratch = True + logger.info("Init from scratch!") + return scratch + + def maybe_batch_sampler_step(self): + """ batch_sampler seed by epoch """ + if hasattr(self.train_loader, "batch_sampler"): + batch_sampler = self.train_loader.batch_sampler + if isinstance(batch_sampler, paddle.io.DistributedBatchSampler): + logger.debug( + f"train_loader.batch_sample.set_epoch: {self.epoch}") + batch_sampler.set_epoch(self.epoch) + + def before_train(self): + from_scratch = self.resume_or_scratch() + if from_scratch: + # scratch: save init model, i.e. 0 epoch + self.save(tag='init', infos=None) + else: + # resume: train next_epoch and next_iteration + self.epoch += 1 + self.iteration += 1 + logger.info( + f"Resume train: epoch {self.epoch }, step {self.iteration}!") + + self.maybe_batch_sampler_step() + + def new_epoch(self): + """Reset the train loader seed and increment `epoch`. + """ + # `iteration` increased by train step + self.epoch += 1 + self.maybe_batch_sampler_step() + + def after_train_batch(self): + if self.args.benchmark_max_step: + profiler.add_profiler_step(self.args.profiler_options) + if self.args.benchmark_max_step and self.iteration > self.args.benchmark_max_step: + logger.info( + f"Reach benchmark-max-step: {self.args.benchmark_max_step}") + sys.exit(0) + + def do_train(self): + """The training process control by epoch.""" + self.before_train() + + logger.info(f"Train Total Examples: {len(self.train_loader.dataset)}") + while self.epoch < self.config.n_epoch: + with Timer("Epoch-Train Time Cost: {}"): + self.model.train() + try: + data_start_time = time.time() + for batch_index, batch in enumerate(self.train_loader): + dataload_time = time.time() - data_start_time + msg = "Train:" + observation = OrderedDict() + with ObsScope(observation): + report("Rank", dist.get_rank()) + report("epoch", self.epoch) + report('step', self.iteration) + report("lr", self.lr_scheduler()) + self.train_batch(batch_index, batch, msg) + self.after_train_batch() + report('iter', batch_index + 1) + report('total', len(self.train_loader)) + report('reader_cost', dataload_time) + observation['batch_cost'] = observation[ + 'reader_cost'] + observation['step_cost'] + observation['samples'] = observation['batch_size'] + observation['ips samples/s'] = observation[ + 'batch_size'] / observation['batch_cost'] + for k, v in observation.items(): + msg += f" {k}: " + msg += f"{v:>.8f}" if isinstance(v, + float) else f"{v}" + msg += "," + msg = msg[:-1] # remove the last "," + logger.info(msg) + data_start_time = time.time() + except Exception as e: + logger.error(e) + raise e + + with Timer("Eval Time Cost: {}"): + total_loss, num_seen_utts = self.valid() + if dist.get_world_size() > 1: + num_seen_utts = paddle.to_tensor(num_seen_utts) + # the default operator in all_reduce function is sum. + dist.all_reduce(num_seen_utts) + total_loss = paddle.to_tensor(total_loss) + dist.all_reduce(total_loss) + cv_loss = total_loss / num_seen_utts + cv_loss = float(cv_loss) + else: + cv_loss = total_loss / num_seen_utts + + logger.info( + 'Epoch {} Val info val_loss {}'.format(self.epoch, cv_loss)) + if self.visualizer: + self.visualizer.add_scalar( + tag='eval/cv_loss', value=cv_loss, step=self.epoch) + self.visualizer.add_scalar( + tag='eval/lr', value=self.lr_scheduler(), step=self.epoch) + + # after epoch + self.save(tag=self.epoch, infos={'val_loss': cv_loss}) + # step lr every epoch + self.lr_scheduler.step() + self.new_epoch() + + def run(self): + """The routine of the experiment after setup. This method is intended + to be used by the user. + """ + try: + with Timer("Training Done: {}"): + self.do_train() + except KeyboardInterrupt: + exit(-1) + finally: + self.destory() + + def restore(self): + """Resume from latest checkpoint at checkpoints in the output + directory or load a specified checkpoint. + + If ``args.checkpoint_path`` is not None, load the checkpoint, else + resume training. + """ + assert self.args.checkpoint_path + infos = self.checkpoint.load_latest_parameters( + self.model, checkpoint_path=self.args.checkpoint_path) + return infos + + def run_test(self): + """Do Test/Decode""" + try: + with Timer("Test/Decode Done: {}"): + with self.eval(): + self.restore() + self.test() + except KeyboardInterrupt: + exit(-1) + + def run_export(self): + """Do Model Export""" + try: + with Timer("Export Done: {}"): + with self.eval(): + self.restore() + self.export() + except KeyboardInterrupt: + exit(-1) + + def run_align(self): + """Do CTC alignment""" + try: + with Timer("Align Done: {}"): + with self.eval(): + self.restore() + self.align() + except KeyboardInterrupt: + sys.exit(-1) + + def setup_output_dir(self): + """Create a directory used for output. + """ + if self.args.output: + output_dir = Path(self.args.output).expanduser() + elif self.args.checkpoint_path: + output_dir = Path( + self.args.checkpoint_path).expanduser().parent.parent + elif self.args.export_path: + output_dir = Path(self.args.export_path).expanduser().parent.parent + self.output_dir = output_dir + self.output_dir.mkdir(parents=True, exist_ok=True) + + self.checkpoint_dir = self.output_dir / "checkpoints" + self.checkpoint_dir.mkdir(parents=True, exist_ok=True) + + self.log_dir = output_dir / "log" + self.log_dir.mkdir(parents=True, exist_ok=True) + + self.test_dir = output_dir / "test" + self.test_dir.mkdir(parents=True, exist_ok=True) + + self.decode_dir = output_dir / "decode" + self.decode_dir.mkdir(parents=True, exist_ok=True) + + self.export_dir = output_dir / "export" + self.export_dir.mkdir(parents=True, exist_ok=True) + + self.visual_dir = output_dir / "visual" + self.visual_dir.mkdir(parents=True, exist_ok=True) + + self.config_dir = output_dir / "conf" + self.config_dir.mkdir(parents=True, exist_ok=True) + + @mp_tools.rank_zero_only + def destory(self): + """Close visualizer to avoid hanging after training""" + # https://github.com/pytorch/fairseq/issues/2357 + if self.visualizer: + self.visualizer.close() + + @mp_tools.rank_zero_only + def setup_visualizer(self): + """Initialize a visualizer to log the experiment. + + The visual log is saved in the output directory. + + Notes + ------ + Only the main process has a visualizer with it. Use multiple + visualizers in multiprocess to write to a same log file may cause + unexpected behaviors. + """ + # visualizer + visualizer = LogWriter(logdir=str(self.visual_dir)) + self.visualizer = visualizer + + @mp_tools.rank_zero_only + def dump_config(self): + """Save the configuration used for this experiment. + + It is saved in to ``config.yaml`` in the output directory at the + beginning of the experiment. + """ + config_file = self.config_dir / "config.yaml" + if self.train and config_file.exists(): + time_stamp = time.strftime("%Y_%m_%d_%H_%M_%s", time.gmtime()) + target_path = self.config_dir / ".".join( + [time_stamp, "config.yaml"]) + config_file.rename(target_path) + + with open(config_file, 'wt') as f: + print(self.config, file=f) + + def train_batch(self): + """The training loop. A subclass should implement this method. + """ + raise NotImplementedError("train_batch should be implemented.") + + @paddle.no_grad() + def valid(self): + """The validation. A subclass should implement this method. + """ + raise NotImplementedError("valid should be implemented.") + + @paddle.no_grad() + def test(self): + """The test. A subclass should implement this method in Tester. + """ + raise NotImplementedError("test should be implemented.") + + @paddle.no_grad() + def export(self): + """The test. A subclass should implement this method in Tester. + """ + raise NotImplementedError("export should be implemented.") + + @paddle.no_grad() + def align(self): + """The align. A subclass should implement this method in Tester. + """ + raise NotImplementedError("align should be implemented.") + + def setup_model(self): + """Setup model, criterion and optimizer, etc. A subclass should + implement this method. + """ + raise NotImplementedError("setup_model should be implemented.") + + def setup_dataloader(self): + """Setup training dataloader and validation dataloader. A subclass + should implement this method. + """ + raise NotImplementedError("setup_dataloader should be implemented.") diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/__init__.py b/ernie-sat/paddlespeech/s2t/training/triggers/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/compare_value_trigger.py b/ernie-sat/paddlespeech/s2t/training/triggers/compare_value_trigger.py new file mode 100644 index 0000000..5c2a272 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/compare_value_trigger.py @@ -0,0 +1,62 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from ..reporter import DictSummary +from .utils import get_trigger + + +class CompareValueTrigger(): + """Trigger invoked when key value getting bigger or lower than before. + + Args: + key (str) : Key of value. + compare_fn ((float, float) -> bool) : Function to compare the values. + trigger (tuple(int, str)) : Trigger that decide the comparison interval. + + """ + + def __init__(self, key, compare_fn, trigger=(1, "epoch")): + self._key = key + self._best_value = None + self._interval_trigger = get_trigger(trigger) + self._init_summary() + self._compare_fn = compare_fn + + def __call__(self, trainer): + """Get value related to the key and compare with current value.""" + observation = trainer.observation + summary = self._summary + key = self._key + if key in observation: + summary.add({key: observation[key]}) + + if not self._interval_trigger(trainer): + return False + + stats = summary.compute_mean() + value = float(stats[key]) # copy to CPU + self._init_summary() + + if self._best_value is None: + # initialize best value + self._best_value = value + return False + elif self._compare_fn(self._best_value, value): + return True + else: + self._best_value = value + return False + + def _init_summary(self): + self._summary = DictSummary() diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/interval_trigger.py b/ernie-sat/paddlespeech/s2t/training/triggers/interval_trigger.py new file mode 100644 index 0000000..14201d2 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/interval_trigger.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class IntervalTrigger(): + """A Predicate to do something every N cycle.""" + + def __init__(self, period: int, unit: str): + if unit not in ("iteration", "epoch"): + raise ValueError("unit should be 'iteration' or 'epoch'") + if period <= 0: + raise ValueError("period should be a positive integer.") + self.period = period + self.unit = unit + self.last_index = None + + def __call__(self, trainer): + if self.last_index is None: + last_index = getattr(trainer.updater.state, self.unit) + self.last_index = last_index + + last_index = self.last_index + index = getattr(trainer.updater.state, self.unit) + fire = index // self.period != last_index // self.period + + self.last_index = index + return fire diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/limit_trigger.py b/ernie-sat/paddlespeech/s2t/training/triggers/limit_trigger.py new file mode 100644 index 0000000..cd96040 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/limit_trigger.py @@ -0,0 +1,32 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class LimitTrigger(): + """A Predicate to decide whether to stop.""" + + def __init__(self, limit: int, unit: str): + if unit not in ("iteration", "epoch"): + raise ValueError("unit should be 'iteration' or 'epoch'") + if limit <= 0: + raise ValueError("limit should be a positive integer.") + self.limit = limit + self.unit = unit + + def __call__(self, trainer): + state = trainer.updater.state + index = getattr(state, self.unit) + fire = index >= self.limit + return fire diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/time_trigger.py b/ernie-sat/paddlespeech/s2t/training/triggers/time_trigger.py new file mode 100644 index 0000000..53c398d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/time_trigger.py @@ -0,0 +1,42 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class TimeTrigger(): + """Trigger based on a fixed time interval. + This trigger accepts iterations with a given interval time. + Args: + period (float): Interval time. It is given in seconds. + """ + + def __init__(self, period): + self._period = period + self._next_time = self._period + + def __call__(self, trainer): + if self._next_time < trainer.elapsed_time: + self._next_time += self._period + return True + else: + return False + + def state_dict(self): + state_dict = { + "next_time": self._next_time, + } + return state_dict + + def set_state_dict(self, state_dict): + self._next_time = state_dict['next_time'] diff --git a/ernie-sat/paddlespeech/s2t/training/triggers/utils.py b/ernie-sat/paddlespeech/s2t/training/triggers/utils.py new file mode 100644 index 0000000..1a7c429 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/triggers/utils.py @@ -0,0 +1,28 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .interval_trigger import IntervalTrigger + + +def never_fail_trigger(trainer): + return False + + +def get_trigger(trigger): + if trigger is None: + return never_fail_trigger + if callable(trigger): + return trigger + else: + trigger = IntervalTrigger(*trigger) + return trigger diff --git a/ernie-sat/paddlespeech/s2t/training/updaters/__init__.py b/ernie-sat/paddlespeech/s2t/training/updaters/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/updaters/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/training/updaters/standard_updater.py b/ernie-sat/paddlespeech/s2t/training/updaters/standard_updater.py new file mode 100644 index 0000000..a320a80 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/updaters/standard_updater.py @@ -0,0 +1,196 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from typing import Dict +from typing import Optional + +import paddle +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler + +from paddlespeech.s2t.training.reporter import report +from paddlespeech.s2t.training.updaters.updater import UpdaterBase +from paddlespeech.s2t.training.updaters.updater import UpdaterState +from paddlespeech.s2t.utils.log import Log + +__all__ = ["StandardUpdater"] + +logger = Log(__name__).getlog() + + +class StandardUpdater(UpdaterBase): + """An example of over-simplification. Things may not be that simple, but + you can subclass it to fit your need. + """ + + def __init__(self, + model: Layer, + optimizer: Optimizer, + scheduler: LRScheduler, + dataloader: DataLoader, + init_state: Optional[UpdaterState]=None): + super().__init__(init_state) + # it is designed to hold multiple models + models = {"main": model} + self.models: Dict[str, Layer] = models + self.model = model + + # it is designed to hold multiple optimizers + optimizers = {"main": optimizer} + self.optimizer = optimizer + self.optimizers: Dict[str, Optimizer] = optimizers + + # it is designed to hold multiple scheduler + schedulers = {"main": scheduler} + self.scheduler = scheduler + self.schedulers: Dict[str, LRScheduler] = schedulers + + # dataloaders + self.dataloader = dataloader + + self.train_iterator = iter(dataloader) + + def update(self): + # We increase the iteration index after updating and before extension. + # Here are the reasons. + + # 0. Snapshotting(as well as other extensions, like visualizer) is + # executed after a step of updating; + # 1. We decide to increase the iteration index after updating and + # before any all extension is executed. + # 3. We do not increase the iteration after extension because we + # prefer a consistent resume behavior, when load from a + # `snapshot_iter_100.pdz` then the next step to train is `101`, + # naturally. But if iteration is increased increased after + # extension(including snapshot), then, a `snapshot_iter_99` is + # loaded. You would need a extra increasing of the iteration idex + # before training to avoid another iteration `99`, which has been + # done before snapshotting. + # 4. Thus iteration index represrnts "currently how mant epochs has + # been done." + # NOTE: use report to capture the correctly value. If you want to + # report the learning rate used for a step, you must report it before + # the learning rate scheduler's step() has been called. In paddle's + # convention, we do not use an extension to change the learning rate. + # so if you want to report it, do it in the updater. + + # Then here comes the next question. When is the proper time to + # increase the epoch index? Since all extensions are executed after + # updating, it is the time that after updating is the proper time to + # increase epoch index. + # 1. If we increase the epoch index before updating, then an extension + # based ot epoch would miss the correct timing. It could only be + # triggerd after an extra updating. + # 2. Theoretically, when an epoch is done, the epoch index should be + # increased. So it would be increase after updating. + # 3. Thus, eppoch index represents "currently how many epochs has been + # done." So it starts from 0. + + # switch to training mode + for model in self.models.values(): + model.train() + + # training for a step is implemented here + with Timier("data time cost:{}"): + batch = self.read_batch() + with Timier("step time cost:{}"): + self.update_core(batch) + + self.state.iteration += 1 + if self.updates_per_epoch is not None: + if self.state.iteration % self.updates_per_epoch == 0: + self.state.epoch += 1 + + def update_core(self, batch): + """A simple case for a training step. Basic assumptions are: + Single model; + Single optimizer; + Single scheduler, and update learning rate each step; + A batch from the dataloader is just the input of the model; + The model return a single loss, or a dict containing serval losses. + Parameters updates at every batch, no gradient accumulation. + """ + loss = self.model(*batch) + + if isinstance(loss, paddle.Tensor): + loss_dict = {"main": loss} + else: + # Dict[str, Tensor] + loss_dict = loss + if "main" not in loss_dict: + main_loss = 0 + for loss_item in loss.values(): + main_loss += loss_item + loss_dict["main"] = main_loss + + for name, loss_item in loss_dict.items(): + report(name, float(loss_item)) + + self.optimizer.clear_grad() + loss_dict["main"].backward() + self.optimizer.step() + self.scheduler.step() + + @property + def updates_per_epoch(self): + """Number of steps per epoch, + determined by the length of the dataloader.""" + length_of_dataloader = None + try: + length_of_dataloader = len(self.dataloader) + except TypeError: + logger.debug("This dataloader has no __len__.") + finally: + return length_of_dataloader + + def new_epoch(self): + """Start a new epoch.""" + # NOTE: all batch sampler for distributed training should + # subclass DistributedBatchSampler and implement `set_epoch` method + if hasattr(self.dataloader, "batch_sampler"): + batch_sampler = self.dataloader.batch_sampler + if isinstance(batch_sampler, DistributedBatchSampler): + batch_sampler.set_epoch(self.state.epoch) + self.train_iterator = iter(self.dataloader) + + def read_batch(self): + """Read a batch from the data loader, auto renew when data is exhausted.""" + try: + batch = next(self.train_iterator) + except StopIteration: + self.new_epoch() + batch = next(self.train_iterator) + return batch + + def state_dict(self): + """State dict of a Updater, model, optimizers/schedulers + and updater state are included.""" + state_dict = super().state_dict() + for name, model in self.models.items(): + state_dict[f"{name}_params"] = model.state_dict() + for name, optim in self.optimizers.items(): + state_dict[f"{name}_optimizer"] = optim.state_dict() + return state_dict + + def set_state_dict(self, state_dict): + """Set state dict for a Updater. Parameters of models, states for + optimizers/schedulers and UpdaterState are restored.""" + for name, model in self.models.items(): + model.set_state_dict(state_dict[f"{name}_params"]) + for name, optim in self.optimizers.items(): + optim.set_state_dict(state_dict[f"{name}_optimizer"]) + super().set_state_dict(state_dict) diff --git a/ernie-sat/paddlespeech/s2t/training/updaters/trainer.py b/ernie-sat/paddlespeech/s2t/training/updaters/trainer.py new file mode 100644 index 0000000..a0698c6 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/updaters/trainer.py @@ -0,0 +1,185 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import sys +import traceback +from collections import OrderedDict +from pathlib import Path +from typing import Callable +from typing import List +from typing import Union + +import six +import tqdm + +from paddlespeech.s2t.training.extensions.extension import Extension +from paddlespeech.s2t.training.extensions.extension import PRIORITY_READER +from paddlespeech.s2t.training.reporter import ObsScope +from paddlespeech.s2t.training.triggers import get_trigger +from paddlespeech.s2t.training.triggers.limit_trigger import LimitTrigger +from paddlespeech.s2t.training.updaters.updater import UpdaterBase + + +class _ExtensionEntry(): + def __init__(self, extension, trigger, priority): + self.extension = extension + self.trigger = trigger + self.priority = priority + + +class Trainer(): + def __init__(self, + updater: UpdaterBase, + stop_trigger: Callable=None, + out: Union[str, Path]='result', + extensions: List[Extension]=None): + self.updater = updater + self.extensions = OrderedDict() + self.stop_trigger = LimitTrigger(*stop_trigger) + self.out = Path(out) + self.observation = None + + self._done = False + if extensions: + for ext in extensions: + self.extend(ext) + + @property + def is_before_training(self): + return self.updater.state.iteration == 0 + + def extend(self, extension, name=None, trigger=None, priority=None): + # get name for the extension + # argument \ + # -> extention's name \ + # -> default_name (class name, when it is an object) \ + # -> function name when it is a function \ + # -> error + + if name is None: + name = getattr(extension, 'name', None) + if name is None: + name = getattr(extension, 'default_name', None) + if name is None: + name = getattr(extension, '__name__', None) + if name is None: + raise ValueError("Name is not given for the extension.") + if name == 'training': + raise ValueError("training is a reserved name.") + + if trigger is None: + trigger = getattr(extension, 'trigger', (1, 'iteration')) + trigger = get_trigger(trigger) + + if priority is None: + priority = getattr(extension, 'priority', PRIORITY_READER) + + # add suffix to avoid nameing conflict + ordinal = 0 + modified_name = name + while modified_name in self.extensions: + ordinal += 1 + modified_name = f"{name}_{ordinal}" + extension.name = modified_name + + self.extensions[modified_name] = _ExtensionEntry(extension, trigger, + priority) + + def get_extension(self, name): + """get extension by name.""" + extensions = self.extensions + if name in extensions: + return extensions[name].extension + else: + raise ValueError(f'extension {name} not found') + + def run(self): + if self._done: + raise RuntimeError("Training is already done!.") + + self.out.mkdir(parents=True, exist_ok=True) + + # sort extensions by priorities once + extension_order = sorted( + self.extensions.keys(), + key=lambda name: self.extensions[name].priority, + reverse=True) + extensions = [(name, self.extensions[name]) for name in extension_order] + + # initializing all extensions + for name, entry in extensions: + if hasattr(entry.extension, "initialize"): + entry.extension.initialize(self) + + update = self.updater.update # training step + stop_trigger = self.stop_trigger + + # display only one progress bar + max_iteration = None + if isinstance(stop_trigger, LimitTrigger): + if stop_trigger.unit == 'epoch': + max_epoch = self.stop_trigger.limit + updates_per_epoch = getattr(self.updater, "updates_per_epoch", + None) + max_iteration = max_epoch * updates_per_epoch if updates_per_epoch else None + else: + max_iteration = self.stop_trigger.limit + + p = tqdm.tqdm(initial=self.updater.state.iteration, total=max_iteration) + + try: + while not stop_trigger(self): + self.observation = {} + # set observation as the `report` target + # you can use `report` freely in Updater.update() + + # updating parameters and state + with ObsScope(self.observation): + update() + p.update() + + # execute extension when necessary + for name, entry in extensions: + if entry.trigger(self): + entry.extension(self) + + # print("###", self.observation) + except Exception as e: + f = sys.stderr + f.write(f"Exception in main training loop: {e}\n") + f.write("Traceback (most recent call last):\n") + traceback.print_tb(sys.exc_info()[2]) + f.write( + "Trainer extensions will try to handle the extension. Then all extensions will finalize." + ) + + # capture the exception in the mian training loop + exc_info = sys.exc_info() + + # try to handle it + for name, entry in extensions: + if hasattr(entry.extension, "on_error"): + try: + entry.extension.on_error(self, e, sys.exc_info()[2]) + except Exception as ee: + f.write(f"Exception in error handler: {ee}\n") + f.write('Traceback (most recent call last):\n') + traceback.print_tb(sys.exc_info()[2]) + + # raise exception in main training loop + six.reraise(*exc_info) + finally: + for name, entry in extensions: + if hasattr(entry.extension, "finalize"): + entry.extension.finalize(self) diff --git a/ernie-sat/paddlespeech/s2t/training/updaters/updater.py b/ernie-sat/paddlespeech/s2t/training/updaters/updater.py new file mode 100644 index 0000000..6875deb --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/training/updaters/updater.py @@ -0,0 +1,85 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from dataclasses import dataclass + +import paddle + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["UpdaterBase", "UpdaterState"] + +logger = Log(__name__).getlog() + + +@dataclass +class UpdaterState: + iteration: int = 0 + epoch: int = 0 + + +class UpdaterBase(): + """An updater is the abstraction of how a model is trained given the + dataloader and the optimizer. + The `update_core` method is a step in the training loop with only necessary + operations (get a batch, forward and backward, update the parameters). + Other stuffs are made extensions. Visualization, saving, loading and + periodical validation and evaluation are not considered here. + But even in such simplist case, things are not that simple. There is an + attempt to standardize this process and requires only the model and + dataset and do all the stuffs automatically. But this may hurt flexibility. + If we assume a batch yield from the dataloader is just the input to the + model, we will find that some model requires more arguments, or just some + keyword arguments. But this prevents us from over-simplifying it. + From another perspective, the batch may includes not just the input, but + also the target. But the model's forward method may just need the input. + We can pass a dict or a super-long tuple to the model and let it pick what + it really needs. But this is an abuse of lazy interface. + After all, we care about how a model is trained. But just how the model is + used for inference. We want to control how a model is trained. We just + don't want to be messed up with other auxiliary code. + So the best practice is to define a model and define a updater for it. + """ + + def __init__(self, init_state=None): + # init state + if init_state is None: + self.state = UpdaterState() + else: + self.state = init_state + + def update(self, batch): + raise NotImplementedError( + "Implement your own `update` method for training a step.") + + def state_dict(self): + state_dict = { + "epoch": self.state.epoch, + "iteration": self.state.iteration, + } + return state_dict + + def set_state_dict(self, state_dict): + self.state.epoch = state_dict["epoch"] + self.state.iteration = state_dict["iteration"] + + def save(self, path): + logger.debug(f"Saving to {path}.") + archive = self.state_dict() + paddle.save(archive, str(path)) + + def load(self, path): + logger.debug(f"Loading from {path}.") + archive = paddle.load(str(path)) + self.set_state_dict(archive) diff --git a/ernie-sat/paddlespeech/s2t/transform/__init__.py b/ernie-sat/paddlespeech/s2t/transform/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/transform/add_deltas.py b/ernie-sat/paddlespeech/s2t/transform/add_deltas.py new file mode 100644 index 0000000..1387fe9 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/add_deltas.py @@ -0,0 +1,54 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import numpy as np + + +def delta(feat, window): + assert window > 0 + delta_feat = np.zeros_like(feat) + for i in range(1, window + 1): + delta_feat[:-i] += i * feat[i:] + delta_feat[i:] += -i * feat[:-i] + delta_feat[-i:] += i * feat[-1] + delta_feat[:i] += -i * feat[0] + delta_feat /= 2 * sum(i**2 for i in range(1, window + 1)) + return delta_feat + + +def add_deltas(x, window=2, order=2): + """ + Args: + x (np.ndarray): speech feat, (T, D). + + Return: + np.ndarray: (T, (1+order)*D) + """ + feats = [x] + for _ in range(order): + feats.append(delta(feats[-1], window)) + return np.concatenate(feats, axis=1) + + +class AddDeltas(): + def __init__(self, window=2, order=2): + self.window = window + self.order = order + + def __repr__(self): + return "{name}(window={window}, order={order}".format( + name=self.__class__.__name__, window=self.window, order=self.order) + + def __call__(self, x): + return add_deltas(x, window=self.window, order=self.order) diff --git a/ernie-sat/paddlespeech/s2t/transform/channel_selector.py b/ernie-sat/paddlespeech/s2t/transform/channel_selector.py new file mode 100644 index 0000000..b078dcf --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/channel_selector.py @@ -0,0 +1,57 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import numpy + + +class ChannelSelector(): + """Select 1ch from multi-channel signal""" + + def __init__(self, train_channel="random", eval_channel=0, axis=1): + self.train_channel = train_channel + self.eval_channel = eval_channel + self.axis = axis + + def __repr__(self): + return ("{name}(train_channel={train_channel}, " + "eval_channel={eval_channel}, axis={axis})".format( + name=self.__class__.__name__, + train_channel=self.train_channel, + eval_channel=self.eval_channel, + axis=self.axis, )) + + def __call__(self, x, train=True): + # Assuming x: [Time, Channel] by default + + if x.ndim <= self.axis: + # If the dimension is insufficient, then unsqueeze + # (e.g [Time] -> [Time, 1]) + ind = tuple( + slice(None) if i < x.ndim else None + for i in range(self.axis + 1)) + x = x[ind] + + if train: + channel = self.train_channel + else: + channel = self.eval_channel + + if channel == "random": + ch = numpy.random.randint(0, x.shape[self.axis]) + else: + ch = channel + + ind = tuple( + slice(None) if i != self.axis else ch for i in range(x.ndim)) + return x[ind] diff --git a/ernie-sat/paddlespeech/s2t/transform/cmvn.py b/ernie-sat/paddlespeech/s2t/transform/cmvn.py new file mode 100644 index 0000000..2db0070 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/cmvn.py @@ -0,0 +1,201 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import io +import json + +import h5py +import kaldiio +import numpy as np + + +class CMVN(): + "Apply Global/Spk CMVN/iverserCMVN." + + def __init__( + self, + stats, + norm_means=True, + norm_vars=False, + filetype="mat", + utt2spk=None, + spk2utt=None, + reverse=False, + std_floor=1.0e-20, ): + self.stats_file = stats + self.norm_means = norm_means + self.norm_vars = norm_vars + self.reverse = reverse + + if isinstance(stats, dict): + stats_dict = dict(stats) + else: + # Use for global CMVN + if filetype == "mat": + stats_dict = {None: kaldiio.load_mat(stats)} + # Use for global CMVN + elif filetype == "npy": + stats_dict = {None: np.load(stats)} + # Use for speaker CMVN + elif filetype == "ark": + self.accept_uttid = True + stats_dict = dict(kaldiio.load_ark(stats)) + # Use for speaker CMVN + elif filetype == "hdf5": + self.accept_uttid = True + stats_dict = h5py.File(stats) + else: + raise ValueError("Not supporting filetype={}".format(filetype)) + + if utt2spk is not None: + self.utt2spk = {} + with io.open(utt2spk, "r", encoding="utf-8") as f: + for line in f: + utt, spk = line.rstrip().split(None, 1) + self.utt2spk[utt] = spk + elif spk2utt is not None: + self.utt2spk = {} + with io.open(spk2utt, "r", encoding="utf-8") as f: + for line in f: + spk, utts = line.rstrip().split(None, 1) + for utt in utts.split(): + self.utt2spk[utt] = spk + else: + self.utt2spk = None + + # Kaldi makes a matrix for CMVN which has a shape of (2, feat_dim + 1), + # and the first vector contains the sum of feats and the second is + # the sum of squares. The last value of the first, i.e. stats[0,-1], + # is the number of samples for this statistics. + self.bias = {} + self.scale = {} + for spk, stats in stats_dict.items(): + assert len(stats) == 2, stats.shape + + count = stats[0, -1] + + # If the feature has two or more dimensions + if not (np.isscalar(count) or isinstance(count, (int, float))): + # The first is only used + count = count.flatten()[0] + + mean = stats[0, :-1] / count + # V(x) = E(x^2) - (E(x))^2 + var = stats[1, :-1] / count - mean * mean + std = np.maximum(np.sqrt(var), std_floor) + self.bias[spk] = -mean + self.scale[spk] = 1 / std + + def __repr__(self): + return ("{name}(stats_file={stats_file}, " + "norm_means={norm_means}, norm_vars={norm_vars}, " + "reverse={reverse})".format( + name=self.__class__.__name__, + stats_file=self.stats_file, + norm_means=self.norm_means, + norm_vars=self.norm_vars, + reverse=self.reverse, )) + + def __call__(self, x, uttid=None): + if self.utt2spk is not None: + spk = self.utt2spk[uttid] + else: + spk = uttid + + if not self.reverse: + # apply cmvn + if self.norm_means: + x = np.add(x, self.bias[spk]) + if self.norm_vars: + x = np.multiply(x, self.scale[spk]) + + else: + # apply reverse cmvn + if self.norm_vars: + x = np.divide(x, self.scale[spk]) + if self.norm_means: + x = np.subtract(x, self.bias[spk]) + + return x + + +class UtteranceCMVN(): + "Apply Utterance CMVN" + + def __init__(self, norm_means=True, norm_vars=False, std_floor=1.0e-20): + self.norm_means = norm_means + self.norm_vars = norm_vars + self.std_floor = std_floor + + def __repr__(self): + return "{name}(norm_means={norm_means}, norm_vars={norm_vars})".format( + name=self.__class__.__name__, + norm_means=self.norm_means, + norm_vars=self.norm_vars, ) + + def __call__(self, x, uttid=None): + # x: [Time, Dim] + square_sums = (x**2).sum(axis=0) + mean = x.mean(axis=0) + + if self.norm_means: + x = np.subtract(x, mean) + + if self.norm_vars: + var = square_sums / x.shape[0] - mean**2 + std = np.maximum(np.sqrt(var), self.std_floor) + x = np.divide(x, std) + + return x + + +class GlobalCMVN(): + "Apply Global CMVN" + + def __init__(self, + cmvn_path, + norm_means=True, + norm_vars=True, + std_floor=1.0e-20): + # cmvn_path: Option[str, dict] + cmvn = cmvn_path + self.cmvn = cmvn + self.norm_means = norm_means + self.norm_vars = norm_vars + self.std_floor = std_floor + if isinstance(cmvn, dict): + cmvn_stats = cmvn + else: + with open(cmvn) as f: + cmvn_stats = json.load(f) + self.count = cmvn_stats['frame_num'] + self.mean = np.array(cmvn_stats['mean_stat']) / self.count + self.square_sums = np.array(cmvn_stats['var_stat']) + self.var = self.square_sums / self.count - self.mean**2 + self.std = np.maximum(np.sqrt(self.var), self.std_floor) + + def __repr__(self): + return f"""{self.__class__.__name__}( + cmvn_path={self.cmvn}, + norm_means={self.norm_means}, + norm_vars={self.norm_vars},)""" + + def __call__(self, x, uttid=None): + # x: [Time, Dim] + if self.norm_means: + x = np.subtract(x, self.mean) + + if self.norm_vars: + x = np.divide(x, self.std) + return x diff --git a/ernie-sat/paddlespeech/s2t/transform/functional.py b/ernie-sat/paddlespeech/s2t/transform/functional.py new file mode 100644 index 0000000..ccb5008 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/functional.py @@ -0,0 +1,86 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import inspect + +from paddlespeech.s2t.transform.transform_interface import TransformInterface +from paddlespeech.s2t.utils.check_kwargs import check_kwargs + + +class FuncTrans(TransformInterface): + """Functional Transformation + + WARNING: + Builtin or C/C++ functions may not work properly + because this class heavily depends on the `inspect` module. + + Usage: + + >>> def foo_bar(x, a=1, b=2): + ... '''Foo bar + ... :param x: input + ... :param int a: default 1 + ... :param int b: default 2 + ... ''' + ... return x + a - b + + + >>> class FooBar(FuncTrans): + ... _func = foo_bar + ... __doc__ = foo_bar.__doc__ + """ + + _func = None + + def __init__(self, **kwargs): + self.kwargs = kwargs + check_kwargs(self.func, kwargs) + + def __call__(self, x): + return self.func(x, **self.kwargs) + + @classmethod + def add_arguments(cls, parser): + fname = cls._func.__name__.replace("_", "-") + group = parser.add_argument_group(fname + " transformation setting") + for k, v in cls.default_params().items(): + # TODO(karita): get help and choices from docstring? + attr = k.replace("_", "-") + group.add_argument(f"--{fname}-{attr}", default=v, type=type(v)) + return parser + + @property + def func(self): + return type(self)._func + + @classmethod + def default_params(cls): + try: + d = dict(inspect.signature(cls._func).parameters) + except ValueError: + d = dict() + return { + k: v.default + for k, v in d.items() if v.default != inspect.Parameter.empty + } + + def __repr__(self): + params = self.default_params() + params.update(**self.kwargs) + ret = self.__class__.__name__ + "(" + if len(params) == 0: + return ret + ")" + for k, v in params.items(): + ret += "{}={}, ".format(k, v) + return ret[:-2] + ")" diff --git a/ernie-sat/paddlespeech/s2t/transform/perturb.py b/ernie-sat/paddlespeech/s2t/transform/perturb.py new file mode 100644 index 0000000..9e41b82 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/perturb.py @@ -0,0 +1,470 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import librosa +import numpy +import scipy +import soundfile + +from paddlespeech.s2t.io.reader import SoundHDF5File + + +class SpeedPerturbation(): + """SpeedPerturbation + + The speed perturbation in kaldi uses sox-speed instead of sox-tempo, + and sox-speed just to resample the input, + i.e pitch and tempo are changed both. + + "Why use speed option instead of tempo -s in SoX for speed perturbation" + https://groups.google.com/forum/#!topic/kaldi-help/8OOG7eE4sZ8 + + Warning: + This function is very slow because of resampling. + I recommmend to apply speed-perturb outside the training using sox. + + """ + + def __init__( + self, + lower=0.9, + upper=1.1, + utt2ratio=None, + keep_length=True, + res_type="kaiser_best", + seed=None, ): + self.res_type = res_type + self.keep_length = keep_length + self.state = numpy.random.RandomState(seed) + + if utt2ratio is not None: + self.utt2ratio = {} + # Use the scheduled ratio for each utterances + self.utt2ratio_file = utt2ratio + self.lower = None + self.upper = None + self.accept_uttid = True + + with open(utt2ratio, "r") as f: + for line in f: + utt, ratio = line.rstrip().split(None, 1) + ratio = float(ratio) + self.utt2ratio[utt] = ratio + else: + self.utt2ratio = None + # The ratio is given on runtime randomly + self.lower = lower + self.upper = upper + + def __repr__(self): + if self.utt2ratio is None: + return "{}(lower={}, upper={}, " "keep_length={}, res_type={})".format( + self.__class__.__name__, + self.lower, + self.upper, + self.keep_length, + self.res_type, ) + else: + return "{}({}, res_type={})".format( + self.__class__.__name__, self.utt2ratio_file, self.res_type) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + x = x.astype(numpy.float32) + if self.accept_uttid: + ratio = self.utt2ratio[uttid] + else: + ratio = self.state.uniform(self.lower, self.upper) + + # Note1: resample requires the sampling-rate of input and output, + # but actually only the ratio is used. + y = librosa.resample( + x, orig_sr=ratio, target_sr=1, res_type=self.res_type) + + if self.keep_length: + diff = abs(len(x) - len(y)) + if len(y) > len(x): + # Truncate noise + y = y[diff // 2:-((diff + 1) // 2)] + elif len(y) < len(x): + # Assume the time-axis is the first: (Time, Channel) + pad_width = [(diff // 2, (diff + 1) // 2)] + [ + (0, 0) for _ in range(y.ndim - 1) + ] + y = numpy.pad( + y, pad_width=pad_width, constant_values=0, mode="constant") + return y + + +class SpeedPerturbationSox(): + """SpeedPerturbationSox + + The speed perturbation in kaldi uses sox-speed instead of sox-tempo, + and sox-speed just to resample the input, + i.e pitch and tempo are changed both. + + To speed up or slow down the sound of a file, + use speed to modify the pitch and the duration of the file. + This raises the speed and reduces the time. + The default factor is 1.0 which makes no change to the audio. + 2.0 doubles speed, thus time length is cut by a half and pitch is one interval higher. + + "Why use speed option instead of tempo -s in SoX for speed perturbation" + https://groups.google.com/forum/#!topic/kaldi-help/8OOG7eE4sZ8 + + tempo option: + sox -t wav input.wav -t wav output.tempo0.9.wav tempo -s 0.9 + + speed option: + sox -t wav input.wav -t wav output.speed0.9.wav speed 0.9 + + If we use speed option like above, the pitch of audio also will be changed, + but the tempo option does not change the pitch. + """ + + def __init__( + self, + lower=0.9, + upper=1.1, + utt2ratio=None, + keep_length=True, + sr=16000, + seed=None, ): + self.sr = sr + self.keep_length = keep_length + self.state = numpy.random.RandomState(seed) + + try: + import soxbindings as sox + except ImportError: + try: + from paddlespeech.s2t.utils import dynamic_pip_install + package = "sox" + dynamic_pip_install.install(package) + package = "soxbindings" + dynamic_pip_install.install(package) + import soxbindings as sox + except Exception: + raise RuntimeError( + "Can not install soxbindings on your system.") + self.sox = sox + + if utt2ratio is not None: + self.utt2ratio = {} + # Use the scheduled ratio for each utterances + self.utt2ratio_file = utt2ratio + self.lower = None + self.upper = None + self.accept_uttid = True + + with open(utt2ratio, "r") as f: + for line in f: + utt, ratio = line.rstrip().split(None, 1) + ratio = float(ratio) + self.utt2ratio[utt] = ratio + else: + self.utt2ratio = None + # The ratio is given on runtime randomly + self.lower = lower + self.upper = upper + + def __repr__(self): + if self.utt2ratio is None: + return f"""{self.__class__.__name__}( + lower={self.lower}, + upper={self.upper}, + keep_length={self.keep_length}, + sample_rate={self.sr})""" + + else: + return f"""{self.__class__.__name__}( + utt2ratio={self.utt2ratio_file}, + sample_rate={self.sr})""" + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + + x = x.astype(numpy.float32) + if self.accept_uttid: + ratio = self.utt2ratio[uttid] + else: + ratio = self.state.uniform(self.lower, self.upper) + + tfm = self.sox.Transformer() + tfm.set_globals(multithread=False) + tfm.speed(ratio) + y = tfm.build_array(input_array=x, sample_rate_in=self.sr) + + if self.keep_length: + diff = abs(len(x) - len(y)) + if len(y) > len(x): + # Truncate noise + y = y[diff // 2:-((diff + 1) // 2)] + elif len(y) < len(x): + # Assume the time-axis is the first: (Time, Channel) + pad_width = [(diff // 2, (diff + 1) // 2)] + [ + (0, 0) for _ in range(y.ndim - 1) + ] + y = numpy.pad( + y, pad_width=pad_width, constant_values=0, mode="constant") + + if y.ndim == 2 and x.ndim == 1: + # (T, C) -> (T) + y = y.sequence(1) + return y + + +class BandpassPerturbation(): + """BandpassPerturbation + + Randomly dropout along the frequency axis. + + The original idea comes from the following: + "randomly-selected frequency band was cut off under the constraint of + leaving at least 1,000 Hz band within the range of less than 4,000Hz." + (The Hitachi/JHU CHiME-5 system: Advances in speech recognition for + everyday home environments using multiple microphone arrays; + http://spandh.dcs.shef.ac.uk/chime_workshop/papers/CHiME_2018_paper_kanda.pdf) + + """ + + def __init__(self, lower=0.0, upper=0.75, seed=None, axes=(-1, )): + self.lower = lower + self.upper = upper + self.state = numpy.random.RandomState(seed) + # x_stft: (Time, Channel, Freq) + self.axes = axes + + def __repr__(self): + return "{}(lower={}, upper={})".format(self.__class__.__name__, + self.lower, self.upper) + + def __call__(self, x_stft, uttid=None, train=True): + if not train: + return x_stft + + if x_stft.ndim == 1: + raise RuntimeError("Input in time-freq domain: " + "(Time, Channel, Freq) or (Time, Freq)") + + ratio = self.state.uniform(self.lower, self.upper) + axes = [i if i >= 0 else x_stft.ndim - i for i in self.axes] + shape = [s if i in axes else 1 for i, s in enumerate(x_stft.shape)] + + mask = self.state.randn(*shape) > ratio + x_stft *= mask + return x_stft + + +class VolumePerturbation(): + def __init__(self, + lower=-1.6, + upper=1.6, + utt2ratio=None, + dbunit=True, + seed=None): + self.dbunit = dbunit + self.utt2ratio_file = utt2ratio + self.lower = lower + self.upper = upper + self.state = numpy.random.RandomState(seed) + + if utt2ratio is not None: + # Use the scheduled ratio for each utterances + self.utt2ratio = {} + self.lower = None + self.upper = None + self.accept_uttid = True + + with open(utt2ratio, "r") as f: + for line in f: + utt, ratio = line.rstrip().split(None, 1) + ratio = float(ratio) + self.utt2ratio[utt] = ratio + else: + # The ratio is given on runtime randomly + self.utt2ratio = None + + def __repr__(self): + if self.utt2ratio is None: + return "{}(lower={}, upper={}, dbunit={})".format( + self.__class__.__name__, self.lower, self.upper, self.dbunit) + else: + return '{}("{}", dbunit={})'.format( + self.__class__.__name__, self.utt2ratio_file, self.dbunit) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + + x = x.astype(numpy.float32) + + if self.accept_uttid: + ratio = self.utt2ratio[uttid] + else: + ratio = self.state.uniform(self.lower, self.upper) + if self.dbunit: + ratio = 10**(ratio / 20) + return x * ratio + + +class NoiseInjection(): + """Add isotropic noise""" + + def __init__( + self, + utt2noise=None, + lower=-20, + upper=-5, + utt2ratio=None, + filetype="list", + dbunit=True, + seed=None, ): + self.utt2noise_file = utt2noise + self.utt2ratio_file = utt2ratio + self.filetype = filetype + self.dbunit = dbunit + self.lower = lower + self.upper = upper + self.state = numpy.random.RandomState(seed) + + if utt2ratio is not None: + # Use the scheduled ratio for each utterances + self.utt2ratio = {} + with open(utt2noise, "r") as f: + for line in f: + utt, snr = line.rstrip().split(None, 1) + snr = float(snr) + self.utt2ratio[utt] = snr + else: + # The ratio is given on runtime randomly + self.utt2ratio = None + + if utt2noise is not None: + self.utt2noise = {} + if filetype == "list": + with open(utt2noise, "r") as f: + for line in f: + utt, filename = line.rstrip().split(None, 1) + signal, rate = soundfile.read(filename, dtype="int16") + # Load all files in memory + self.utt2noise[utt] = (signal, rate) + + elif filetype == "sound.hdf5": + self.utt2noise = SoundHDF5File(utt2noise, "r") + else: + raise ValueError(filetype) + else: + self.utt2noise = None + + if utt2noise is not None and utt2ratio is not None: + if set(self.utt2ratio) != set(self.utt2noise): + raise RuntimeError("The uttids mismatch between {} and {}". + format(utt2ratio, utt2noise)) + + def __repr__(self): + if self.utt2ratio is None: + return "{}(lower={}, upper={}, dbunit={})".format( + self.__class__.__name__, self.lower, self.upper, self.dbunit) + else: + return '{}("{}", dbunit={})'.format( + self.__class__.__name__, self.utt2ratio_file, self.dbunit) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + x = x.astype(numpy.float32) + + # 1. Get ratio of noise to signal in sound pressure level + if uttid is not None and self.utt2ratio is not None: + ratio = self.utt2ratio[uttid] + else: + ratio = self.state.uniform(self.lower, self.upper) + + if self.dbunit: + ratio = 10**(ratio / 20) + scale = ratio * numpy.sqrt((x**2).mean()) + + # 2. Get noise + if self.utt2noise is not None: + # Get noise from the external source + if uttid is not None: + noise, rate = self.utt2noise[uttid] + else: + # Randomly select the noise source + noise = self.state.choice(list(self.utt2noise.values())) + # Normalize the level + noise /= numpy.sqrt((noise**2).mean()) + + # Adjust the noise length + diff = abs(len(x) - len(noise)) + offset = self.state.randint(0, diff) + if len(noise) > len(x): + # Truncate noise + noise = noise[offset:-(diff - offset)] + else: + noise = numpy.pad( + noise, pad_width=[offset, diff - offset], mode="wrap") + + else: + # Generate white noise + noise = self.state.normal(0, 1, x.shape) + + # 3. Add noise to signal + return x + noise * scale + + +class RIRConvolve(): + def __init__(self, utt2rir, filetype="list"): + self.utt2rir_file = utt2rir + self.filetype = filetype + + self.utt2rir = {} + if filetype == "list": + with open(utt2rir, "r") as f: + for line in f: + utt, filename = line.rstrip().split(None, 1) + signal, rate = soundfile.read(filename, dtype="int16") + self.utt2rir[utt] = (signal, rate) + + elif filetype == "sound.hdf5": + self.utt2rir = SoundHDF5File(utt2rir, "r") + else: + raise NotImplementedError(filetype) + + def __repr__(self): + return '{}("{}")'.format(self.__class__.__name__, self.utt2rir_file) + + def __call__(self, x, uttid=None, train=True): + if not train: + return x + + x = x.astype(numpy.float32) + + if x.ndim != 1: + # Must be single channel + raise RuntimeError( + "Input x must be one dimensional array, but got {}".format( + x.shape)) + + rir, rate = self.utt2rir[uttid] + if rir.ndim == 2: + # FIXME(kamo): Use chainer.convolution_1d? + # return [Time, Channel] + return numpy.stack( + [scipy.convolve(x, r, mode="same") for r in rir], axis=-1) + else: + return scipy.convolve(x, rir, mode="same") diff --git a/ernie-sat/paddlespeech/s2t/transform/spec_augment.py b/ernie-sat/paddlespeech/s2t/transform/spec_augment.py new file mode 100644 index 0000000..5ce9508 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/spec_augment.py @@ -0,0 +1,214 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Spec Augment module for preprocessing i.e., data augmentation""" +import random + +import numpy +from PIL import Image +from PIL.Image import BICUBIC + +from paddlespeech.s2t.transform.functional import FuncTrans + + +def time_warp(x, max_time_warp=80, inplace=False, mode="PIL"): + """time warp for spec augment + + move random center frame by the random width ~ uniform(-window, window) + :param numpy.ndarray x: spectrogram (time, freq) + :param int max_time_warp: maximum time frames to warp + :param bool inplace: overwrite x with the result + :param str mode: "PIL" (default, fast, not differentiable) or "sparse_image_warp" + (slow, differentiable) + :returns numpy.ndarray: time warped spectrogram (time, freq) + """ + window = max_time_warp + if window == 0: + return x + + if mode == "PIL": + t = x.shape[0] + if t - window <= window: + return x + # NOTE: randrange(a, b) emits a, a + 1, ..., b - 1 + center = random.randrange(window, t - window) + warped = random.randrange(center - window, center + + window) + 1 # 1 ... t - 1 + + left = Image.fromarray(x[:center]).resize((x.shape[1], warped), BICUBIC) + right = Image.fromarray(x[center:]).resize((x.shape[1], t - warped), + BICUBIC) + if inplace: + x[:warped] = left + x[warped:] = right + return x + return numpy.concatenate((left, right), 0) + elif mode == "sparse_image_warp": + import paddle + + from espnet.utils import spec_augment + + # TODO(karita): make this differentiable again + return spec_augment.time_warp(paddle.to_tensor(x), window).numpy() + else: + raise NotImplementedError("unknown resize mode: " + mode + + ", choose one from (PIL, sparse_image_warp).") + + +class TimeWarp(FuncTrans): + _func = time_warp + __doc__ = time_warp.__doc__ + + def __call__(self, x, train): + if not train: + return x + return super().__call__(x) + + +def freq_mask(x, F=30, n_mask=2, replace_with_zero=True, inplace=False): + """freq mask for spec agument + + :param numpy.ndarray x: (time, freq) + :param int n_mask: the number of masks + :param bool inplace: overwrite + :param bool replace_with_zero: pad zero on mask if true else use mean + """ + if inplace: + cloned = x + else: + cloned = x.copy() + + num_mel_channels = cloned.shape[1] + fs = numpy.random.randint(0, F, size=(n_mask, 2)) + + for f, mask_end in fs: + f_zero = random.randrange(0, num_mel_channels - f) + mask_end += f_zero + + # avoids randrange error if values are equal and range is empty + if f_zero == f_zero + f: + continue + + if replace_with_zero: + cloned[:, f_zero:mask_end] = 0 + else: + cloned[:, f_zero:mask_end] = cloned.mean() + return cloned + + +class FreqMask(FuncTrans): + _func = freq_mask + __doc__ = freq_mask.__doc__ + + def __call__(self, x, train): + if not train: + return x + return super().__call__(x) + + +def time_mask(spec, T=40, n_mask=2, replace_with_zero=True, inplace=False): + """freq mask for spec agument + + :param numpy.ndarray spec: (time, freq) + :param int n_mask: the number of masks + :param bool inplace: overwrite + :param bool replace_with_zero: pad zero on mask if true else use mean + """ + if inplace: + cloned = spec + else: + cloned = spec.copy() + len_spectro = cloned.shape[0] + ts = numpy.random.randint(0, T, size=(n_mask, 2)) + for t, mask_end in ts: + # avoid randint range error + if len_spectro - t <= 0: + continue + t_zero = random.randrange(0, len_spectro - t) + + # avoids randrange error if values are equal and range is empty + if t_zero == t_zero + t: + continue + + mask_end += t_zero + if replace_with_zero: + cloned[t_zero:mask_end] = 0 + else: + cloned[t_zero:mask_end] = cloned.mean() + return cloned + + +class TimeMask(FuncTrans): + _func = time_mask + __doc__ = time_mask.__doc__ + + def __call__(self, x, train): + if not train: + return x + return super().__call__(x) + + +def spec_augment( + x, + resize_mode="PIL", + max_time_warp=80, + max_freq_width=27, + n_freq_mask=2, + max_time_width=100, + n_time_mask=2, + inplace=True, + replace_with_zero=True, ): + """spec agument + + apply random time warping and time/freq masking + default setting is based on LD (Librispeech double) in Table 2 + https://arxiv.org/pdf/1904.08779.pdf + + :param numpy.ndarray x: (time, freq) + :param str resize_mode: "PIL" (fast, nondifferentiable) or "sparse_image_warp" + (slow, differentiable) + :param int max_time_warp: maximum frames to warp the center frame in spectrogram (W) + :param int freq_mask_width: maximum width of the random freq mask (F) + :param int n_freq_mask: the number of the random freq mask (m_F) + :param int time_mask_width: maximum width of the random time mask (T) + :param int n_time_mask: the number of the random time mask (m_T) + :param bool inplace: overwrite intermediate array + :param bool replace_with_zero: pad zero on mask if true else use mean + """ + assert isinstance(x, numpy.ndarray) + assert x.ndim == 2 + x = time_warp(x, max_time_warp, inplace=inplace, mode=resize_mode) + x = freq_mask( + x, + max_freq_width, + n_freq_mask, + inplace=inplace, + replace_with_zero=replace_with_zero, ) + x = time_mask( + x, + max_time_width, + n_time_mask, + inplace=inplace, + replace_with_zero=replace_with_zero, ) + return x + + +class SpecAugment(FuncTrans): + _func = spec_augment + __doc__ = spec_augment.__doc__ + + def __call__(self, x, train): + if not train: + return x + return super().__call__(x) diff --git a/ernie-sat/paddlespeech/s2t/transform/spectrogram.py b/ernie-sat/paddlespeech/s2t/transform/spectrogram.py new file mode 100644 index 0000000..4a65548 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/spectrogram.py @@ -0,0 +1,475 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import librosa +import numpy as np +import paddle +from python_speech_features import logfbank + +import paddleaudio.compliance.kaldi as kaldi + + +def stft(x, + n_fft, + n_shift, + win_length=None, + window="hann", + center=True, + pad_mode="reflect"): + # x: [Time, Channel] + if x.ndim == 1: + single_channel = True + # x: [Time] -> [Time, Channel] + x = x[:, None] + else: + single_channel = False + x = x.astype(np.float32) + + # FIXME(kamo): librosa.stft can't use multi-channel? + # x: [Time, Channel, Freq] + x = np.stack( + [ + librosa.stft( + y=x[:, ch], + n_fft=n_fft, + hop_length=n_shift, + win_length=win_length, + window=window, + center=center, + pad_mode=pad_mode, ).T for ch in range(x.shape[1]) + ], + axis=1, ) + + if single_channel: + # x: [Time, Channel, Freq] -> [Time, Freq] + x = x[:, 0] + return x + + +def istft(x, n_shift, win_length=None, window="hann", center=True): + # x: [Time, Channel, Freq] + if x.ndim == 2: + single_channel = True + # x: [Time, Freq] -> [Time, Channel, Freq] + x = x[:, None, :] + else: + single_channel = False + + # x: [Time, Channel] + x = np.stack( + [ + librosa.istft( + stft_matrix=x[:, ch].T, # [Time, Freq] -> [Freq, Time] + hop_length=n_shift, + win_length=win_length, + window=window, + center=center, ) for ch in range(x.shape[1]) + ], + axis=1, ) + + if single_channel: + # x: [Time, Channel] -> [Time] + x = x[:, 0] + return x + + +def stft2logmelspectrogram(x_stft, + fs, + n_mels, + n_fft, + fmin=None, + fmax=None, + eps=1e-10): + # x_stft: (Time, Channel, Freq) or (Time, Freq) + fmin = 0 if fmin is None else fmin + fmax = fs / 2 if fmax is None else fmax + + # spc: (Time, Channel, Freq) or (Time, Freq) + spc = np.abs(x_stft) + # mel_basis: (Mel_freq, Freq) + mel_basis = librosa.filters.mel( + sr=fs, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax) + # lmspc: (Time, Channel, Mel_freq) or (Time, Mel_freq) + lmspc = np.log10(np.maximum(eps, np.dot(spc, mel_basis.T))) + + return lmspc + + +def spectrogram(x, n_fft, n_shift, win_length=None, window="hann"): + # x: (Time, Channel) -> spc: (Time, Channel, Freq) + spc = np.abs(stft(x, n_fft, n_shift, win_length, window=window)) + return spc + + +def logmelspectrogram( + x, + fs, + n_mels, + n_fft, + n_shift, + win_length=None, + window="hann", + fmin=None, + fmax=None, + eps=1e-10, + pad_mode="reflect", ): + # stft: (Time, Channel, Freq) or (Time, Freq) + x_stft = stft( + x, + n_fft=n_fft, + n_shift=n_shift, + win_length=win_length, + window=window, + pad_mode=pad_mode, ) + + return stft2logmelspectrogram( + x_stft, + fs=fs, + n_mels=n_mels, + n_fft=n_fft, + fmin=fmin, + fmax=fmax, + eps=eps) + + +class Spectrogram(): + def __init__(self, n_fft, n_shift, win_length=None, window="hann"): + self.n_fft = n_fft + self.n_shift = n_shift + self.win_length = win_length + self.window = window + + def __repr__(self): + return ("{name}(n_fft={n_fft}, n_shift={n_shift}, " + "win_length={win_length}, window={window})".format( + name=self.__class__.__name__, + n_fft=self.n_fft, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, )) + + def __call__(self, x): + return spectrogram( + x, + n_fft=self.n_fft, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, ) + + +class LogMelSpectrogram(): + def __init__( + self, + fs, + n_mels, + n_fft, + n_shift, + win_length=None, + window="hann", + fmin=None, + fmax=None, + eps=1e-10, ): + self.fs = fs + self.n_mels = n_mels + self.n_fft = n_fft + self.n_shift = n_shift + self.win_length = win_length + self.window = window + self.fmin = fmin + self.fmax = fmax + self.eps = eps + + def __repr__(self): + return ("{name}(fs={fs}, n_mels={n_mels}, n_fft={n_fft}, " + "n_shift={n_shift}, win_length={win_length}, window={window}, " + "fmin={fmin}, fmax={fmax}, eps={eps}))".format( + name=self.__class__.__name__, + fs=self.fs, + n_mels=self.n_mels, + n_fft=self.n_fft, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, + fmin=self.fmin, + fmax=self.fmax, + eps=self.eps, )) + + def __call__(self, x): + return logmelspectrogram( + x, + fs=self.fs, + n_mels=self.n_mels, + n_fft=self.n_fft, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, ) + + +class Stft2LogMelSpectrogram(): + def __init__(self, fs, n_mels, n_fft, fmin=None, fmax=None, eps=1e-10): + self.fs = fs + self.n_mels = n_mels + self.n_fft = n_fft + self.fmin = fmin + self.fmax = fmax + self.eps = eps + + def __repr__(self): + return ("{name}(fs={fs}, n_mels={n_mels}, n_fft={n_fft}, " + "fmin={fmin}, fmax={fmax}, eps={eps}))".format( + name=self.__class__.__name__, + fs=self.fs, + n_mels=self.n_mels, + n_fft=self.n_fft, + fmin=self.fmin, + fmax=self.fmax, + eps=self.eps, )) + + def __call__(self, x): + return stft2logmelspectrogram( + x, + fs=self.fs, + n_mels=self.n_mels, + n_fft=self.n_fft, + fmin=self.fmin, + fmax=self.fmax, ) + + +class Stft(): + def __init__( + self, + n_fft, + n_shift, + win_length=None, + window="hann", + center=True, + pad_mode="reflect", ): + self.n_fft = n_fft + self.n_shift = n_shift + self.win_length = win_length + self.window = window + self.center = center + self.pad_mode = pad_mode + + def __repr__(self): + return ("{name}(n_fft={n_fft}, n_shift={n_shift}, " + "win_length={win_length}, window={window}," + "center={center}, pad_mode={pad_mode})".format( + name=self.__class__.__name__, + n_fft=self.n_fft, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, + center=self.center, + pad_mode=self.pad_mode, )) + + def __call__(self, x): + return stft( + x, + self.n_fft, + self.n_shift, + win_length=self.win_length, + window=self.window, + center=self.center, + pad_mode=self.pad_mode, ) + + +class IStft(): + def __init__(self, n_shift, win_length=None, window="hann", center=True): + self.n_shift = n_shift + self.win_length = win_length + self.window = window + self.center = center + + def __repr__(self): + return ("{name}(n_shift={n_shift}, " + "win_length={win_length}, window={window}," + "center={center})".format( + name=self.__class__.__name__, + n_shift=self.n_shift, + win_length=self.win_length, + window=self.window, + center=self.center, )) + + def __call__(self, x): + return istft( + x, + self.n_shift, + win_length=self.win_length, + window=self.window, + center=self.center, ) + + +class LogMelSpectrogramKaldi(): + def __init__( + self, + fs=16000, + n_mels=80, + n_shift=160, # unit:sample, 10ms + win_length=400, # unit:sample, 25ms + energy_floor=0.0, + dither=0.1): + """ + The Kaldi implementation of LogMelSpectrogram + Args: + fs (int): sample rate of the audio + n_mels (int): number of mel filter banks + n_shift (int): number of points in a frame shift + win_length (int): number of points in a frame windows + energy_floor (float): Floor on energy in Spectrogram computation (absolute) + dither (float): Dithering constant + + Returns: + LogMelSpectrogramKaldi + """ + + self.fs = fs + self.n_mels = n_mels + num_point_ms = fs / 1000 + self.n_frame_length = win_length / num_point_ms + self.n_frame_shift = n_shift / num_point_ms + self.energy_floor = energy_floor + self.dither = dither + + def __repr__(self): + return ( + "{name}(fs={fs}, n_mels={n_mels}, " + "n_frame_shift={n_frame_shift}, n_frame_length={n_frame_length}, " + "dither={dither}))".format( + name=self.__class__.__name__, + fs=self.fs, + n_mels=self.n_mels, + n_frame_shift=self.n_frame_shift, + n_frame_length=self.n_frame_length, + dither=self.dither, )) + + def __call__(self, x, train): + """ + Args: + x (np.ndarray): shape (Ti,) + train (bool): True, train mode. + + Raises: + ValueError: not support (Ti, C) + + Returns: + np.ndarray: (T, D) + """ + dither = self.dither if train else 0.0 + if x.ndim != 1: + raise ValueError("Not support x: [Time, Channel]") + waveform = paddle.to_tensor(np.expand_dims(x, 0), dtype=paddle.float32) + mat = kaldi.fbank( + waveform, + n_mels=self.n_mels, + frame_length=self.n_frame_length, + frame_shift=self.n_frame_shift, + dither=dither, + energy_floor=self.energy_floor, + sr=self.fs) + mat = np.squeeze(mat.numpy()) + return mat + + +class LogMelSpectrogramKaldi_decay(): + def __init__( + self, + fs=16000, + n_mels=80, + n_fft=512, # fft point + n_shift=160, # unit:sample, 10ms + win_length=400, # unit:sample, 25ms + window="povey", + fmin=20, + fmax=None, + eps=1e-10, + dither=1.0): + self.fs = fs + self.n_mels = n_mels + self.n_fft = n_fft + if n_shift > win_length: + raise ValueError("Stride size must not be greater than " + "window size.") + self.n_shift = n_shift / fs # unit: ms + self.win_length = win_length / fs # unit: ms + + self.window = window + self.fmin = fmin + if fmax is None: + fmax_ = fmax if fmax else self.fs / 2 + elif fmax > int(self.fs / 2): + raise ValueError("fmax must not be greater than half of " + "sample rate.") + self.fmax = fmax_ + + self.eps = eps + self.remove_dc_offset = True + self.preemph = 0.97 + self.dither = dither # only work in train mode + + def __repr__(self): + return ( + "{name}(fs={fs}, n_mels={n_mels}, n_fft={n_fft}, " + "n_shift={n_shift}, win_length={win_length}, preemph={preemph}, window={window}, " + "fmin={fmin}, fmax={fmax}, eps={eps}, dither={dither}))".format( + name=self.__class__.__name__, + fs=self.fs, + n_mels=self.n_mels, + n_fft=self.n_fft, + n_shift=self.n_shift, + preemph=self.preemph, + win_length=self.win_length, + window=self.window, + fmin=self.fmin, + fmax=self.fmax, + eps=self.eps, + dither=self.dither, )) + + def __call__(self, x, train): + """ + + Args: + x (np.ndarray): shape (Ti,) + train (bool): True, train mode. + + Raises: + ValueError: not support (Ti, C) + + Returns: + np.ndarray: (T, D) + """ + dither = self.dither if train else 0.0 + if x.ndim != 1: + raise ValueError("Not support x: [Time, Channel]") + + if x.dtype in np.sctypes['float']: + # PCM32 -> PCM16 + bits = np.iinfo(np.int16).bits + x = x * 2**(bits - 1) + + # logfbank need PCM16 input + y = logfbank( + signal=x, + samplerate=self.fs, + winlen=self.win_length, # unit ms + winstep=self.n_shift, # unit ms + nfilt=self.n_mels, + nfft=self.n_fft, + lowfreq=self.fmin, + highfreq=self.fmax, + dither=dither, + remove_dc_offset=self.remove_dc_offset, + preemph=self.preemph, + wintype=self.window) + return y diff --git a/ernie-sat/paddlespeech/s2t/transform/transform_interface.py b/ernie-sat/paddlespeech/s2t/transform/transform_interface.py new file mode 100644 index 0000000..8bc6242 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/transform_interface.py @@ -0,0 +1,35 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) + + +class TransformInterface: + """Transform Interface""" + + def __call__(self, x): + raise NotImplementedError("__call__ method is not implemented") + + @classmethod + def add_arguments(cls, parser): + return parser + + def __repr__(self): + return self.__class__.__name__ + "()" + + +class Identity(TransformInterface): + """Identity Function""" + + def __call__(self, x): + return x diff --git a/ernie-sat/paddlespeech/s2t/transform/transformation.py b/ernie-sat/paddlespeech/s2t/transform/transformation.py new file mode 100644 index 0000000..3b433cb --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/transformation.py @@ -0,0 +1,158 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Transformation module.""" +import copy +import io +import logging +from collections import OrderedDict +from collections.abc import Sequence +from inspect import signature + +import yaml + +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +import_alias = dict( + identity="paddlespeech.s2t.transform.transform_interface:Identity", + time_warp="paddlespeech.s2t.transform.spec_augment:TimeWarp", + time_mask="paddlespeech.s2t.transform.spec_augment:TimeMask", + freq_mask="paddlespeech.s2t.transform.spec_augment:FreqMask", + spec_augment="paddlespeech.s2t.transform.spec_augment:SpecAugment", + speed_perturbation="paddlespeech.s2t.transform.perturb:SpeedPerturbation", + speed_perturbation_sox="paddlespeech.s2t.transform.perturb:SpeedPerturbationSox", + volume_perturbation="paddlespeech.s2t.transform.perturb:VolumePerturbation", + noise_injection="paddlespeech.s2t.transform.perturb:NoiseInjection", + bandpass_perturbation="paddlespeech.s2t.transform.perturb:BandpassPerturbation", + rir_convolve="paddlespeech.s2t.transform.perturb:RIRConvolve", + delta="paddlespeech.s2t.transform.add_deltas:AddDeltas", + cmvn="paddlespeech.s2t.transform.cmvn:CMVN", + utterance_cmvn="paddlespeech.s2t.transform.cmvn:UtteranceCMVN", + fbank="paddlespeech.s2t.transform.spectrogram:LogMelSpectrogram", + spectrogram="paddlespeech.s2t.transform.spectrogram:Spectrogram", + stft="paddlespeech.s2t.transform.spectrogram:Stft", + istft="paddlespeech.s2t.transform.spectrogram:IStft", + stft2fbank="paddlespeech.s2t.transform.spectrogram:Stft2LogMelSpectrogram", + wpe="paddlespeech.s2t.transform.wpe:WPE", + channel_selector="paddlespeech.s2t.transform.channel_selector:ChannelSelector", + fbank_kaldi="paddlespeech.s2t.transform.spectrogram:LogMelSpectrogramKaldi", + cmvn_json="paddlespeech.s2t.transform.cmvn:GlobalCMVN") + + +class Transformation(): + """Apply some functions to the mini-batch + + Examples: + >>> kwargs = {"process": [{"type": "fbank", + ... "n_mels": 80, + ... "fs": 16000}, + ... {"type": "cmvn", + ... "stats": "data/train/cmvn.ark", + ... "norm_vars": True}, + ... {"type": "delta", "window": 2, "order": 2}]} + >>> transform = Transformation(kwargs) + >>> bs = 10 + >>> xs = [np.random.randn(100, 80).astype(np.float32) + ... for _ in range(bs)] + >>> xs = transform(xs) + """ + + def __init__(self, conffile=None): + if conffile is not None: + if isinstance(conffile, dict): + self.conf = copy.deepcopy(conffile) + else: + with io.open(conffile, encoding="utf-8") as f: + self.conf = yaml.safe_load(f) + assert isinstance(self.conf, dict), type(self.conf) + else: + self.conf = {"mode": "sequential", "process": []} + + self.functions = OrderedDict() + if self.conf.get("mode", "sequential") == "sequential": + for idx, process in enumerate(self.conf["process"]): + assert isinstance(process, dict), type(process) + opts = dict(process) + process_type = opts.pop("type") + class_obj = dynamic_import(process_type, import_alias) + # TODO(karita): assert issubclass(class_obj, TransformInterface) + try: + self.functions[idx] = class_obj(**opts) + except TypeError: + try: + signa = signature(class_obj) + except ValueError: + # Some function, e.g. built-in function, are failed + pass + else: + logging.error("Expected signature: {}({})".format( + class_obj.__name__, signa)) + raise + else: + raise NotImplementedError( + "Not supporting mode={}".format(self.conf["mode"])) + + def __repr__(self): + rep = "\n" + "\n".join(" {}: {}".format(k, v) + for k, v in self.functions.items()) + return "{}({})".format(self.__class__.__name__, rep) + + def __call__(self, xs, uttid_list=None, **kwargs): + """Return new mini-batch + + :param Union[Sequence[np.ndarray], np.ndarray] xs: + :param Union[Sequence[str], str] uttid_list: + :return: batch: + :rtype: List[np.ndarray] + """ + if not isinstance(xs, Sequence): + is_batch = False + xs = [xs] + else: + is_batch = True + + if isinstance(uttid_list, str): + uttid_list = [uttid_list for _ in range(len(xs))] + + if self.conf.get("mode", "sequential") == "sequential": + for idx in range(len(self.conf["process"])): + func = self.functions[idx] + # TODO(karita): use TrainingTrans and UttTrans to check __call__ args + # Derive only the args which the func has + try: + param = signature(func).parameters + except ValueError: + # Some function, e.g. built-in function, are failed + param = {} + _kwargs = {k: v for k, v in kwargs.items() if k in param} + try: + if uttid_list is not None and "uttid" in param: + xs = [ + func(x, u, **_kwargs) + for x, u in zip(xs, uttid_list) + ] + else: + xs = [func(x, **_kwargs) for x in xs] + except Exception: + logging.fatal("Catch a exception from {}th func: {}".format( + idx, func)) + raise + else: + raise NotImplementedError( + "Not supporting mode={}".format(self.conf["mode"])) + + if is_batch: + return xs + else: + return xs[0] diff --git a/ernie-sat/paddlespeech/s2t/transform/wpe.py b/ernie-sat/paddlespeech/s2t/transform/wpe.py new file mode 100644 index 0000000..777379d --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/transform/wpe.py @@ -0,0 +1,58 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from nara_wpe.wpe import wpe + + +class WPE(object): + def __init__(self, + taps=10, + delay=3, + iterations=3, + psd_context=0, + statistics_mode="full"): + self.taps = taps + self.delay = delay + self.iterations = iterations + self.psd_context = psd_context + self.statistics_mode = statistics_mode + + def __repr__(self): + return ("{name}(taps={taps}, delay={delay}" + "iterations={iterations}, psd_context={psd_context}, " + "statistics_mode={statistics_mode})".format( + name=self.__class__.__name__, + taps=self.taps, + delay=self.delay, + iterations=self.iterations, + psd_context=self.psd_context, + statistics_mode=self.statistics_mode, )) + + def __call__(self, xs): + """Return enhanced + + :param np.ndarray xs: (Time, Channel, Frequency) + :return: enhanced_xs + :rtype: np.ndarray + + """ + # nara_wpe.wpe: (F, C, T) + xs = wpe( + xs.transpose((2, 1, 0)), + taps=self.taps, + delay=self.delay, + iterations=self.iterations, + psd_context=self.psd_context, + statistics_mode=self.statistics_mode, ) + return xs.transpose(2, 1, 0) diff --git a/ernie-sat/paddlespeech/s2t/utils/__init__.py b/ernie-sat/paddlespeech/s2t/utils/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/utils/asr_utils.py b/ernie-sat/paddlespeech/s2t/utils/asr_utils.py new file mode 100644 index 0000000..9184fd6 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/asr_utils.py @@ -0,0 +1,52 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference espnet Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +import json + +import numpy as np + +__all__ = ["label_smoothing_dist"] + + +def label_smoothing_dist(odim, lsm_type, transcript=None, blank=0): + """Obtain label distribution for loss smoothing. + + :param odim: + :param lsm_type: + :param blank: + :param transcript: + :return: + """ + if transcript is not None: + with open(transcript, "rb") as f: + trans_json = json.load(f)["utts"] + + if lsm_type == "unigram": + assert transcript is not None, ( + "transcript is required for %s label smoothing" % lsm_type) + labelcount = np.zeros(odim) + for k, v in trans_json.items(): + ids = np.array([int(n) for n in v["output"][0]["tokenid"].split()]) + # to avoid an error when there is no text in an uttrance + if len(ids) > 0: + labelcount[ids] += 1 + labelcount[odim - 1] = len(transcript) # count + labelcount[labelcount == 0] = 1 # flooring + labelcount[blank] = 0 # remove counts for blank + labeldist = labelcount.astype(np.float32) / np.sum(labelcount) + else: + logging.error("Error: unexpected label smoothing type: %s" % lsm_type) + sys.exit() + + return labeldist diff --git a/ernie-sat/paddlespeech/s2t/utils/bleu_score.py b/ernie-sat/paddlespeech/s2t/utils/bleu_score.py new file mode 100644 index 0000000..d7eb9c7 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/bleu_score.py @@ -0,0 +1,118 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""This module provides functions to calculate bleu score in different level. +e.g. wer for word-level, cer for char-level. +""" +import numpy as np +import sacrebleu + +__all__ = ['bleu', 'char_bleu', "ErrorCalculator"] + + +def bleu(hypothesis, reference): + """Calculate BLEU. BLEU compares reference text and + hypothesis text in word-level using scarebleu. + + :param reference: The reference sentences. + :type reference: list[list[str]] + :param hypothesis: The hypothesis sentence. + :type hypothesis: list[str] + :raises ValueError: If the reference length is zero. + """ + + return sacrebleu.corpus_bleu(hypothesis, reference) + + +def char_bleu(hypothesis, reference): + """Calculate BLEU. BLEU compares reference text and + hypothesis text in char-level using scarebleu. + + :param reference: The reference sentences. + :type reference: list[list[str]] + :param hypothesis: The hypothesis sentence. + :type hypothesis: list[str] + :raises ValueError: If the reference number is zero. + """ + hypothesis = [' '.join(list(hyp.replace(' ', ''))) for hyp in hypothesis] + reference = [[' '.join(list(ref_i.replace(' ', ''))) for ref_i in ref] + for ref in reference] + + return sacrebleu.corpus_bleu(hypothesis, reference) + + +class ErrorCalculator(): + """Calculate BLEU for ST and MT models during training. + + :param y_hats: numpy array with predicted text + :param y_pads: numpy array with true (target) text + :param char_list: vocabulary list + :param sym_space: space symbol + :param sym_pad: pad symbol + :param report_bleu: report BLUE score if True + """ + + def __init__(self, char_list, sym_space, sym_pad, report_bleu=False): + """Construct an ErrorCalculator object.""" + super().__init__() + self.char_list = char_list + self.space = sym_space + self.pad = sym_pad + self.report_bleu = report_bleu + if self.space in self.char_list: + self.idx_space = self.char_list.index(self.space) + else: + self.idx_space = None + + def __call__(self, ys_hat, ys_pad): + """Calculate corpus-level BLEU score. + + :param torch.Tensor ys_hat: prediction (batch, seqlen) + :param torch.Tensor ys_pad: reference (batch, seqlen) + :return: corpus-level BLEU score in a mini-batch + :rtype float + """ + bleu = None + if not self.report_bleu: + return bleu + + bleu = self.calculate_corpus_bleu(ys_hat, ys_pad) + return bleu + + def calculate_corpus_bleu(self, ys_hat, ys_pad): + """Calculate corpus-level BLEU score in a mini-batch. + + :param torch.Tensor seqs_hat: prediction (batch, seqlen) + :param torch.Tensor seqs_true: reference (batch, seqlen) + :return: corpus-level BLEU score + :rtype float + """ + seqs_hat, seqs_true = [], [] + for i, y_hat in enumerate(ys_hat): + y_true = ys_pad[i] + eos_true = np.where(y_true == -1)[0] + ymax = eos_true[0] if len(eos_true) > 0 else len(y_true) + # NOTE: padding index (-1) in y_true is used to pad y_hat + # because y_hats is not padded with -1 + seq_hat = [self.char_list[int(idx)] for idx in y_hat[:ymax]] + seq_true = [ + self.char_list[int(idx)] for idx in y_true if int(idx) != -1 + ] + seq_hat_text = "".join(seq_hat).replace(self.space, " ") + seq_hat_text = seq_hat_text.replace(self.pad, "") + seq_true_text = "".join(seq_true).replace(self.space, " ") + seqs_hat.append(seq_hat_text) + seqs_true.append(seq_true_text) + bleu = sacrebleu.corpus_bleu(seqs_hat, [[ref] for ref in seqs_true]) + return bleu.score * 100 diff --git a/ernie-sat/paddlespeech/s2t/utils/check_kwargs.py b/ernie-sat/paddlespeech/s2t/utils/check_kwargs.py new file mode 100644 index 0000000..0aa839a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/check_kwargs.py @@ -0,0 +1,35 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import inspect + + +def check_kwargs(func, kwargs, name=None): + """check kwargs are valid for func + + If kwargs are invalid, raise TypeError as same as python default + :param function func: function to be validated + :param dict kwargs: keyword arguments for func + :param str name: name used in TypeError (default is func name) + """ + try: + params = inspect.signature(func).parameters + except ValueError: + return + if name is None: + name = func.__name__ + for k in kwargs.keys(): + if k not in params: + raise TypeError( + f"{name}() got an unexpected keyword argument '{k}'") diff --git a/ernie-sat/paddlespeech/s2t/utils/checkpoint.py b/ernie-sat/paddlespeech/s2t/utils/checkpoint.py new file mode 100644 index 0000000..1d24c88 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/checkpoint.py @@ -0,0 +1,298 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import glob +import json +import os +import re +from pathlib import Path +from typing import Text +from typing import Union + +import paddle +from paddle import distributed as dist +from paddle.optimizer import Optimizer + +from paddlespeech.s2t.utils import mp_tools +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["Checkpoint"] + + +class Checkpoint(): + def __init__(self, kbest_n: int=5, latest_n: int=1): + self.best_records: Mapping[Path, float] = {} + self.latest_records = [] + self.kbest_n = kbest_n + self.latest_n = latest_n + self._save_all = (kbest_n == -1) + + def save_parameters(self, + checkpoint_dir, + tag_or_iteration: Union[int, Text], + model: paddle.nn.Layer, + optimizer: Optimizer=None, + infos: dict=None, + metric_type="val_loss"): + """Save checkpoint in best_n and latest_n. + + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + tag_or_iteration (int or str): the latest iteration(step or epoch) number or tag. + model (Layer): model to be checkpointed. + optimizer (Optimizer, optional): optimizer to be checkpointed. + infos (dict or None)): any info you want to save. + metric_type (str, optional): metric type. Defaults to "val_loss". + """ + if (metric_type not in infos.keys()): + self._save_parameters(checkpoint_dir, tag_or_iteration, model, + optimizer, infos) + return + + #save best + if self._should_save_best(infos[metric_type]): + self._save_best_checkpoint_and_update( + infos[metric_type], checkpoint_dir, tag_or_iteration, model, + optimizer, infos) + #save latest + self._save_latest_checkpoint_and_update( + checkpoint_dir, tag_or_iteration, model, optimizer, infos) + + if isinstance(tag_or_iteration, int): + self._save_checkpoint_record(checkpoint_dir, tag_or_iteration) + + def load_parameters(self, + model, + optimizer=None, + checkpoint_dir=None, + checkpoint_path=None, + record_file="checkpoint_latest"): + """Load a last model checkpoint from disk. + Args: + model (Layer): model to load parameters. + optimizer (Optimizer, optional): optimizer to load states if needed. + Defaults to None. + checkpoint_dir (str, optional): the directory where checkpoint is saved. + checkpoint_path (str, optional): if specified, load the checkpoint + stored in the checkpoint_path(prefix) and the argument 'checkpoint_dir' will + be ignored. Defaults to None. + record_file "checkpoint_latest" or "checkpoint_best" + Returns: + configs (dict): epoch or step, lr and other meta info should be saved. + """ + configs = {} + + if checkpoint_path: + pass + elif checkpoint_dir is not None and record_file is not None: + # load checkpint from record file + checkpoint_record = os.path.join(checkpoint_dir, record_file) + iteration = self._load_checkpoint_idx(checkpoint_record) + if iteration == -1: + return configs + checkpoint_path = os.path.join(checkpoint_dir, + "{}".format(iteration)) + else: + raise ValueError( + "At least one of 'checkpoint_path' or 'checkpoint_dir' should be specified!" + ) + + rank = dist.get_rank() + + params_path = checkpoint_path + ".pdparams" + model_dict = paddle.load(params_path) + model.set_state_dict(model_dict) + logger.info("Rank {}: Restore model from {}".format(rank, params_path)) + + optimizer_path = checkpoint_path + ".pdopt" + if optimizer and os.path.isfile(optimizer_path): + optimizer_dict = paddle.load(optimizer_path) + optimizer.set_state_dict(optimizer_dict) + logger.info("Rank {}: Restore optimizer state from {}".format( + rank, optimizer_path)) + + info_path = re.sub('.pdparams$', '.json', params_path) + if os.path.exists(info_path): + with open(info_path, 'r') as fin: + configs = json.load(fin) + return configs + + def load_latest_parameters(self, + model, + optimizer=None, + checkpoint_dir=None, + checkpoint_path=None): + """Load a last model checkpoint from disk. + Args: + model (Layer): model to load parameters. + optimizer (Optimizer, optional): optimizer to load states if needed. + Defaults to None. + checkpoint_dir (str, optional): the directory where checkpoint is saved. + checkpoint_path (str, optional): if specified, load the checkpoint + stored in the checkpoint_path(prefix) and the argument 'checkpoint_dir' will + be ignored. Defaults to None. + Returns: + configs (dict): epoch or step, lr and other meta info should be saved. + """ + return self.load_parameters(model, optimizer, checkpoint_dir, + checkpoint_path, "checkpoint_latest") + + def load_best_parameters(self, + model, + optimizer=None, + checkpoint_dir=None, + checkpoint_path=None): + """Load a last model checkpoint from disk. + Args: + model (Layer): model to load parameters. + optimizer (Optimizer, optional): optimizer to load states if needed. + Defaults to None. + checkpoint_dir (str, optional): the directory where checkpoint is saved. + checkpoint_path (str, optional): if specified, load the checkpoint + stored in the checkpoint_path(prefix) and the argument 'checkpoint_dir' will + be ignored. Defaults to None. + Returns: + configs (dict): epoch or step, lr and other meta info should be saved. + """ + return self.load_parameters(model, optimizer, checkpoint_dir, + checkpoint_path, "checkpoint_best") + + def _should_save_best(self, metric: float) -> bool: + if not self._best_full(): + return True + + # already full + worst_record_path = max(self.best_records, key=self.best_records.get) + # worst_record_path = max(self.best_records.iteritems(), key=operator.itemgetter(1))[0] + worst_metric = self.best_records[worst_record_path] + return metric < worst_metric + + def _best_full(self): + return (not self._save_all) and len(self.best_records) == self.kbest_n + + def _latest_full(self): + return len(self.latest_records) == self.latest_n + + def _save_best_checkpoint_and_update(self, metric, checkpoint_dir, + tag_or_iteration, model, optimizer, + infos): + # remove the worst + if self._best_full(): + worst_record_path = max(self.best_records, + key=self.best_records.get) + self.best_records.pop(worst_record_path) + if (worst_record_path not in self.latest_records): + logger.info( + "remove the worst checkpoint: {}".format(worst_record_path)) + self._del_checkpoint(checkpoint_dir, worst_record_path) + + # add the new one + self._save_parameters(checkpoint_dir, tag_or_iteration, model, + optimizer, infos) + self.best_records[tag_or_iteration] = metric + + def _save_latest_checkpoint_and_update( + self, checkpoint_dir, tag_or_iteration, model, optimizer, infos): + # remove the old + if self._latest_full(): + to_del_fn = self.latest_records.pop(0) + if (to_del_fn not in self.best_records.keys()): + logger.info( + "remove the latest checkpoint: {}".format(to_del_fn)) + self._del_checkpoint(checkpoint_dir, to_del_fn) + self.latest_records.append(tag_or_iteration) + + self._save_parameters(checkpoint_dir, tag_or_iteration, model, + optimizer, infos) + + def _del_checkpoint(self, checkpoint_dir, tag_or_iteration): + checkpoint_path = os.path.join(checkpoint_dir, + "{}".format(tag_or_iteration)) + for filename in glob.glob(checkpoint_path + ".*"): + os.remove(filename) + logger.info("delete file: {}".format(filename)) + + def _load_checkpoint_idx(self, checkpoint_record: str) -> int: + """Get the iteration number corresponding to the latest saved checkpoint. + Args: + checkpoint_path (str): the saved path of checkpoint. + Returns: + int: the latest iteration number. -1 for no checkpoint to load. + """ + if not os.path.isfile(checkpoint_record): + return -1 + + # Fetch the latest checkpoint index. + with open(checkpoint_record, "rt") as handle: + latest_checkpoint = handle.readlines()[-1].strip() + iteration = int(latest_checkpoint.split(":")[-1]) + return iteration + + def _save_checkpoint_record(self, checkpoint_dir: str, iteration: int): + """Save the iteration number of the latest model to be checkpoint record. + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + iteration (int): the latest iteration number. + Returns: + None + """ + checkpoint_record_latest = os.path.join(checkpoint_dir, + "checkpoint_latest") + checkpoint_record_best = os.path.join(checkpoint_dir, "checkpoint_best") + + with open(checkpoint_record_best, "w") as handle: + for i in self.best_records.keys(): + handle.write("model_checkpoint_path:{}\n".format(i)) + with open(checkpoint_record_latest, "w") as handle: + for i in self.latest_records: + handle.write("model_checkpoint_path:{}\n".format(i)) + + @mp_tools.rank_zero_only + def _save_parameters(self, + checkpoint_dir: str, + tag_or_iteration: Union[int, str], + model: paddle.nn.Layer, + optimizer: Optimizer=None, + infos: dict=None): + """Checkpoint the latest trained model parameters. + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + tag_or_iteration (int or str): the latest iteration(step or epoch) number. + model (Layer): model to be checkpointed. + optimizer (Optimizer, optional): optimizer to be checkpointed. + Defaults to None. + infos (dict or None): any info you want to save. + Returns: + None + """ + checkpoint_path = os.path.join(checkpoint_dir, + "{}".format(tag_or_iteration)) + + model_dict = model.state_dict() + params_path = checkpoint_path + ".pdparams" + paddle.save(model_dict, params_path) + logger.info("Saved model to {}".format(params_path)) + + if optimizer: + opt_dict = optimizer.state_dict() + optimizer_path = checkpoint_path + ".pdopt" + paddle.save(opt_dict, optimizer_path) + logger.info("Saved optimzier state to {}".format(optimizer_path)) + + info_path = re.sub('.pdparams$', '.json', params_path) + infos = {} if infos is None else infos + with open(info_path, 'w') as fout: + data = json.dumps(infos) + fout.write(data) diff --git a/ernie-sat/paddlespeech/s2t/utils/cli_readers.py b/ernie-sat/paddlespeech/s2t/utils/cli_readers.py new file mode 100644 index 0000000..735d590 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/cli_readers.py @@ -0,0 +1,242 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import io +import logging +import sys + +import h5py +import kaldiio +import soundfile + +from paddlespeech.s2t.io.reader import SoundHDF5File + + +def file_reader_helper( + rspecifier: str, + filetype: str="mat", + return_shape: bool=False, + segments: str=None, ): + """Read uttid and array in kaldi style + + This function might be a bit confusing as "ark" is used + for HDF5 to imitate "kaldi-rspecifier". + + Args: + rspecifier: Give as "ark:feats.ark" or "scp:feats.scp" + filetype: "mat" is kaldi-martix, "hdf5": HDF5 + return_shape: Return the shape of the matrix, + instead of the matrix. This can reduce IO cost for HDF5. + segments (str): The file format is + " \n" + "e.g. call-861225-A-0050-0065 call-861225-A 5.0 6.5\n" + Returns: + Generator[Tuple[str, np.ndarray], None, None]: + + Examples: + Read from kaldi-matrix ark file: + + >>> for u, array in file_reader_helper('ark:feats.ark', 'mat'): + ... array + + Read from HDF5 file: + + >>> for u, array in file_reader_helper('ark:feats.h5', 'hdf5'): + ... array + + """ + if filetype == "mat": + return KaldiReader( + rspecifier, return_shape=return_shape, segments=segments) + elif filetype == "hdf5": + return HDF5Reader(rspecifier, return_shape=return_shape) + elif filetype == "sound.hdf5": + return SoundHDF5Reader(rspecifier, return_shape=return_shape) + elif filetype == "sound": + return SoundReader(rspecifier, return_shape=return_shape) + else: + raise NotImplementedError(f"filetype={filetype}") + + +class KaldiReader: + def __init__(self, rspecifier, return_shape=False, segments=None): + self.rspecifier = rspecifier + self.return_shape = return_shape + self.segments = segments + + def __iter__(self): + with kaldiio.ReadHelper( + self.rspecifier, segments=self.segments) as reader: + for key, array in reader: + if self.return_shape: + array = array.shape + yield key, array + + +class HDF5Reader: + def __init__(self, rspecifier, return_shape=False): + if ":" not in rspecifier: + raise ValueError('Give "rspecifier" such as "ark:some.ark: {}"'. + format(self.rspecifier)) + self.rspecifier = rspecifier + self.ark_or_scp, self.filepath = self.rspecifier.split(":", 1) + if self.ark_or_scp not in ["ark", "scp"]: + raise ValueError(f"Must be scp or ark: {self.ark_or_scp}") + + self.return_shape = return_shape + + def __iter__(self): + if self.ark_or_scp == "scp": + hdf5_dict = {} + with open(self.filepath, "r", encoding="utf-8") as f: + for line in f: + key, value = line.rstrip().split(None, 1) + + if ":" not in value: + raise RuntimeError( + "scp file for hdf5 should be like: " + '"uttid filepath.h5:key": {}({})'.format( + line, self.filepath)) + path, h5_key = value.split(":", 1) + + hdf5_file = hdf5_dict.get(path) + if hdf5_file is None: + try: + hdf5_file = h5py.File(path, "r") + except Exception: + logging.error("Error when loading {}".format(path)) + raise + hdf5_dict[path] = hdf5_file + + try: + data = hdf5_file[h5_key] + except Exception: + logging.error("Error when loading {} with key={}". + format(path, h5_key)) + raise + + if self.return_shape: + yield key, data.shape + else: + yield key, data[()] + + # Closing all files + for k in hdf5_dict: + try: + hdf5_dict[k].close() + except Exception: + pass + + else: + if self.filepath == "-": + # Required h5py>=2.9 + filepath = io.BytesIO(sys.stdin.buffer.read()) + else: + filepath = self.filepath + with h5py.File(filepath, "r") as f: + for key in f: + if self.return_shape: + yield key, f[key].shape + else: + yield key, f[key][()] + + +class SoundHDF5Reader: + def __init__(self, rspecifier, return_shape=False): + if ":" not in rspecifier: + raise ValueError('Give "rspecifier" such as "ark:some.ark: {}"'. + format(rspecifier)) + self.ark_or_scp, self.filepath = rspecifier.split(":", 1) + if self.ark_or_scp not in ["ark", "scp"]: + raise ValueError(f"Must be scp or ark: {self.ark_or_scp}") + self.return_shape = return_shape + + def __iter__(self): + if self.ark_or_scp == "scp": + hdf5_dict = {} + with open(self.filepath, "r", encoding="utf-8") as f: + for line in f: + key, value = line.rstrip().split(None, 1) + + if ":" not in value: + raise RuntimeError( + "scp file for hdf5 should be like: " + '"uttid filepath.h5:key": {}({})'.format( + line, self.filepath)) + path, h5_key = value.split(":", 1) + + hdf5_file = hdf5_dict.get(path) + if hdf5_file is None: + try: + hdf5_file = SoundHDF5File(path, "r") + except Exception: + logging.error("Error when loading {}".format(path)) + raise + hdf5_dict[path] = hdf5_file + + try: + data = hdf5_file[h5_key] + except Exception: + logging.error("Error when loading {} with key={}". + format(path, h5_key)) + raise + + # Change Tuple[ndarray, int] -> Tuple[int, ndarray] + # (soundfile style -> scipy style) + array, rate = data + if self.return_shape: + array = array.shape + yield key, (rate, array) + + # Closing all files + for k in hdf5_dict: + try: + hdf5_dict[k].close() + except Exception: + pass + + else: + if self.filepath == "-": + # Required h5py>=2.9 + filepath = io.BytesIO(sys.stdin.buffer.read()) + else: + filepath = self.filepath + for key, (a, r) in SoundHDF5File(filepath, "r").items(): + if self.return_shape: + a = a.shape + yield key, (r, a) + + +class SoundReader: + def __init__(self, rspecifier, return_shape=False): + if ":" not in rspecifier: + raise ValueError('Give "rspecifier" such as "scp:some.scp: {}"'. + format(rspecifier)) + self.ark_or_scp, self.filepath = rspecifier.split(":", 1) + if self.ark_or_scp != "scp": + raise ValueError('Only supporting "scp" for sound file: {}'.format( + self.ark_or_scp)) + self.return_shape = return_shape + + def __iter__(self): + with open(self.filepath, "r", encoding="utf-8") as f: + for line in f: + key, sound_file_path = line.rstrip().split(None, 1) + # Assume PCM16 + array, rate = soundfile.read(sound_file_path, dtype="int16") + # Change Tuple[ndarray, int] -> Tuple[int, ndarray] + # (soundfile style -> scipy style) + if self.return_shape: + array = array.shape + yield key, (rate, array) diff --git a/ernie-sat/paddlespeech/s2t/utils/cli_utils.py b/ernie-sat/paddlespeech/s2t/utils/cli_utils.py new file mode 100644 index 0000000..ccb0d3c --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/cli_utils.py @@ -0,0 +1,71 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import sys +from collections.abc import Sequence + +import numpy +from distutils.util import strtobool as dist_strtobool + + +def strtobool(x): + # distutils.util.strtobool returns integer, but it's confusing, + return bool(dist_strtobool(x)) + + +def get_commandline_args(): + extra_chars = [ + " ", + ";", + "&", + "(", + ")", + "|", + "^", + "<", + ">", + "?", + "*", + "[", + "]", + "$", + "`", + '"', + "\\", + "!", + "{", + "}", + ] + + # Escape the extra characters for shell + argv = [ + arg.replace("'", "'\\''") if all(char not in arg + for char in extra_chars) else + "'" + arg.replace("'", "'\\''") + "'" for arg in sys.argv + ] + + return sys.executable + " " + " ".join(argv) + + +def is_scipy_wav_style(value): + # If Tuple[int, numpy.ndarray] or not + return (isinstance(value, Sequence) and len(value) == 2 and + isinstance(value[0], int) and isinstance(value[1], numpy.ndarray)) + + +def assert_scipy_wav_style(value): + assert is_scipy_wav_style( + value), "Must be Tuple[int, numpy.ndarray], but got {}".format( + type(value) if not isinstance(value, Sequence) else "{}[{}]".format( + type(value), ", ".join(str(type(v)) for v in value))) diff --git a/ernie-sat/paddlespeech/s2t/utils/cli_writers.py b/ernie-sat/paddlespeech/s2t/utils/cli_writers.py new file mode 100644 index 0000000..d3a4c2b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/cli_writers.py @@ -0,0 +1,294 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from pathlib import Path +from typing import Dict + +import h5py +import kaldiio +import numpy +import soundfile + +from paddlespeech.s2t.io.reader import SoundHDF5File +from paddlespeech.s2t.utils.cli_utils import assert_scipy_wav_style + + +def file_writer_helper( + wspecifier: str, + filetype: str="mat", + write_num_frames: str=None, + compress: bool=False, + compression_method: int=2, + pcm_format: str="wav", ): + """Write matrices in kaldi style + + Args: + wspecifier: e.g. ark,scp:out.ark,out.scp + filetype: "mat" is kaldi-martix, "hdf5": HDF5 + write_num_frames: e.g. 'ark,t:num_frames.txt' + compress: Compress or not + compression_method: Specify compression level + + Write in kaldi-matrix-ark with "kaldi-scp" file: + + >>> with file_writer_helper('ark,scp:out.ark,out.scp') as f: + >>> f['uttid'] = array + + This "scp" has the following format: + + uttidA out.ark:1234 + uttidB out.ark:2222 + + where, 1234 and 2222 points the strating byte address of the matrix. + (For detail, see official documentation of Kaldi) + + Write in HDF5 with "scp" file: + + >>> with file_writer_helper('ark,scp:out.h5,out.scp', 'hdf5') as f: + >>> f['uttid'] = array + + This "scp" file is created as: + + uttidA out.h5:uttidA + uttidB out.h5:uttidB + + HDF5 can be, unlike "kaldi-ark", accessed to any keys, + so originally "scp" is not required for random-reading. + Nevertheless we create "scp" for HDF5 because it is useful + for some use-case. e.g. Concatenation, Splitting. + + """ + if filetype == "mat": + return KaldiWriter( + wspecifier, + write_num_frames=write_num_frames, + compress=compress, + compression_method=compression_method, ) + elif filetype == "hdf5": + return HDF5Writer( + wspecifier, write_num_frames=write_num_frames, compress=compress) + elif filetype == "sound.hdf5": + return SoundHDF5Writer( + wspecifier, + write_num_frames=write_num_frames, + pcm_format=pcm_format) + elif filetype == "sound": + return SoundWriter( + wspecifier, + write_num_frames=write_num_frames, + pcm_format=pcm_format) + else: + raise NotImplementedError(f"filetype={filetype}") + + +class BaseWriter: + def __setitem__(self, key, value): + raise NotImplementedError + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): + try: + self.writer.close() + except Exception: + pass + + if self.writer_scp is not None: + try: + self.writer_scp.close() + except Exception: + pass + + if self.writer_nframe is not None: + try: + self.writer_nframe.close() + except Exception: + pass + + +def get_num_frames_writer(write_num_frames: str): + """get_num_frames_writer + + Examples: + >>> get_num_frames_writer('ark,t:num_frames.txt') + """ + if write_num_frames is not None: + if ":" not in write_num_frames: + raise ValueError('Must include ":", write_num_frames={}'.format( + write_num_frames)) + + nframes_type, nframes_file = write_num_frames.split(":", 1) + if nframes_type != "ark,t": + raise ValueError("Only supporting text mode. " + "e.g. --write-num-frames=ark,t:foo.txt :" + "{}".format(nframes_type)) + + return open(nframes_file, "w", encoding="utf-8") + + +class KaldiWriter(BaseWriter): + def __init__(self, + wspecifier, + write_num_frames=None, + compress=False, + compression_method=2): + if compress: + self.writer = kaldiio.WriteHelper( + wspecifier, compression_method=compression_method) + else: + self.writer = kaldiio.WriteHelper(wspecifier) + self.writer_scp = None + if write_num_frames is not None: + self.writer_nframe = get_num_frames_writer(write_num_frames) + else: + self.writer_nframe = None + + def __setitem__(self, key, value): + self.writer[key] = value + if self.writer_nframe is not None: + self.writer_nframe.write(f"{key} {len(value)}\n") + + +def parse_wspecifier(wspecifier: str) -> Dict[str, str]: + """Parse wspecifier to dict + + Examples: + >>> parse_wspecifier('ark,scp:out.ark,out.scp') + {'ark': 'out.ark', 'scp': 'out.scp'} + + """ + ark_scp, filepath = wspecifier.split(":", 1) + if ark_scp not in ["ark", "scp,ark", "ark,scp"]: + raise ValueError("{} is not allowed: {}".format(ark_scp, wspecifier)) + ark_scps = ark_scp.split(",") + filepaths = filepath.split(",") + if len(ark_scps) != len(filepaths): + raise ValueError("Mismatch: {} and {}".format(ark_scp, filepath)) + spec_dict = dict(zip(ark_scps, filepaths)) + return spec_dict + + +class HDF5Writer(BaseWriter): + """HDF5Writer + + Examples: + >>> with HDF5Writer('ark:out.h5', compress=True) as f: + ... f['key'] = array + """ + + def __init__(self, wspecifier, write_num_frames=None, compress=False): + spec_dict = parse_wspecifier(wspecifier) + self.filename = spec_dict["ark"] + + if compress: + self.kwargs = {"compression": "gzip"} + else: + self.kwargs = {} + self.writer = h5py.File(spec_dict["ark"], "w") + if "scp" in spec_dict: + self.writer_scp = open(spec_dict["scp"], "w", encoding="utf-8") + else: + self.writer_scp = None + if write_num_frames is not None: + self.writer_nframe = get_num_frames_writer(write_num_frames) + else: + self.writer_nframe = None + + def __setitem__(self, key, value): + self.writer.create_dataset(key, data=value, **self.kwargs) + + if self.writer_scp is not None: + self.writer_scp.write(f"{key} {self.filename}:{key}\n") + if self.writer_nframe is not None: + self.writer_nframe.write(f"{key} {len(value)}\n") + + +class SoundHDF5Writer(BaseWriter): + """SoundHDF5Writer + + Examples: + >>> fs = 16000 + >>> with SoundHDF5Writer('ark:out.h5') as f: + ... f['key'] = fs, array + """ + + def __init__(self, wspecifier, write_num_frames=None, pcm_format="wav"): + self.pcm_format = pcm_format + spec_dict = parse_wspecifier(wspecifier) + self.filename = spec_dict["ark"] + self.writer = SoundHDF5File( + spec_dict["ark"], "w", format=self.pcm_format) + if "scp" in spec_dict: + self.writer_scp = open(spec_dict["scp"], "w", encoding="utf-8") + else: + self.writer_scp = None + if write_num_frames is not None: + self.writer_nframe = get_num_frames_writer(write_num_frames) + else: + self.writer_nframe = None + + def __setitem__(self, key, value): + assert_scipy_wav_style(value) + # Change Tuple[int, ndarray] -> Tuple[ndarray, int] + # (scipy style -> soundfile style) + value = (value[1], value[0]) + self.writer.create_dataset(key, data=value) + + if self.writer_scp is not None: + self.writer_scp.write(f"{key} {self.filename}:{key}\n") + if self.writer_nframe is not None: + self.writer_nframe.write(f"{key} {len(value[0])}\n") + + +class SoundWriter(BaseWriter): + """SoundWriter + + Examples: + >>> fs = 16000 + >>> with SoundWriter('ark,scp:outdir,out.scp') as f: + ... f['key'] = fs, array + """ + + def __init__(self, wspecifier, write_num_frames=None, pcm_format="wav"): + self.pcm_format = pcm_format + spec_dict = parse_wspecifier(wspecifier) + # e.g. ark,scp:dirname,wav.scp + # -> The wave files are found in dirname/*.wav + self.dirname = spec_dict["ark"] + Path(self.dirname).mkdir(parents=True, exist_ok=True) + self.writer = None + + if "scp" in spec_dict: + self.writer_scp = open(spec_dict["scp"], "w", encoding="utf-8") + else: + self.writer_scp = None + if write_num_frames is not None: + self.writer_nframe = get_num_frames_writer(write_num_frames) + else: + self.writer_nframe = None + + def __setitem__(self, key, value): + assert_scipy_wav_style(value) + rate, signal = value + wavfile = Path(self.dirname) / (key + "." + self.pcm_format) + soundfile.write(wavfile, signal.astype(numpy.int16), rate) + + if self.writer_scp is not None: + self.writer_scp.write(f"{key} {wavfile}\n") + if self.writer_nframe is not None: + self.writer_nframe.write(f"{key} {len(signal)}\n") diff --git a/ernie-sat/paddlespeech/s2t/utils/ctc_utils.py b/ernie-sat/paddlespeech/s2t/utils/ctc_utils.py new file mode 100644 index 0000000..886b720 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/ctc_utils.py @@ -0,0 +1,211 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +from pathlib import Path +from typing import List + +import numpy as np +import paddle + +from paddlespeech.s2t.utils import text_grid +from paddlespeech.s2t.utils import utility +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = ["forced_align", "remove_duplicates_and_blank", "insert_blank"] + + +def remove_duplicates_and_blank(hyp: List[int], blank_id=0) -> List[int]: + """ctc alignment to ctc label ids. + + "abaa-acee-" -> "abaace" + + Args: + hyp (List[int]): hypotheses ids, (L) + blank_id (int, optional): blank id. Defaults to 0. + + Returns: + List[int]: remove dupicate ids, then remove blank id. + """ + new_hyp: List[int] = [] + cur = 0 + while cur < len(hyp): + # add non-blank into new_hyp + if hyp[cur] != blank_id: + new_hyp.append(hyp[cur]) + # skip repeat label + prev = cur + while cur < len(hyp) and hyp[cur] == hyp[prev]: + cur += 1 + return new_hyp + + +def insert_blank(label: np.ndarray, blank_id: int=0) -> np.ndarray: + """Insert blank token between every two label token. + + "abcdefg" -> "-a-b-c-d-e-f-g-" + + Args: + label ([np.ndarray]): label ids, List[int], (L). + blank_id (int, optional): blank id. Defaults to 0. + + Returns: + [np.ndarray]: (2L+1). + """ + label = np.expand_dims(label, 1) #[L, 1] + blanks = np.zeros((label.shape[0], 1), dtype=np.int64) + blank_id + label = np.concatenate([blanks, label], axis=1) #[L, 2] + label = label.reshape(-1) #[2L], -l-l-l + label = np.append(label, label[0]) #[2L + 1], -l-l-l- + return label + + +def forced_align(ctc_probs: paddle.Tensor, y: paddle.Tensor, + blank_id=0) -> List[int]: + """ctc forced alignment. + + https://distill.pub/2017/ctc/ + + Args: + ctc_probs (paddle.Tensor): hidden state sequence, 2d tensor (T, D) + y (paddle.Tensor): label id sequence tensor, 1d tensor (L) + blank_id (int): blank symbol index + Returns: + List[int]: best alignment result, (T). + """ + y_insert_blank = insert_blank(y, blank_id) #(2L+1) + + log_alpha = paddle.zeros( + (ctc_probs.shape[0], len(y_insert_blank))) #(T, 2L+1) + log_alpha = log_alpha - float('inf') # log of zero + + # TODO(Hui Zhang): zeros not support paddle.int16 + # self.__setitem_varbase__(item, value) When assign a value to a paddle.Tensor, the data type of the paddle.Tensor not support int16 + state_path = (paddle.zeros( + (ctc_probs.shape[0], len(y_insert_blank)), dtype=paddle.int32) - 1 + ) # state path, Tuple((T, 2L+1)) + + # init start state + # TODO(Hui Zhang): VarBase.__getitem__() not support np.int64 + log_alpha[0, 0] = ctc_probs[0][int(y_insert_blank[0])] # State-b, Sb + log_alpha[0, 1] = ctc_probs[0][int(y_insert_blank[1])] # State-nb, Snb + + for t in range(1, ctc_probs.shape[0]): # T + for s in range(len(y_insert_blank)): # 2L+1 + if y_insert_blank[s] == blank_id or s < 2 or y_insert_blank[ + s] == y_insert_blank[s - 2]: + candidates = paddle.to_tensor( + [log_alpha[t - 1, s], log_alpha[t - 1, s - 1]]) + prev_state = [s, s - 1] + else: + candidates = paddle.to_tensor([ + log_alpha[t - 1, s], + log_alpha[t - 1, s - 1], + log_alpha[t - 1, s - 2], + ]) + prev_state = [s, s - 1, s - 2] + # TODO(Hui Zhang): VarBase.__getitem__() not support np.int64 + log_alpha[t, s] = paddle.max(candidates) + ctc_probs[t][int( + y_insert_blank[s])] + state_path[t, s] = prev_state[paddle.argmax(candidates)] + # TODO(Hui Zhang): zeros not support paddle.int16 + # self.__setitem_varbase__(item, value) When assign a value to a paddle.Tensor, the data type of the paddle.Tensor not support int16 + state_seq = -1 * paddle.ones((ctc_probs.shape[0], 1), dtype=paddle.int32) + + candidates = paddle.to_tensor([ + log_alpha[-1, len(y_insert_blank) - 1], # Sb + log_alpha[-1, len(y_insert_blank) - 2] # Snb + ]) + prev_state = [len(y_insert_blank) - 1, len(y_insert_blank) - 2] + state_seq[-1] = prev_state[paddle.argmax(candidates)] + for t in range(ctc_probs.shape[0] - 2, -1, -1): + state_seq[t] = state_path[t + 1, state_seq[t + 1, 0]] + + output_alignment = [] + for t in range(0, ctc_probs.shape[0]): + output_alignment.append(y_insert_blank[state_seq[t, 0]]) + + return output_alignment + + +def ctc_align(config, model, dataloader, batch_size, stride_ms, token_dict, + result_file): + """ctc alignment. + + Args: + config (cfgNode): config + model (nn.Layer): U2 Model. + dataloader (io.DataLoader): dataloader. + batch_size (int): decoding batchsize. + stride_ms (int): audio feature stride in ms unit. + token_dict (List[str]): vocab list, e.g. ['blank', 'unk', 'a', 'b', '']. + result_file (str): alignment output file, e.g. /path/to/xxx.align. + """ + if batch_size > 1: + logger.fatal('alignment mode must be running with batch_size == 1') + sys.exit(1) + assert result_file and result_file.endswith('.align') + + model.eval() + # conv subsampling rate + subsample = utility.get_subsample(config) + logger.info(f"Align Total Examples: {len(dataloader.dataset)}") + + with open(result_file, 'w') as fout: + # one example in batch + for i, batch in enumerate(dataloader): + key, feat, feats_length, target, target_length = batch + + # 1. Encoder + encoder_out, encoder_mask = model._forward_encoder( + feat, feats_length) # (B, maxlen, encoder_dim) + maxlen = encoder_out.shape[1] + ctc_probs = model.ctc.log_softmax( + encoder_out) # (1, maxlen, vocab_size) + + # 2. alignment + ctc_probs = ctc_probs.squeeze(0) + target = target.squeeze(0) + alignment = forced_align(ctc_probs, target) + + logger.info(f"align ids: {key[0]} {alignment}") + fout.write('{} {}\n'.format(key[0], alignment)) + + # 3. gen praat + # segment alignment + align_segs = text_grid.segment_alignment(alignment) + logger.info(f"align tokens: {key[0]}, {align_segs}") + + # IntervalTier, List["start end token\n"] + tierformat = text_grid.align_to_tierformat(align_segs, subsample, + token_dict) + + # write tier + align_output_path = Path(result_file).parent / "align" + align_output_path.mkdir(parents=True, exist_ok=True) + tier_path = align_output_path / (key[0] + ".tier") + with tier_path.open('w') as f: + f.writelines(tierformat) + + # write textgrid + textgrid_path = align_output_path / (key[0] + ".TextGrid") + second_per_frame = 1. / (1000. / + stride_ms) # 25ms window, 10ms stride + second_per_example = ( + len(alignment) + 1) * subsample * second_per_frame + text_grid.generate_textgrid( + maxtime=second_per_example, + intervals=tierformat, + output=str(textgrid_path)) diff --git a/ernie-sat/paddlespeech/s2t/utils/dynamic_import.py b/ernie-sat/paddlespeech/s2t/utils/dynamic_import.py new file mode 100644 index 0000000..bd738ed --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/dynamic_import.py @@ -0,0 +1,69 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import importlib +import inspect +from typing import Any +from typing import Dict +from typing import List +from typing import Text + +from paddlespeech.s2t.utils.log import Log +from paddlespeech.s2t.utils.tensor_utils import has_tensor + +logger = Log(__name__).getlog() + +__all__ = ["dynamic_import", "instance_class"] + + +def dynamic_import(import_path, alias=dict()): + """dynamic import module and class + + :param str import_path: syntax 'module_name:class_name' + e.g., 'paddlespeech.s2t.models.u2:U2Model' + :param dict alias: shortcut for registered class + :return: imported class + """ + if import_path not in alias and ":" not in import_path: + raise ValueError( + "import_path should be one of {} or " + 'include ":", e.g. "paddlespeech.s2t.models.u2:U2Model" : ' + "{}".format(set(alias), import_path)) + if ":" not in import_path: + import_path = alias[import_path] + + module_name, objname = import_path.split(":") + m = importlib.import_module(module_name) + return getattr(m, objname) + + +def filter_valid_args(args: Dict[Text, Any], valid_keys: List[Text]): + # filter by `valid_keys` and filter `val` is not None + new_args = { + key: val + for key, val in args.items() if (key in valid_keys and val is not None) + } + return new_args + + +def filter_out_tensor(args: Dict[Text, Any]): + return {key: val for key, val in args.items() if not has_tensor(val)} + + +def instance_class(module_class, args: Dict[Text, Any]): + valid_keys = inspect.signature(module_class).parameters.keys() + new_args = filter_valid_args(args, valid_keys) + logger.info( + f"Instance: {module_class.__name__} {filter_out_tensor(new_args)}.") + return module_class(**new_args) diff --git a/ernie-sat/paddlespeech/s2t/utils/dynamic_pip_install.py b/ernie-sat/paddlespeech/s2t/utils/dynamic_pip_install.py new file mode 100644 index 0000000..39e9c35 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/dynamic_pip_install.py @@ -0,0 +1,22 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pip + + +def install(package_name): + if int(pip.__version__.split('.')[0]) > 9: + from pip._internal import main + else: + from pip import main + main(['install', package_name]) diff --git a/ernie-sat/paddlespeech/s2t/utils/error_rate.py b/ernie-sat/paddlespeech/s2t/utils/error_rate.py new file mode 100644 index 0000000..548376a --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/error_rate.py @@ -0,0 +1,364 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module provides functions to calculate error rate in different level. +e.g. wer for word-level, cer for char-level. +""" +from itertools import groupby + +import editdistance +import numpy as np + +__all__ = ['word_errors', 'char_errors', 'wer', 'cer', "ErrorCalculator"] + + +def _levenshtein_distance(ref, hyp): + """Levenshtein distance is a string metric for measuring the difference + between two sequences. Informally, the levenshtein disctance is defined as + the minimum number of single-character edits (substitutions, insertions or + deletions) required to change one word into the other. We can naturally + extend the edits to word level when calculate levenshtein disctance for + two sentences. + """ + m = len(ref) + n = len(hyp) + + # special case + if ref == hyp: + return 0 + if m == 0: + return n + if n == 0: + return m + + if m < n: + ref, hyp = hyp, ref + m, n = n, m + + # use O(min(m, n)) space + distance = np.zeros((2, n + 1), dtype=np.int32) + + # initialize distance matrix + for j in range(n + 1): + distance[0][j] = j + + # calculate levenshtein distance + for i in range(1, m + 1): + prev_row_idx = (i - 1) % 2 + cur_row_idx = i % 2 + distance[cur_row_idx][0] = i + for j in range(1, n + 1): + if ref[i - 1] == hyp[j - 1]: + distance[cur_row_idx][j] = distance[prev_row_idx][j - 1] + else: + s_num = distance[prev_row_idx][j - 1] + 1 + i_num = distance[cur_row_idx][j - 1] + 1 + d_num = distance[prev_row_idx][j] + 1 + distance[cur_row_idx][j] = min(s_num, i_num, d_num) + + return distance[m % 2][n] + + +def word_errors(reference, hypothesis, ignore_case=False, delimiter=' '): + """Compute the levenshtein distance between reference sequence and + hypothesis sequence in word-level. + + :param reference: The reference sentence. + :type reference: str + :param hypothesis: The hypothesis sentence. + :type hypothesis: str + :param ignore_case: Whether case-sensitive or not. + :type ignore_case: bool + :param delimiter: Delimiter of input sentences. + :type delimiter: char + :return: Levenshtein distance and word number of reference sentence. + :rtype: list + """ + if ignore_case: + reference = reference.lower() + hypothesis = hypothesis.lower() + + ref_words = list(filter(None, reference.split(delimiter))) + hyp_words = list(filter(None, hypothesis.split(delimiter))) + + edit_distance = _levenshtein_distance(ref_words, hyp_words) + # `editdistance.eavl precision` less than `_levenshtein_distance` + # edit_distance = editdistance.eval(ref_words, hyp_words) + return float(edit_distance), len(ref_words) + + +def char_errors(reference, hypothesis, ignore_case=False, remove_space=False): + """Compute the levenshtein distance between reference sequence and + hypothesis sequence in char-level. + + :param reference: The reference sentence. + :type reference: str + :param hypothesis: The hypothesis sentence. + :type hypothesis: str + :param ignore_case: Whether case-sensitive or not. + :type ignore_case: bool + :param remove_space: Whether remove internal space characters + :type remove_space: bool + :return: Levenshtein distance and length of reference sentence. + :rtype: list + """ + if ignore_case: + reference = reference.lower() + hypothesis = hypothesis.lower() + + join_char = ' ' + if remove_space: + join_char = '' + + reference = join_char.join(list(filter(None, reference.split(' ')))) + hypothesis = join_char.join(list(filter(None, hypothesis.split(' ')))) + + edit_distance = _levenshtein_distance(reference, hypothesis) + # `editdistance.eavl precision` less than `_levenshtein_distance` + # edit_distance = editdistance.eval(reference, hypothesis) + return float(edit_distance), len(reference) + + +def wer(reference, hypothesis, ignore_case=False, delimiter=' '): + """Calculate word error rate (WER). WER compares reference text and + hypothesis text in word-level. WER is defined as: + + .. math:: + WER = (Sw + Dw + Iw) / Nw + + where + + .. code-block:: text + + Sw is the number of words subsituted, + Dw is the number of words deleted, + Iw is the number of words inserted, + Nw is the number of words in the reference + + We can use levenshtein distance to calculate WER. Please draw an attention + that empty items will be removed when splitting sentences by delimiter. + + :param reference: The reference sentence. + :type reference: str + :param hypothesis: The hypothesis sentence. + :type hypothesis: str + :param ignore_case: Whether case-sensitive or not. + :type ignore_case: bool + :param delimiter: Delimiter of input sentences. + :type delimiter: char + :return: Word error rate. + :rtype: float + :raises ValueError: If word number of reference is zero. + """ + edit_distance, ref_len = word_errors(reference, hypothesis, ignore_case, + delimiter) + + if ref_len == 0: + raise ValueError("Reference's word number should be greater than 0.") + + wer = float(edit_distance) / ref_len + return wer + + +def cer(reference, hypothesis, ignore_case=False, remove_space=False): + """Calculate charactor error rate (CER). CER compares reference text and + hypothesis text in char-level. CER is defined as: + + .. math:: + CER = (Sc + Dc + Ic) / Nc + + where + + .. code-block:: text + + Sc is the number of characters substituted, + Dc is the number of characters deleted, + Ic is the number of characters inserted + Nc is the number of characters in the reference + + We can use levenshtein distance to calculate CER. Chinese input should be + encoded to unicode. Please draw an attention that the leading and tailing + space characters will be truncated and multiple consecutive space + characters in a sentence will be replaced by one space character. + + :param reference: The reference sentence. + :type reference: str + :param hypothesis: The hypothesis sentence. + :type hypothesis: str + :param ignore_case: Whether case-sensitive or not. + :type ignore_case: bool + :param remove_space: Whether remove internal space characters + :type remove_space: bool + :return: Character error rate. + :rtype: float + :raises ValueError: If the reference length is zero. + """ + edit_distance, ref_len = char_errors(reference, hypothesis, ignore_case, + remove_space) + + if ref_len == 0: + raise ValueError("Length of reference should be greater than 0.") + + cer = float(edit_distance) / ref_len + return cer + + +class ErrorCalculator(): + """Calculate CER and WER for E2E_ASR and CTC models during training. + + :param y_hats: numpy array with predicted text + :param y_pads: numpy array with true (target) text + :param char_list: List[str] + :param sym_space: + :param sym_blank: + :return: + """ + + def __init__(self, + char_list, + sym_space, + sym_blank, + report_cer=False, + report_wer=False): + """Construct an ErrorCalculator object.""" + super().__init__() + + self.report_cer = report_cer + self.report_wer = report_wer + + self.char_list = char_list + self.space = sym_space + self.blank = sym_blank + self.idx_blank = self.char_list.index(self.blank) + if self.space in self.char_list: + self.idx_space = self.char_list.index(self.space) + else: + self.idx_space = None + + def __call__(self, ys_hat, ys_pad, is_ctc=False): + """Calculate sentence-level WER/CER score. + + :param paddle.Tensor ys_hat: prediction (batch, seqlen) + :param paddle.Tensor ys_pad: reference (batch, seqlen) + :param bool is_ctc: calculate CER score for CTC + :return: sentence-level WER score + :rtype float + :return: sentence-level CER score + :rtype float + """ + cer, wer = None, None + if is_ctc: + return self.calculate_cer_ctc(ys_hat, ys_pad) + elif not self.report_cer and not self.report_wer: + return cer, wer + + seqs_hat, seqs_true = self.convert_to_char(ys_hat, ys_pad) + if self.report_cer: + cer = self.calculate_cer(seqs_hat, seqs_true) + + if self.report_wer: + wer = self.calculate_wer(seqs_hat, seqs_true) + return cer, wer + + def calculate_cer_ctc(self, ys_hat, ys_pad): + """Calculate sentence-level CER score for CTC. + + :param paddle.Tensor ys_hat: prediction (batch, seqlen) + :param paddle.Tensor ys_pad: reference (batch, seqlen) + :return: average sentence-level CER score + :rtype float + """ + cers, char_ref_lens = [], [] + for i, y in enumerate(ys_hat): + y_hat = [x[0] for x in groupby(y)] + y_true = ys_pad[i] + seq_hat, seq_true = [], [] + for idx in y_hat: + idx = int(idx) + if idx != -1 and idx != self.idx_blank and idx != self.idx_space: + seq_hat.append(self.char_list[int(idx)]) + + for idx in y_true: + idx = int(idx) + if idx != -1 and idx != self.idx_blank and idx != self.idx_space: + seq_true.append(self.char_list[int(idx)]) + + hyp_chars = "".join(seq_hat) + ref_chars = "".join(seq_true) + if len(ref_chars) > 0: + cers.append(editdistance.eval(hyp_chars, ref_chars)) + char_ref_lens.append(len(ref_chars)) + + cer_ctc = float(sum(cers)) / sum(char_ref_lens) if cers else None + return cer_ctc + + def convert_to_char(self, ys_hat, ys_pad): + """Convert index to character. + + :param paddle.Tensor seqs_hat: prediction (batch, seqlen) + :param paddle.Tensor seqs_true: reference (batch, seqlen) + :return: token list of prediction + :rtype list + :return: token list of reference + :rtype list + """ + seqs_hat, seqs_true = [], [] + for i, y_hat in enumerate(ys_hat): + y_true = ys_pad[i] + eos_true = np.where(y_true == -1)[0] + ymax = eos_true[0] if len(eos_true) > 0 else len(y_true) + # NOTE: padding index (-1) in y_true is used to pad y_hat + seq_hat = [self.char_list[int(idx)] for idx in y_hat[:ymax]] + seq_true = [ + self.char_list[int(idx)] for idx in y_true if int(idx) != -1 + ] + seq_hat_text = "".join(seq_hat).replace(self.space, " ") + seq_hat_text = seq_hat_text.replace(self.blank, "") + seq_true_text = "".join(seq_true).replace(self.space, " ") + seqs_hat.append(seq_hat_text) + seqs_true.append(seq_true_text) + return seqs_hat, seqs_true + + def calculate_cer(self, seqs_hat, seqs_true): + """Calculate sentence-level CER score. + + :param list seqs_hat: prediction + :param list seqs_true: reference + :return: average sentence-level CER score + :rtype float + """ + char_eds, char_ref_lens = [], [] + for i, seq_hat_text in enumerate(seqs_hat): + seq_true_text = seqs_true[i] + hyp_chars = seq_hat_text.replace(" ", "") + ref_chars = seq_true_text.replace(" ", "") + char_eds.append(editdistance.eval(hyp_chars, ref_chars)) + char_ref_lens.append(len(ref_chars)) + return float(sum(char_eds)) / sum(char_ref_lens) + + def calculate_wer(self, seqs_hat, seqs_true): + """Calculate sentence-level WER score. + + :param list seqs_hat: prediction + :param list seqs_true: reference + :return: average sentence-level WER score + :rtype float + """ + word_eds, word_ref_lens = [], [] + for i, seq_hat_text in enumerate(seqs_hat): + seq_true_text = seqs_true[i] + hyp_words = seq_hat_text.split() + ref_words = seq_true_text.split() + word_eds.append(editdistance.eval(hyp_words, ref_words)) + word_ref_lens.append(len(ref_words)) + return float(sum(word_eds)) / sum(word_ref_lens) diff --git a/ernie-sat/paddlespeech/s2t/utils/layer_tools.py b/ernie-sat/paddlespeech/s2t/utils/layer_tools.py new file mode 100644 index 0000000..fb076c0 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/layer_tools.py @@ -0,0 +1,88 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from paddle import nn + +__all__ = [ + "summary", "gradient_norm", "freeze", "unfreeze", "print_grads", + "print_params" +] + + +def summary(layer: nn.Layer, print_func=print): + if print_func is None: + return + num_params = num_elements = 0 + for name, param in layer.state_dict().items(): + if print_func: + print_func( + "{} | {} | {}".format(name, param.shape, np.prod(param.shape))) + num_elements += np.prod(param.shape) + num_params += 1 + if print_func: + num_elements = num_elements / 1024**2 + print_func( + f"Total parameters: {num_params}, {num_elements:.2f}M elements.") + + +def print_grads(model, print_func=print): + if print_func is None: + return + for n, p in model.named_parameters(): + msg = f"param grad: {n}: shape: {p.shape} grad: {p.grad}" + print_func(msg) + + +def print_params(model, print_func=print): + if print_func is None: + return + total = 0.0 + num_params = 0.0 + for n, p in model.named_parameters(): + msg = f"{n} | {p.shape} | {np.prod(p.shape)} | {not p.stop_gradient}" + total += np.prod(p.shape) + num_params += 1 + if print_func: + print_func(msg) + if print_func: + total = total / 1024**2 + print_func(f"Total parameters: {num_params}, {total:.2f}M elements.") + + +def gradient_norm(layer: nn.Layer): + grad_norm_dict = {} + for name, param in layer.state_dict().items(): + if param.trainable: + grad = param.gradient() # return numpy.ndarray + grad_norm_dict[name] = np.linalg.norm(grad) / grad.size + return grad_norm_dict + + +def recursively_remove_weight_norm(layer: nn.Layer): + for layer in layer.sublayers(): + try: + nn.utils.remove_weight_norm(layer) + except ValueError as e: + # ther is not weight norm hoom in this layer + pass + + +def freeze(layer: nn.Layer): + for param in layer.parameters(): + param.trainable = False + + +def unfreeze(layer: nn.Layer): + for param in layer.parameters(): + param.trainable = True diff --git a/ernie-sat/paddlespeech/s2t/utils/log.py b/ernie-sat/paddlespeech/s2t/utils/log.py new file mode 100644 index 0000000..4f51b7f --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/log.py @@ -0,0 +1,162 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import getpass +import inspect +import os +import socket +import sys + +from loguru import logger +from paddle import inference + + +def find_log_dir(log_dir=None): + """Returns the most suitable directory to put log files into. + Args: + log_dir: str|None, if specified, the logfile(s) will be created in that + directory. Otherwise if the --log_dir command-line flag is provided, + the logfile will be created in that directory. Otherwise the logfile + will be created in a standard location. + Raises: + FileNotFoundError: raised when it cannot find a log directory. + """ + # Get a list of possible log dirs (will try to use them in order). + if log_dir: + # log_dir was explicitly specified as an arg, so use it and it alone. + dirs = [log_dir] + else: + dirs = ['/tmp/', './'] + + # Find the first usable log dir. + for d in dirs: + if os.path.isdir(d) and os.access(d, os.W_OK): + return d + raise FileNotFoundError( + "Can't find a writable directory for logs, tried %s" % dirs) + + +def find_log_dir_and_names(program_name=None, log_dir=None): + """Computes the directory and filename prefix for log file. + Args: + program_name: str|None, the filename part of the path to the program that + is running without its extension. e.g: if your program is called + 'usr/bin/foobar.py' this method should probably be called with + program_name='foobar' However, this is just a convention, you can + pass in any string you want, and it will be used as part of the + log filename. If you don't pass in anything, the default behavior + is as described in the example. In python standard logging mode, + the program_name will be prepended with py_ if it is the program_name + argument is omitted. + log_dir: str|None, the desired log directory. + Returns: + (log_dir, file_prefix, symlink_prefix) + Raises: + FileNotFoundError: raised in Python 3 when it cannot find a log directory. + OSError: raised in Python 2 when it cannot find a log directory. + """ + if not program_name: + # Strip the extension (foobar.par becomes foobar, and + # fubar.py becomes fubar). We do this so that the log + # file names are similar to C++ log file names. + program_name = os.path.splitext(os.path.basename(sys.argv[0]))[0] + + # Prepend py_ to files so that python code gets a unique file, and + # so that C++ libraries do not try to write to the same log files as us. + program_name = 'py_%s' % program_name + + actual_log_dir = find_log_dir(log_dir=log_dir) + + try: + username = getpass.getuser() + except KeyError: + # This can happen, e.g. when running under docker w/o passwd file. + if hasattr(os, 'getuid'): + # Windows doesn't have os.getuid + username = str(os.getuid()) + else: + username = 'unknown' + hostname = socket.gethostname() + file_prefix = '%s.%s.%s.log' % (program_name, hostname, username) + + return actual_log_dir, file_prefix, program_name + + +class Log(): + """Default Logger for all.""" + logger.remove() + + _call_from_cli = False + _frame = inspect.currentframe() + while _frame: + if 'paddlespeech/cli/__init__.py' in _frame.f_code.co_filename or 'paddlespeech/t2s' in _frame.f_code.co_filename: + _call_from_cli = True + break + _frame = _frame.f_back + + if _call_from_cli: + logger.add( + sys.stdout, + level='ERROR', + enqueue=True, + filter=lambda record: record['level'].no >= 20) + else: + logger.add( + sys.stdout, + level='INFO', + enqueue=True, + filter=lambda record: record['level'].no >= 20) + _, file_prefix, _ = find_log_dir_and_names() + sink_prefix = os.path.join("exp/log", file_prefix) + sink_path = sink_prefix[:-3] + "{time}.log" + logger.add(sink_path, level='DEBUG', enqueue=True, rotation="500 MB") + + def __init__(self, name=None): + pass + + def getlog(self): + return logger + + +class Autolog: + """Just used by fullchain project""" + + def __init__(self, + batch_size, + model_name="DeepSpeech", + model_precision="fp32"): + import auto_log + pid = os.getpid() + if os.environ.get('CUDA_VISIBLE_DEVICES', None): + gpu_id = int(os.environ['CUDA_VISIBLE_DEVICES'].split(',')[0]) + infer_config = inference.Config() + infer_config.enable_use_gpu(100, gpu_id) + else: + gpu_id = None + infer_config = inference.Config() + + self.autolog = auto_log.AutoLogger( + model_name=model_name, + model_precision=model_precision, + batch_size=batch_size, + data_shape="dynamic", + save_path="./output/auto_log.lpg", + inference_config=infer_config, + pids=pid, + process_name=None, + gpu_ids=gpu_id, + time_keys=['preprocess_time', 'inference_time', 'postprocess_time'], + warmup=0) + + def getlog(self): + return self.autolog diff --git a/ernie-sat/paddlespeech/s2t/utils/mp_tools.py b/ernie-sat/paddlespeech/s2t/utils/mp_tools.py new file mode 100644 index 0000000..d3e25aa --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/mp_tools.py @@ -0,0 +1,30 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from functools import wraps + +from paddle import distributed as dist + +__all__ = ["rank_zero_only"] + + +def rank_zero_only(func): + @wraps(func) + def wrapper(*args, **kwargs): + rank = dist.get_rank() + if rank != 0: + return + result = func(*args, **kwargs) + return result + + return wrapper diff --git a/ernie-sat/paddlespeech/s2t/utils/profiler.py b/ernie-sat/paddlespeech/s2t/utils/profiler.py new file mode 100644 index 0000000..3592157 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/profiler.py @@ -0,0 +1,119 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +import paddle + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +# A global variable to record the number of calling times for profiler +# functions. It is used to specify the tracing range of training steps. +_profiler_step_id = 0 + +# A global variable to avoid parsing from string every time. +_profiler_options = None + + +class ProfilerOptions(object): + ''' + Use a string to initialize a ProfilerOptions. + The string should be in the format: "key1=value1;key2=value;key3=value3". + For example: + "profile_path=model.profile" + "batch_range=[50, 60]; profile_path=model.profile" + "batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile" + ProfilerOptions supports following key-value pair: + batch_range - a integer list, e.g. [100, 110]. + state - a string, the optional values are 'CPU', 'GPU' or 'All'. + sorted_key - a string, the optional values are 'calls', 'total', + 'max', 'min' or 'ave. + tracer_option - a string, the optional values are 'Default', 'OpDetail', + 'AllOpDetail'. + profile_path - a string, the path to save the serialized profile data, + which can be used to generate a timeline. + exit_on_finished - a boolean. + ''' + + def __init__(self, options_str): + assert isinstance(options_str, str) + + self._options = { + 'batch_range': [10, 20], + 'state': 'All', + 'sorted_key': 'total', + 'tracer_option': 'Default', + 'profile_path': '/tmp/profile', + 'exit_on_finished': True + } + self._parse_from_string(options_str) + + def _parse_from_string(self, options_str): + if not options_str: + return + + for kv in options_str.replace(' ', '').split(';'): + key, value = kv.split('=') + if key == 'batch_range': + value_list = value.replace('[', '').replace(']', '').split(',') + value_list = list(map(int, value_list)) + if len(value_list) >= 2 and value_list[0] >= 0 and value_list[ + 1] > value_list[0]: + self._options[key] = value_list + elif key == 'exit_on_finished': + self._options[key] = value.lower() in ("yes", "true", "t", "1") + elif key in [ + 'state', 'sorted_key', 'tracer_option', 'profile_path' + ]: + self._options[key] = value + + def __getitem__(self, name): + if self._options.get(name, None) is None: + raise ValueError( + "ProfilerOptions does not have an option named %s." % name) + return self._options[name] + + +def add_profiler_step(options_str=None): + ''' + Enable the operator-level timing using PaddlePaddle's profiler. + The profiler uses a independent variable to count the profiler steps. + One call of this function is treated as a profiler step. + + Args: + profiler_options - a string to initialize the ProfilerOptions. + Default is None, and the profiler is disabled. + ''' + if options_str is None: + return + + global _profiler_step_id + global _profiler_options + + if _profiler_options is None: + _profiler_options = ProfilerOptions(options_str) + logger.info(f"Profiler: {options_str}") + logger.info(f"Profiler: {_profiler_options._options}") + + if _profiler_step_id == _profiler_options['batch_range'][0]: + paddle.utils.profiler.start_profiler(_profiler_options['state'], + _profiler_options['tracer_option']) + elif _profiler_step_id == _profiler_options['batch_range'][1]: + paddle.utils.profiler.stop_profiler(_profiler_options['sorted_key'], + _profiler_options['profile_path']) + if _profiler_options['exit_on_finished']: + sys.exit(0) + + _profiler_step_id += 1 diff --git a/ernie-sat/paddlespeech/s2t/utils/socket_server.py b/ernie-sat/paddlespeech/s2t/utils/socket_server.py new file mode 100644 index 0000000..691ea96 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/socket_server.py @@ -0,0 +1,113 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import random +import socket +import socketserver +import struct +import time +import wave +from time import gmtime +from time import strftime + +import jsonlines + +__all__ = ["socket_send", "warm_up_test", "AsrTCPServer", "AsrRequestHandler"] + + +def socket_send(server_ip: str, server_port: str, data: bytes): + # Connect to server and send data + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((server_ip, server_port)) + sent = data + sock.sendall(struct.pack('>i', len(sent)) + sent) + print('Speech[length=%d] Sent.' % len(sent)) + # Receive data from the server and shut down + received = sock.recv(1024) + print("Recognition Results: {}".format(received.decode('utf8'))) + sock.close() + + +def warm_up_test(audio_process_handler, + manifest_path, + num_test_cases, + random_seed=0): + """Warming-up test.""" + with jsonlines.open(manifest_path) as reader: + manifest = list(reader) + rng = random.Random(random_seed) + samples = rng.sample(manifest, num_test_cases) + for idx, sample in enumerate(samples): + print("Warm-up Test Case %d: %s" % (idx, sample['feat'])) + start_time = time.time() + transcript = audio_process_handler(sample['feat']) + finish_time = time.time() + print("Response Time: %f, Transcript: %s" % + (finish_time - start_time, transcript)) + + +class AsrTCPServer(socketserver.TCPServer): + """The ASR TCP Server.""" + + def __init__(self, + server_address, + RequestHandlerClass, + speech_save_dir, + audio_process_handler, + bind_and_activate=True): + self.speech_save_dir = speech_save_dir + self.audio_process_handler = audio_process_handler + socketserver.TCPServer.__init__( + self, server_address, RequestHandlerClass, bind_and_activate=True) + + +class AsrRequestHandler(socketserver.BaseRequestHandler): + """The ASR request handler.""" + + def handle(self): + # receive data through TCP socket + chunk = self.request.recv(1024) + target_len = struct.unpack('>i', chunk[:4])[0] + data = chunk[4:] + while len(data) < target_len: + chunk = self.request.recv(1024) + data += chunk + # write to file + filename = self._write_to_file(data) + + print("Received utterance[length=%d] from %s, saved to %s." % + (len(data), self.client_address[0], filename)) + start_time = time.time() + transcript = self.server.audio_process_handler(filename) + finish_time = time.time() + print("Response Time: %f, Transcript: %s" % + (finish_time - start_time, transcript)) + self.request.sendall(transcript.encode('utf-8')) + + def _write_to_file(self, data): + # prepare save dir and filename + if not os.path.exists(self.server.speech_save_dir): + os.mkdir(self.server.speech_save_dir) + timestamp = strftime("%Y%m%d%H%M%S", gmtime()) + out_filename = os.path.join( + self.server.speech_save_dir, + timestamp + "_" + self.client_address[0] + ".wav") + # write to wav file + file = wave.open(out_filename, 'wb') + file.setnchannels(1) + file.setsampwidth(2) + file.setframerate(16000) + file.writeframes(data) + file.close() + return out_filename diff --git a/ernie-sat/paddlespeech/s2t/utils/spec_augment.py b/ernie-sat/paddlespeech/s2t/utils/spec_augment.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/spec_augment.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/s2t/utils/tensor_utils.py b/ernie-sat/paddlespeech/s2t/utils/tensor_utils.py new file mode 100644 index 0000000..0dbaa0b --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/tensor_utils.py @@ -0,0 +1,195 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Unility functions for Transformer.""" +from typing import List +from typing import Tuple + +import paddle + +from paddlespeech.s2t.utils.log import Log + +__all__ = ["pad_sequence", "add_sos_eos", "th_accuracy", "has_tensor"] + +logger = Log(__name__).getlog() + + +def has_tensor(val): + if isinstance(val, (list, tuple)): + for item in val: + if has_tensor(item): + return True + elif isinstance(val, dict): + for k, v in val.items(): + print(k) + if has_tensor(v): + return True + else: + return paddle.is_tensor(val) + + +def pad_sequence(sequences: List[paddle.Tensor], + batch_first: bool=False, + padding_value: float=0.0) -> paddle.Tensor: + r"""Pad a list of variable length Tensors with ``padding_value`` + + ``pad_sequence`` stacks a list of Tensors along a new dimension, + and pads them to equal length. For example, if the input is list of + sequences with size ``L x *`` and if batch_first is False, and ``T x B x *`` + otherwise. + + `B` is batch size. It is equal to the number of elements in ``sequences``. + `T` is length of the longest sequence. + `L` is length of the sequence. + `*` is any number of trailing dimensions, including none. + + Example: + >>> from paddle.nn.utils.rnn import pad_sequence + >>> a = paddle.ones(25, 300) + >>> b = paddle.ones(22, 300) + >>> c = paddle.ones(15, 300) + >>> pad_sequence([a, b, c]).size() + paddle.Tensor([25, 3, 300]) + + Note: + This function returns a Tensor of size ``T x B x *`` or ``B x T x *`` + where `T` is the length of the longest sequence. This function assumes + trailing dimensions and type of all the Tensors in sequences are same. + + Args: + sequences (list[Tensor]): list of variable length sequences. + batch_first (bool, optional): output will be in ``B x T x *`` if True, or in + ``T x B x *`` otherwise + padding_value (float, optional): value for padded elements. Default: 0. + + Returns: + Tensor of size ``T x B x *`` if :attr:`batch_first` is ``False``. + Tensor of size ``B x T x *`` otherwise + """ + + # assuming trailing dimensions and type of all the Tensors + # in sequences are same and fetching those from sequences[0] + max_size = sequences[0].size() + # (TODO Hui Zhang): slice not supprot `end==start` + # trailing_dims = max_size[1:] + trailing_dims = max_size[1:] if max_size.ndim >= 2 else () + max_len = max([s.shape[0] for s in sequences]) + if batch_first: + out_dims = (len(sequences), max_len) + trailing_dims + else: + out_dims = (max_len, len(sequences)) + trailing_dims + + out_tensor = sequences[0].new_full(out_dims, padding_value) + for i, tensor in enumerate(sequences): + length = tensor.shape[0] + # use index notation to prevent duplicate references to the tensor + logger.info( + f"length {length}, out_tensor {out_tensor.shape}, tensor {tensor.shape}" + ) + if batch_first: + # TODO (Hui Zhang): set_value op not supprot `end==start` + # TODO (Hui Zhang): set_value op not support int16 + # TODO (Hui Zhang): set_varbase 2 rank not support [0,0,...] + # out_tensor[i, :length, ...] = tensor + if length != 0: + out_tensor[i, :length] = tensor + else: + out_tensor[i, length] = tensor + else: + # TODO (Hui Zhang): set_value op not supprot `end==start` + # out_tensor[:length, i, ...] = tensor + if length != 0: + out_tensor[:length, i] = tensor + else: + out_tensor[length, i] = tensor + + return out_tensor + + +def add_sos_eos(ys_pad: paddle.Tensor, sos: int, eos: int, + ignore_id: int) -> Tuple[paddle.Tensor, paddle.Tensor]: + """Add and labels. + Args: + ys_pad (paddle.Tensor): batch of padded target sequences (B, Lmax) + sos (int): index of + eos (int): index of + ignore_id (int): index of padding + Returns: + ys_in (paddle.Tensor) : (B, Lmax + 1) + ys_out (paddle.Tensor) : (B, Lmax + 1) + Examples: + >>> sos_id = 10 + >>> eos_id = 11 + >>> ignore_id = -1 + >>> ys_pad + tensor([[ 1, 2, 3, 4, 5], + [ 4, 5, 6, -1, -1], + [ 7, 8, 9, -1, -1]], dtype=paddle.int32) + >>> ys_in,ys_out=add_sos_eos(ys_pad, sos_id , eos_id, ignore_id) + >>> ys_in + tensor([[10, 1, 2, 3, 4, 5], + [10, 4, 5, 6, 11, 11], + [10, 7, 8, 9, 11, 11]]) + >>> ys_out + tensor([[ 1, 2, 3, 4, 5, 11], + [ 4, 5, 6, 11, -1, -1], + [ 7, 8, 9, 11, -1, -1]]) + """ + # TODO(Hui Zhang): using comment code, + #_sos = paddle.to_tensor( + # [sos], dtype=paddle.long, stop_gradient=True, place=ys_pad.place) + #_eos = paddle.to_tensor( + # [eos], dtype=paddle.long, stop_gradient=True, place=ys_pad.place) + #ys = [y[y != ignore_id] for y in ys_pad] # parse padded ys + #ys_in = [paddle.cat([_sos, y], dim=0) for y in ys] + #ys_out = [paddle.cat([y, _eos], dim=0) for y in ys] + #return pad_sequence(ys_in, padding_value=eos), pad_sequence(ys_out, padding_value=ignore_id) + B = ys_pad.shape[0] + _sos = paddle.ones([B, 1], dtype=ys_pad.dtype) * sos + _eos = paddle.ones([B, 1], dtype=ys_pad.dtype) * eos + ys_in = paddle.cat([_sos, ys_pad], dim=1) + mask_pad = (ys_in == ignore_id) + ys_in = ys_in.masked_fill(mask_pad, eos) + + ys_out = paddle.cat([ys_pad, _eos], dim=1) + ys_out = ys_out.masked_fill(mask_pad, eos) + mask_eos = (ys_out == ignore_id) + ys_out = ys_out.masked_fill(mask_eos, eos) + ys_out = ys_out.masked_fill(mask_pad, ignore_id) + return ys_in, ys_out + + +def th_accuracy(pad_outputs: paddle.Tensor, + pad_targets: paddle.Tensor, + ignore_label: int) -> float: + """Calculate accuracy. + Args: + pad_outputs (Tensor): Prediction tensors (B * Lmax, D). + pad_targets (LongTensor): Target label tensors (B, Lmax, D). + ignore_label (int): Ignore label id. + Returns: + float: Accuracy value (0.0 - 1.0). + """ + pad_pred = pad_outputs.view(pad_targets.shape[0], pad_targets.shape[1], + pad_outputs.shape[1]).argmax(2) + mask = pad_targets != ignore_label + #TODO(Hui Zhang): sum not support bool type + # numerator = paddle.sum( + # pad_pred.masked_select(mask) == pad_targets.masked_select(mask)) + numerator = ( + pad_pred.masked_select(mask) == pad_targets.masked_select(mask)) + numerator = paddle.sum(numerator.type_as(pad_targets)) + #TODO(Hui Zhang): sum not support bool type + # denominator = paddle.sum(mask) + denominator = paddle.sum(mask.type_as(pad_targets)) + return float(numerator) / float(denominator) diff --git a/ernie-sat/paddlespeech/s2t/utils/text_grid.py b/ernie-sat/paddlespeech/s2t/utils/text_grid.py new file mode 100644 index 0000000..cbd9856 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/text_grid.py @@ -0,0 +1,128 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from wenet(https://github.com/wenet-e2e/wenet) +from typing import Dict +from typing import List +from typing import Text + +import textgrid + + +def segment_alignment(alignment: List[int], blank_id=0) -> List[List[int]]: + """segment ctc alignment ids by continuous blank and repeat label. + + Args: + alignment (List[int]): ctc alignment id sequence. + e.g. [0, 0, 0, 1, 1, 1, 2, 0, 0, 3] + blank_id (int, optional): blank id. Defaults to 0. + + Returns: + List[List[int]]: token align, segment aligment id sequence. + e.g. [[0, 0, 0, 1, 1, 1], [2], [0, 0, 3]] + """ + # convert alignment to a praat format, which is a doing phonetics + # by computer and helps analyzing alignment + align_segs = [] + # get frames level duration for each token + start = 0 + end = 0 + while end < len(alignment): + while end < len(alignment) and alignment[end] == blank_id: # blank + end += 1 + if end == len(alignment): + align_segs[-1].extend(alignment[start:]) + break + end += 1 + while end < len(alignment) and alignment[end - 1] == alignment[ + end]: # repeat label + end += 1 + align_segs.append(alignment[start:end]) + start = end + return align_segs + + +def align_to_tierformat(align_segs: List[List[int]], + subsample: int, + token_dict: Dict[int, Text], + blank_id=0) -> List[Text]: + """Generate textgrid.Interval format from alignment segmentations. + + Args: + align_segs (List[List[int]]): segmented ctc alignment ids. + subsample (int): 25ms frame_length, 10ms hop_length, 1/subsample + token_dict (Dict[int, Text]): int -> str map. + + Returns: + List[Text]: list of textgrid.Interval text, str(start, end, text). + """ + hop_length = 10 # ms + second_ms = 1000 # ms + frame_per_second = second_ms / hop_length # 25ms frame_length, 10ms hop_length + second_per_frame = 1.0 / frame_per_second + + begin = 0 + duration = 0 + tierformat = [] + + for idx, tokens in enumerate(align_segs): + token_len = len(tokens) + token = tokens[-1] + # time duration in second + duration = token_len * subsample * second_per_frame + if idx < len(align_segs) - 1: + print(f"{begin:.2f} {begin + duration:.2f} {token_dict[token]}") + tierformat.append( + f"{begin:.2f} {begin + duration:.2f} {token_dict[token]}\n") + else: + for i in tokens: + if i != blank_id: + token = i + break + print(f"{begin:.2f} {begin + duration:.2f} {token_dict[token]}") + tierformat.append( + f"{begin:.2f} {begin + duration:.2f} {token_dict[token]}\n") + begin = begin + duration + + return tierformat + + +def generate_textgrid(maxtime: float, + intervals: List[Text], + output: Text, + name: Text='ali') -> None: + """Create alignment textgrid file. + + Args: + maxtime (float): audio duartion. + intervals (List[Text]): ctc output alignment. e.g. "start-time end-time word" per item. + output (Text): textgrid filepath. + name (Text, optional): tier or layer name. Defaults to 'ali'. + """ + # Download Praat: https://www.fon.hum.uva.nl/praat/ + avg_interval = maxtime / (len(intervals) + 1) + print(f"average second/token: {avg_interval}") + margin = 0.0001 + + tg = textgrid.TextGrid(maxTime=maxtime) + tier = textgrid.IntervalTier(name=name, maxTime=maxtime) + + i = 0 + for dur in intervals: + s, e, text = dur.split() + tier.add(minTime=float(s) + margin, maxTime=float(e), mark=text) + + tg.append(tier) + + tg.write(output) + print("successfully generator textgrid {}.".format(output)) diff --git a/ernie-sat/paddlespeech/s2t/utils/utility.py b/ernie-sat/paddlespeech/s2t/utils/utility.py new file mode 100644 index 0000000..fdd8c02 --- /dev/null +++ b/ernie-sat/paddlespeech/s2t/utils/utility.py @@ -0,0 +1,140 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains common utility functions.""" +import math +import os +import random +import sys +from contextlib import contextmanager +from pprint import pformat +from typing import List + +import distutils.util +import numpy as np +import paddle +import soundfile + +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() + +__all__ = [ + "all_version", "UpdateConfig", "seed_all", 'print_arguments', + 'add_arguments', "log_add" +] + + +def all_version(): + vers = { + "python": sys.version, + "paddle": paddle.__version__, + "paddle_commit": paddle.version.commit, + "soundfile": soundfile.__version__, + } + logger.info(f"Deps Module Version:{pformat(list(vers.items()))}") + + +@contextmanager +def UpdateConfig(config): + """Update yacs config""" + config.defrost() + yield + config.freeze() + + +def seed_all(seed: int=20210329): + """freeze random generator seed.""" + np.random.seed(seed) + random.seed(seed) + paddle.seed(seed) + + +def print_arguments(args, info=None): + """Print argparse's arguments. + + Usage: + + .. code-block:: python + + parser = argparse.ArgumentParser() + parser.add_argument("name", default="Jonh", type=str, help="User name.") + args = parser.parse_args() + print_arguments(args) + + :param args: Input argparse.Namespace for printing. + :type args: argparse.Namespace + """ + filename = "" + if info: + filename = info["__file__"] + filename = os.path.basename(filename) + print(f"----------- {filename} Arguments -----------") + for arg, value in sorted(vars(args).items()): + print("%s: %s" % (arg, value)) + print("-----------------------------------------------------------") + + +def add_arguments(argname, type, default, help, argparser, **kwargs): + """Add argparse's argument. + + Usage: + + .. code-block:: python + + parser = argparse.ArgumentParser() + add_argument("name", str, "Jonh", "User name.", parser) + args = parser.parse_args() + """ + type = distutils.util.strtobool if type == bool else type + argparser.add_argument( + "--" + argname, + default=default, + type=type, + help=help + ' Default: %(default)s.', + **kwargs) + + +def log_add(args: List[int]) -> float: + """Stable log add + + Args: + args (List[int]): log scores + + Returns: + float: sum of log scores + """ + if all(a == -float('inf') for a in args): + return -float('inf') + a_max = max(args) + lsp = math.log(sum(math.exp(a - a_max) for a in args)) + return a_max + lsp + + +def get_subsample(config): + """Subsample rate from config. + + Args: + config (yacs.config.CfgNode): yaml config + + Returns: + int: subsample rate. + """ + input_layer = config["encoder_conf"]["input_layer"] + assert input_layer in ["conv2d", "conv2d6", "conv2d8"] + if input_layer == "conv2d": + return 4 + elif input_layer == "conv2d6": + return 6 + elif input_layer == "conv2d8": + return 8 diff --git a/ernie-sat/paddlespeech/server/README.md b/ernie-sat/paddlespeech/server/README.md new file mode 100644 index 0000000..819fe44 --- /dev/null +++ b/ernie-sat/paddlespeech/server/README.md @@ -0,0 +1,37 @@ +# PaddleSpeech Server Command Line + +([简体中文](./README_cn.md)|English) + + The simplest approach to use PaddleSpeech Server including server and client. + + ## PaddleSpeech Server + ### Help + ```bash + paddlespeech_server help + ``` + ### Start the server + First set the service-related configuration parameters, similar to `./conf/application.yaml`. Set `engine_list`, which represents the speech tasks included in the service to be started + Then start the service: + ```bash + paddlespeech_server start --config_file ./conf/application.yaml + ``` + + ## PaddleSpeech Client + ### Help + ```bash + paddlespeech_client help + ``` + ### Access speech recognition services + ``` + paddlespeech_client asr --server_ip 127.0.0.1 --port 8090 --input input_16k.wav + ``` + + ### Access text to speech services + ```bash + paddlespeech_client tts --server_ip 127.0.0.1 --port 8090 --input "你好,欢迎使用百度飞桨深度学习框架!" --output output.wav + ``` + + ### Access audio classification services + ```bash + paddlespeech_client cls --server_ip 127.0.0.1 --port 8090 --input input.wav + ``` diff --git a/ernie-sat/paddlespeech/server/README_cn.md b/ernie-sat/paddlespeech/server/README_cn.md new file mode 100644 index 0000000..c0a4a73 --- /dev/null +++ b/ernie-sat/paddlespeech/server/README_cn.md @@ -0,0 +1,37 @@ +# PaddleSpeech Server 命令行工具 + +(简体中文|[English](./README.md)) + +它提供了最简便的方式调用 PaddleSpeech 语音服务用一行命令就可以轻松启动服务和调用服务。 + + ## 服务端命令行使用 + ### 帮助 + ```bash + paddlespeech_server help + ``` + ### 启动服务 + 首先设置服务相关配置文件,类似于 `./conf/application.yaml`,设置 `engine_list`,该值表示即将启动的服务中包含的语音任务。 + 然后启动服务: + ```bash + paddlespeech_server start --config_file ./conf/application.yaml + ``` + + ## 客户端命令行使用 + ### 帮助 + ```bash + paddlespeech_client help + ``` + ### 访问语音识别服务 + ``` + paddlespeech_client asr --server_ip 127.0.0.1 --port 8090 --input input_16k.wav + ``` + + ### 访问语音合成服务 + ```bash + paddlespeech_client tts --server_ip 127.0.0.1 --port 8090 --input "你好,欢迎使用百度飞桨深度学习框架!" --output output.wav + ``` + + ### 访问音频分类服务 + ```bash + paddlespeech_client cls --server_ip 127.0.0.1 --port 8090 --input input.wav + ``` diff --git a/ernie-sat/paddlespeech/server/__init__.py b/ernie-sat/paddlespeech/server/__init__.py new file mode 100644 index 0000000..97722c0 --- /dev/null +++ b/ernie-sat/paddlespeech/server/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import _locale + +from .base_commands import ClientBaseCommand +from .base_commands import ClientHelpCommand +from .base_commands import ServerBaseCommand +from .base_commands import ServerHelpCommand +from .bin.paddlespeech_client import ASRClientExecutor +from .bin.paddlespeech_client import CLSClientExecutor +from .bin.paddlespeech_client import TTSClientExecutor +from .bin.paddlespeech_server import ServerExecutor + +_locale._getdefaultlocale = (lambda *args: ['en_US', 'utf8']) diff --git a/ernie-sat/paddlespeech/server/base_commands.py b/ernie-sat/paddlespeech/server/base_commands.py new file mode 100644 index 0000000..d123929 --- /dev/null +++ b/ernie-sat/paddlespeech/server/base_commands.py @@ -0,0 +1,82 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from .entry import client_commands +from .entry import server_commands +from .util import cli_client_register +from .util import cli_server_register +from .util import get_client_command +from .util import get_server_command + +__all__ = [ + 'ServerBaseCommand', + 'ServerHelpCommand', + 'ClientBaseCommand', + 'ClientHelpCommand', +] + + +@cli_server_register(name='paddlespeech_server') +class ServerBaseCommand: + def execute(self, argv: List[str]) -> bool: + help = get_server_command('paddlespeech_server.help') + return help().execute(argv) + + +@cli_server_register( + name='paddlespeech_server.help', description='Show help for commands.') +class ServerHelpCommand: + def execute(self, argv: List[str]) -> bool: + msg = 'Usage:\n' + msg += ' paddlespeech_server \n\n' + msg += 'Commands:\n' + for command, detail in server_commands['paddlespeech_server'].items(): + if command.startswith('_'): + continue + + if '_description' not in detail: + continue + msg += ' {:<15} {}\n'.format(command, + detail['_description']) + + print(msg) + return True + + +@cli_client_register(name='paddlespeech_client') +class ClientBaseCommand: + def execute(self, argv: List[str]) -> bool: + help = get_client_command('paddlespeech_client.help') + return help().execute(argv) + + +@cli_client_register( + name='paddlespeech_client.help', description='Show help for commands.') +class ClientHelpCommand: + def execute(self, argv: List[str]) -> bool: + msg = 'Usage:\n' + msg += ' paddlespeech_client \n\n' + msg += 'Commands:\n' + for command, detail in client_commands['paddlespeech_client'].items(): + if command.startswith('_'): + continue + + if '_description' not in detail: + continue + msg += ' {:<15} {}\n'.format(command, + detail['_description']) + + print(msg) + return True diff --git a/ernie-sat/paddlespeech/server/bin/__init__.py b/ernie-sat/paddlespeech/server/bin/__init__.py new file mode 100644 index 0000000..025aab0 --- /dev/null +++ b/ernie-sat/paddlespeech/server/bin/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .paddlespeech_client import ASRClientExecutor +from .paddlespeech_client import TTSClientExecutor +from .paddlespeech_server import ServerExecutor +from .paddlespeech_server import ServerStatsExecutor diff --git a/ernie-sat/paddlespeech/server/bin/main.py b/ernie-sat/paddlespeech/server/bin/main.py new file mode 100644 index 0000000..81824c8 --- /dev/null +++ b/ernie-sat/paddlespeech/server/bin/main.py @@ -0,0 +1,77 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse + +import uvicorn +from fastapi import FastAPI + +from paddlespeech.server.engine.engine_pool import init_engine_pool +from paddlespeech.server.restful.api import setup_router as setup_http_router +from paddlespeech.server.utils.config import get_config +from paddlespeech.server.ws.api import setup_router as setup_ws_router + +app = FastAPI( + title="PaddleSpeech Serving API", description="Api", version="0.0.1") + + +def init(config): + """system initialization + + Args: + config (CfgNode): config object + + Returns: + bool: + """ + # init api + api_list = list(engine.split("_")[0] for engine in config.engine_list) + if config.protocol == "websocket": + api_router = setup_ws_router(api_list) + elif config.protocol == "http": + api_router = setup_http_router(api_list) + else: + raise Exception("unsupported protocol") + app.include_router(api_router) + + if not init_engine_pool(config): + return False + + return True + + +def main(args): + """main function""" + + config = get_config(args.config_file) + + if init(config): + uvicorn.run(app, host=config.host, port=config.port, debug=True) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--config_file", + action="store", + help="yaml file of the app", + default="./conf/application.yaml") + + parser.add_argument( + "--log_file", + action="store", + help="log file", + default="./log/paddlespeech.log") + args = parser.parse_args() + + main(args) diff --git a/ernie-sat/paddlespeech/server/bin/paddlespeech_client.py b/ernie-sat/paddlespeech/server/bin/paddlespeech_client.py new file mode 100644 index 0000000..413f008 --- /dev/null +++ b/ernie-sat/paddlespeech/server/bin/paddlespeech_client.py @@ -0,0 +1,289 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import base64 +import io +import json +import os +import random +import time +from typing import List + +import numpy as np +import requests +import soundfile + +from ..executor import BaseExecutor +from ..util import cli_client_register +from ..util import stats_wrapper +from paddlespeech.cli.log import logger +from paddlespeech.server.utils.audio_process import wav2pcm +from paddlespeech.server.utils.util import wav2base64 + +__all__ = ['TTSClientExecutor', 'ASRClientExecutor', 'CLSClientExecutor'] + + +@cli_client_register( + name='paddlespeech_client.tts', description='visit tts service') +class TTSClientExecutor(BaseExecutor): + def __init__(self): + super(TTSClientExecutor, self).__init__() + self.parser = argparse.ArgumentParser( + prog='paddlespeech_client.tts', add_help=True) + self.parser.add_argument( + '--server_ip', type=str, default='127.0.0.1', help='server ip') + self.parser.add_argument( + '--port', type=int, default=8090, help='server port') + self.parser.add_argument( + '--input', + type=str, + default=None, + help='Text to be synthesized.', + required=True) + self.parser.add_argument( + '--spk_id', type=int, default=0, help='Speaker id') + self.parser.add_argument( + '--speed', + type=float, + default=1.0, + help='Audio speed, the value should be set between 0 and 3') + self.parser.add_argument( + '--volume', + type=float, + default=1.0, + help='Audio volume, the value should be set between 0 and 3') + self.parser.add_argument( + '--sample_rate', + type=int, + default=0, + choices=[0, 8000, 16000], + help='Sampling rate, the default is the same as the model') + self.parser.add_argument( + '--output', type=str, default=None, help='Synthesized audio file') + + def postprocess(self, wav_base64: str, outfile: str) -> float: + audio_data_byte = base64.b64decode(wav_base64) + # from byte + samples, sample_rate = soundfile.read( + io.BytesIO(audio_data_byte), dtype='float32') + + # transform audio + if outfile.endswith(".wav"): + soundfile.write(outfile, samples, sample_rate) + elif outfile.endswith(".pcm"): + temp_wav = str(random.getrandbits(128)) + ".wav" + soundfile.write(temp_wav, samples, sample_rate) + wav2pcm(temp_wav, outfile, data_type=np.int16) + os.system("rm %s" % (temp_wav)) + else: + logger.error("The format for saving audio only supports wav or pcm") + + def execute(self, argv: List[str]) -> bool: + args = self.parser.parse_args(argv) + input_ = args.input + server_ip = args.server_ip + port = args.port + spk_id = args.spk_id + speed = args.speed + volume = args.volume + sample_rate = args.sample_rate + output = args.output + + try: + time_start = time.time() + res = self( + input=input_, + server_ip=server_ip, + port=port, + spk_id=spk_id, + speed=speed, + volume=volume, + sample_rate=sample_rate, + output=output) + time_end = time.time() + time_consume = time_end - time_start + response_dict = res.json() + logger.info(response_dict["message"]) + logger.info("Save synthesized audio successfully on %s." % (output)) + logger.info("Audio duration: %f s." % + (response_dict['result']['duration'])) + logger.info("Response time: %f s." % (time_consume)) + return True + except Exception as e: + logger.error("Failed to synthesized audio.") + return False + + @stats_wrapper + def __call__(self, + input: str, + server_ip: str="127.0.0.1", + port: int=8090, + spk_id: int=0, + speed: float=1.0, + volume: float=1.0, + sample_rate: int=0, + output: str=None): + """ + Python API to call an executor. + """ + + url = 'http://' + server_ip + ":" + str(port) + '/paddlespeech/tts' + request = { + "text": input, + "spk_id": spk_id, + "speed": speed, + "volume": volume, + "sample_rate": sample_rate, + "save_path": output + } + + res = requests.post(url, json.dumps(request)) + response_dict = res.json() + if output is not None: + self.postprocess(response_dict["result"]["audio"], output) + return res + + +@cli_client_register( + name='paddlespeech_client.asr', description='visit asr service') +class ASRClientExecutor(BaseExecutor): + def __init__(self): + super(ASRClientExecutor, self).__init__() + self.parser = argparse.ArgumentParser( + prog='paddlespeech_client.asr', add_help=True) + self.parser.add_argument( + '--server_ip', type=str, default='127.0.0.1', help='server ip') + self.parser.add_argument( + '--port', type=int, default=8090, help='server port') + self.parser.add_argument( + '--input', + type=str, + default=None, + help='Audio file to be recognized', + required=True) + self.parser.add_argument( + '--sample_rate', type=int, default=16000, help='audio sample rate') + self.parser.add_argument( + '--lang', type=str, default="zh_cn", help='language') + self.parser.add_argument( + '--audio_format', type=str, default="wav", help='audio format') + + def execute(self, argv: List[str]) -> bool: + args = self.parser.parse_args(argv) + input_ = args.input + server_ip = args.server_ip + port = args.port + sample_rate = args.sample_rate + lang = args.lang + audio_format = args.audio_format + + try: + time_start = time.time() + res = self( + input=input_, + server_ip=server_ip, + port=port, + sample_rate=sample_rate, + lang=lang, + audio_format=audio_format) + time_end = time.time() + logger.info(res.json()) + logger.info("Response time %f s." % (time_end - time_start)) + return True + except Exception as e: + logger.error("Failed to speech recognition.") + return False + + @stats_wrapper + def __call__(self, + input: str, + server_ip: str="127.0.0.1", + port: int=8090, + sample_rate: int=16000, + lang: str="zh_cn", + audio_format: str="wav"): + """ + Python API to call an executor. + """ + + url = 'http://' + server_ip + ":" + str(port) + '/paddlespeech/asr' + audio = wav2base64(input) + data = { + "audio": audio, + "audio_format": audio_format, + "sample_rate": sample_rate, + "lang": lang, + } + + res = requests.post(url=url, data=json.dumps(data)) + return res + + +@cli_client_register( + name='paddlespeech_client.cls', description='visit cls service') +class CLSClientExecutor(BaseExecutor): + def __init__(self): + super(CLSClientExecutor, self).__init__() + self.parser = argparse.ArgumentParser( + prog='paddlespeech_client.cls', add_help=True) + self.parser.add_argument( + '--server_ip', type=str, default='127.0.0.1', help='server ip') + self.parser.add_argument( + '--port', type=int, default=8090, help='server port') + self.parser.add_argument( + '--input', + type=str, + default=None, + help='Audio file to classify.', + required=True) + self.parser.add_argument( + '--topk', + type=int, + default=1, + help='Return topk scores of classification result.') + + def execute(self, argv: List[str]) -> bool: + args = self.parser.parse_args(argv) + input_ = args.input + server_ip = args.server_ip + port = args.port + topk = args.topk + + try: + time_start = time.time() + res = self(input=input_, server_ip=server_ip, port=port, topk=topk) + time_end = time.time() + logger.info(res.json()) + logger.info("Response time %f s." % (time_end - time_start)) + return True + except Exception as e: + logger.error("Failed to speech classification.") + return False + + @stats_wrapper + def __call__(self, + input: str, + server_ip: str="127.0.0.1", + port: int=8090, + topk: int=1): + """ + Python API to call an executor. + """ + + url = 'http://' + server_ip + ":" + str(port) + '/paddlespeech/cls' + audio = wav2base64(input) + data = {"audio": audio, "topk": topk} + + res = requests.post(url=url, data=json.dumps(data)) + return res diff --git a/ernie-sat/paddlespeech/server/bin/paddlespeech_server.py b/ernie-sat/paddlespeech/server/bin/paddlespeech_server.py new file mode 100644 index 0000000..f6a7f42 --- /dev/null +++ b/ernie-sat/paddlespeech/server/bin/paddlespeech_server.py @@ -0,0 +1,198 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from typing import List + +import uvicorn +from fastapi import FastAPI +from prettytable import PrettyTable + +from ..executor import BaseExecutor +from ..util import cli_server_register +from ..util import stats_wrapper +from paddlespeech.cli.log import logger +from paddlespeech.server.engine.engine_pool import init_engine_pool +from paddlespeech.server.restful.api import setup_router +from paddlespeech.server.utils.config import get_config + +__all__ = ['ServerExecutor', 'ServerStatsExecutor'] + +app = FastAPI( + title="PaddleSpeech Serving API", description="Api", version="0.0.1") + + +@cli_server_register( + name='paddlespeech_server.start', description='Start the service') +class ServerExecutor(BaseExecutor): + def __init__(self): + super(ServerExecutor, self).__init__() + self.parser = argparse.ArgumentParser( + prog='paddlespeech_server.start', add_help=True) + self.parser.add_argument( + "--config_file", + action="store", + help="yaml file of the app", + default=None, + required=True) + + self.parser.add_argument( + "--log_file", + action="store", + help="log file", + default="./log/paddlespeech.log") + + def init(self, config) -> bool: + """system initialization + + Args: + config (CfgNode): config object + + Returns: + bool: + """ + # init api + api_list = list(engine.split("_")[0] for engine in config.engine_list) + api_router = setup_router(api_list) + app.include_router(api_router) + + if not init_engine_pool(config): + return False + + return True + + def execute(self, argv: List[str]) -> bool: + args = self.parser.parse_args(argv) + config = get_config(args.config_file) + + if self.init(config): + uvicorn.run(app, host=config.host, port=config.port, debug=True) + + @stats_wrapper + def __call__(self, + config_file: str="./conf/application.yaml", + log_file: str="./log/paddlespeech.log"): + """ + Python API to call an executor. + """ + config = get_config(config_file) + if self.init(config): + uvicorn.run(app, host=config.host, port=config.port, debug=True) + + +@cli_server_register( + name='paddlespeech_server.stats', + description='Get the models supported by each speech task in the service.') +class ServerStatsExecutor(): + def __init__(self): + super(ServerStatsExecutor, self).__init__() + + self.parser = argparse.ArgumentParser( + prog='paddlespeech_server.stats', add_help=True) + self.parser.add_argument( + '--task', + type=str, + default=None, + choices=['asr', 'tts', 'cls'], + help='Choose speech task.', + required=True) + self.task_choices = ['asr', 'tts', 'cls'] + self.model_name_format = { + 'asr': 'Model-Language-Sample Rate', + 'tts': 'Model-Language', + 'cls': 'Model-Sample Rate' + } + + def show_support_models(self, pretrained_models: dict): + fields = self.model_name_format[self.task].split("-") + table = PrettyTable(fields) + for key in pretrained_models: + table.add_row(key.split("-")) + print(table) + + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. + """ + parser_args = self.parser.parse_args(argv) + self.task = parser_args.task + if self.task not in self.task_choices: + logger.error( + "Please input correct speech task, choices = ['asr', 'tts']") + return False + + elif self.task == 'asr': + try: + from paddlespeech.cli.asr.infer import pretrained_models + logger.info( + "Here is the table of ASR pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + # show ASR static pretrained model + from paddlespeech.server.engine.asr.paddleinference.asr_engine import pretrained_models + logger.info( + "Here is the table of ASR static pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + return True + except BaseException: + logger.error( + "Failed to get the table of ASR pretrained models supported in the service." + ) + return False + + elif self.task == 'tts': + try: + from paddlespeech.cli.tts.infer import pretrained_models + logger.info( + "Here is the table of TTS pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + # show TTS static pretrained model + from paddlespeech.server.engine.tts.paddleinference.tts_engine import pretrained_models + logger.info( + "Here is the table of TTS static pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + return True + except BaseException: + logger.error( + "Failed to get the table of TTS pretrained models supported in the service." + ) + return False + + elif self.task == 'cls': + try: + from paddlespeech.cli.cls.infer import pretrained_models + logger.info( + "Here is the table of CLS pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + # show CLS static pretrained model + from paddlespeech.server.engine.cls.paddleinference.cls_engine import pretrained_models + logger.info( + "Here is the table of CLS static pretrained models supported in the service." + ) + self.show_support_models(pretrained_models) + + return True + except BaseException: + logger.error( + "Failed to get the table of CLS pretrained models supported in the service." + ) + return False diff --git a/ernie-sat/paddlespeech/server/conf/application.yaml b/ernie-sat/paddlespeech/server/conf/application.yaml new file mode 100644 index 0000000..849349c --- /dev/null +++ b/ernie-sat/paddlespeech/server/conf/application.yaml @@ -0,0 +1,157 @@ +# This is the parameter configuration file for PaddleSpeech Serving. + +################################################################################# +# SERVER SETTING # +################################################################################# +host: 127.0.0.1 +port: 8090 + +# The task format in the engin_list is: _ +# task choices = ['asr_python', 'asr_inference', 'tts_python', 'tts_inference'] +# protocol = ['websocket', 'http'] (only one can be selected). +# http only support offline engine type. +protocol: 'http' +engine_list: ['asr_python', 'tts_python', 'cls_python'] + + +################################################################################# +# ENGINE CONFIG # +################################################################################# + +################################### ASR ######################################### +################### speech task: asr; engine_type: python ####################### +asr_python: + model: 'conformer_wenetspeech' + lang: 'zh' + sample_rate: 16000 + cfg_path: # [optional] + ckpt_path: # [optional] + decode_method: 'attention_rescoring' + force_yes: True + device: # set 'gpu:id' or 'cpu' + + +################### speech task: asr; engine_type: inference ####################### +asr_inference: + # model_type choices=['deepspeech2offline_aishell'] + model_type: 'deepspeech2offline_aishell' + am_model: # the pdmodel file of am static model [optional] + am_params: # the pdiparams file of am static model [optional] + lang: 'zh' + sample_rate: 16000 + cfg_path: + decode_method: + force_yes: True + + am_predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + + +################### speech task: asr; engine_type: online ####################### +asr_online: + model_type: 'deepspeech2online_aishell' + am_model: # the pdmodel file of am static model [optional] + am_params: # the pdiparams file of am static model [optional] + lang: 'zh' + sample_rate: 16000 + cfg_path: + decode_method: + force_yes: True + + am_predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + + +################################### TTS ######################################### +################### speech task: tts; engine_type: python ####################### +tts_python: + # am (acoustic model) choices=['speedyspeech_csmsc', 'fastspeech2_csmsc', + # 'fastspeech2_ljspeech', 'fastspeech2_aishell3', + # 'fastspeech2_vctk'] + am: 'fastspeech2_csmsc' + am_config: + am_ckpt: + am_stat: + phones_dict: + tones_dict: + speaker_dict: + spk_id: 0 + + # voc (vocoder) choices=['pwgan_csmsc', 'pwgan_ljspeech', 'pwgan_aishell3', + # 'pwgan_vctk', 'mb_melgan_csmsc'] + voc: 'pwgan_csmsc' + voc_config: + voc_ckpt: + voc_stat: + + # others + lang: 'zh' + device: # set 'gpu:id' or 'cpu' + + +################### speech task: tts; engine_type: inference ####################### +tts_inference: + # am (acoustic model) choices=['speedyspeech_csmsc', 'fastspeech2_csmsc'] + am: 'fastspeech2_csmsc' + am_model: # the pdmodel file of your am static model (XX.pdmodel) + am_params: # the pdiparams file of your am static model (XX.pdipparams) + am_sample_rate: 24000 + phones_dict: + tones_dict: + speaker_dict: + spk_id: 0 + + am_predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + + # voc (vocoder) choices=['pwgan_csmsc', 'mb_melgan_csmsc','hifigan_csmsc'] + voc: 'pwgan_csmsc' + voc_model: # the pdmodel file of your vocoder static model (XX.pdmodel) + voc_params: # the pdiparams file of your vocoder static model (XX.pdipparams) + voc_sample_rate: 24000 + + voc_predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + + # others + lang: 'zh' + + +################################### CLS ######################################### +################### speech task: cls; engine_type: python ####################### +cls_python: + # model choices=['panns_cnn14', 'panns_cnn10', 'panns_cnn6'] + model: 'panns_cnn14' + cfg_path: # [optional] Config of cls task. + ckpt_path: # [optional] Checkpoint file of model. + label_file: # [optional] Label file of cls task. + device: # set 'gpu:id' or 'cpu' + + +################### speech task: cls; engine_type: inference ####################### +cls_inference: + # model_type choices=['panns_cnn14', 'panns_cnn10', 'panns_cnn6'] + model_type: 'panns_cnn14' + cfg_path: + model_path: # the pdmodel file of am static model [optional] + params_path: # the pdiparams file of am static model [optional] + label_file: # [optional] Label file of cls task. + + predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + diff --git a/ernie-sat/paddlespeech/server/conf/ws_application.yaml b/ernie-sat/paddlespeech/server/conf/ws_application.yaml new file mode 100644 index 0000000..ef23593 --- /dev/null +++ b/ernie-sat/paddlespeech/server/conf/ws_application.yaml @@ -0,0 +1,51 @@ +# This is the parameter configuration file for PaddleSpeech Serving. + +################################################################################# +# SERVER SETTING # +################################################################################# +host: 0.0.0.0 +port: 8091 + +# The task format in the engin_list is: _ +# task choices = ['asr_online', 'tts_online'] +# protocol = ['websocket', 'http'] (only one can be selected). +# websocket only support online engine type. +protocol: 'websocket' +engine_list: ['asr_online'] + + +################################################################################# +# ENGINE CONFIG # +################################################################################# + +################################### ASR ######################################### +################### speech task: asr; engine_type: online ####################### +asr_online: + model_type: 'deepspeech2online_aishell' + am_model: # the pdmodel file of am static model [optional] + am_params: # the pdiparams file of am static model [optional] + lang: 'zh' + sample_rate: 16000 + cfg_path: + decode_method: + force_yes: True + + am_predictor_conf: + device: # set 'gpu:id' or 'cpu' + switch_ir_optim: True + glog_info: False # True -> print glog + summary: True # False -> do not show predictor config + + chunk_buffer_conf: + frame_duration_ms: 80 + shift_ms: 40 + sample_rate: 16000 + sample_width: 2 + + vad_conf: + aggressiveness: 2 + sample_rate: 16000 + frame_duration_ms: 20 + sample_width: 2 + padding_ms: 200 + padding_ratio: 0.9 diff --git a/ernie-sat/paddlespeech/server/download.py b/ernie-sat/paddlespeech/server/download.py new file mode 100644 index 0000000..ea943dd --- /dev/null +++ b/ernie-sat/paddlespeech/server/download.py @@ -0,0 +1,329 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import hashlib +import os +import os.path as osp +import shutil +import subprocess +import tarfile +import time +import zipfile + +import requests +from tqdm import tqdm + +from paddlespeech.cli.log import logger + +__all__ = ['get_path_from_url'] + +DOWNLOAD_RETRY_LIMIT = 3 + + +def _is_url(path): + """ + Whether path is URL. + Args: + path (string): URL string or not. + """ + return path.startswith('http://') or path.startswith('https://') + + +def _map_path(url, root_dir): + # parse path after download under root_dir + fname = osp.split(url)[-1] + fpath = fname + return osp.join(root_dir, fpath) + + +def _get_unique_endpoints(trainer_endpoints): + # Sorting is to avoid different environmental variables for each card + trainer_endpoints.sort() + ips = set() + unique_endpoints = set() + for endpoint in trainer_endpoints: + ip = endpoint.split(":")[0] + if ip in ips: + continue + ips.add(ip) + unique_endpoints.add(endpoint) + logger.info("unique_endpoints {}".format(unique_endpoints)) + return unique_endpoints + + +def get_path_from_url(url, + root_dir, + md5sum=None, + check_exist=True, + decompress=True, + method='get'): + """ Download from given url to root_dir. + if file or directory specified by url is exists under + root_dir, return the path directly, otherwise download + from url and decompress it, return the path. + Args: + url (str): download url + root_dir (str): root dir for downloading, it should be + WEIGHTS_HOME or DATASET_HOME + md5sum (str): md5 sum of download package + decompress (bool): decompress zip or tar file. Default is `True` + method (str): which download method to use. Support `wget` and `get`. Default is `get`. + Returns: + str: a local path to save downloaded models & weights & datasets. + """ + + from paddle.fluid.dygraph.parallel import ParallelEnv + + assert _is_url(url), "downloading from {} not a url".format(url) + # parse path after download to decompress under root_dir + fullpath = _map_path(url, root_dir) + # Mainly used to solve the problem of downloading data from different + # machines in the case of multiple machines. Different ips will download + # data, and the same ip will only download data once. + unique_endpoints = _get_unique_endpoints(ParallelEnv().trainer_endpoints[:]) + if osp.exists(fullpath) and check_exist and _md5check(fullpath, md5sum): + logger.info("Found {}".format(fullpath)) + else: + if ParallelEnv().current_endpoint in unique_endpoints: + fullpath = _download(url, root_dir, md5sum, method=method) + else: + while not os.path.exists(fullpath): + time.sleep(1) + + if ParallelEnv().current_endpoint in unique_endpoints: + if decompress and (tarfile.is_tarfile(fullpath) or + zipfile.is_zipfile(fullpath)): + fullpath = _decompress(fullpath) + + return fullpath + + +def _get_download(url, fullname): + # using requests.get method + fname = osp.basename(fullname) + try: + req = requests.get(url, stream=True) + except Exception as e: # requests.exceptions.ConnectionError + logger.info("Downloading {} from {} failed with exception {}".format( + fname, url, str(e))) + return False + + if req.status_code != 200: + raise RuntimeError("Downloading from {} failed with code " + "{}!".format(url, req.status_code)) + + # For protecting download interupted, download to + # tmp_fullname firstly, move tmp_fullname to fullname + # after download finished + tmp_fullname = fullname + "_tmp" + total_size = req.headers.get('content-length') + with open(tmp_fullname, 'wb') as f: + if total_size: + with tqdm(total=(int(total_size) + 1023) // 1024) as pbar: + for chunk in req.iter_content(chunk_size=1024): + f.write(chunk) + pbar.update(1) + else: + for chunk in req.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + shutil.move(tmp_fullname, fullname) + + return fullname + + +def _wget_download(url, fullname): + # using wget to download url + tmp_fullname = fullname + "_tmp" + # –user-agent + command = 'wget -O {} -t {} {}'.format(tmp_fullname, DOWNLOAD_RETRY_LIMIT, + url) + subprc = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _ = subprc.communicate() + + if subprc.returncode != 0: + raise RuntimeError( + '{} failed. Please make sure `wget` is installed or {} exists'. + format(command, url)) + + shutil.move(tmp_fullname, fullname) + + return fullname + + +_download_methods = { + 'get': _get_download, + 'wget': _wget_download, +} + + +def _download(url, path, md5sum=None, method='get'): + """ + Download from url, save to path. + url (str): download url + path (str): download to given path + md5sum (str): md5 sum of download package + method (str): which download method to use. Support `wget` and `get`. Default is `get`. + """ + assert method in _download_methods, 'make sure `{}` implemented'.format( + method) + + if not osp.exists(path): + os.makedirs(path) + + fname = osp.split(url)[-1] + fullname = osp.join(path, fname) + retry_cnt = 0 + + logger.info("Downloading {} from {}".format(fname, url)) + while not (osp.exists(fullname) and _md5check(fullname, md5sum)): + if retry_cnt < DOWNLOAD_RETRY_LIMIT: + retry_cnt += 1 + else: + raise RuntimeError("Download from {} failed. " + "Retry limit reached".format(url)) + + if not _download_methods[method](url, fullname): + time.sleep(1) + continue + + return fullname + + +def _md5check(fullname, md5sum=None): + if md5sum is None: + return True + + logger.info("File {} md5 checking...".format(fullname)) + md5 = hashlib.md5() + with open(fullname, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + md5.update(chunk) + calc_md5sum = md5.hexdigest() + + if calc_md5sum != md5sum: + logger.info("File {} md5 check failed, {}(calc) != " + "{}(base)".format(fullname, calc_md5sum, md5sum)) + return False + return True + + +def _decompress(fname): + """ + Decompress for zip and tar file + """ + logger.info("Decompressing {}...".format(fname)) + + # For protecting decompressing interupted, + # decompress to fpath_tmp directory firstly, if decompress + # successed, move decompress files to fpath and delete + # fpath_tmp and remove download compress file. + + if tarfile.is_tarfile(fname): + uncompressed_path = _uncompress_file_tar(fname) + elif zipfile.is_zipfile(fname): + uncompressed_path = _uncompress_file_zip(fname) + else: + raise TypeError("Unsupport compress file type {}".format(fname)) + + return uncompressed_path + + +def _uncompress_file_zip(filepath): + files = zipfile.ZipFile(filepath, 'r') + file_list = files.namelist() + + file_dir = os.path.dirname(filepath) + + if _is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + + for item in file_list: + files.extract(item, file_dir) + + elif _is_a_single_dir(file_list): + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[0] + uncompressed_path = os.path.join(file_dir, rootpath) + + for item in file_list: + files.extract(item, file_dir) + + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + if not os.path.exists(uncompressed_path): + os.makedirs(uncompressed_path) + for item in file_list: + files.extract(item, os.path.join(file_dir, rootpath)) + + files.close() + + return uncompressed_path + + +def _uncompress_file_tar(filepath, mode="r:*"): + files = tarfile.open(filepath, mode) + file_list = files.getnames() + + file_dir = os.path.dirname(filepath) + + if _is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + for item in file_list: + files.extract(item, file_dir) + elif _is_a_single_dir(file_list): + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + for item in file_list: + files.extract(item, file_dir) + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + if not os.path.exists(uncompressed_path): + os.makedirs(uncompressed_path) + + for item in file_list: + files.extract(item, os.path.join(file_dir, rootpath)) + + files.close() + + return uncompressed_path + + +def _is_a_single_file(file_list): + if len(file_list) == 1 and file_list[0].find(os.sep) < -1: + return True + return False + + +def _is_a_single_dir(file_list): + new_file_list = [] + for file_path in file_list: + if '/' in file_path: + file_path = file_path.replace('/', os.sep) + elif '\\' in file_path: + file_path = file_path.replace('\\', os.sep) + new_file_list.append(file_path) + + file_name = new_file_list[0].split(os.sep)[0] + for i in range(1, len(new_file_list)): + if file_name != new_file_list[i].split(os.sep)[0]: + return False + return True diff --git a/ernie-sat/paddlespeech/server/engine/__init__.py b/ernie-sat/paddlespeech/server/engine/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/asr/__init__.py b/ernie-sat/paddlespeech/server/engine/asr/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/asr/online/__init__.py b/ernie-sat/paddlespeech/server/engine/asr/online/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/online/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/asr/online/asr_engine.py b/ernie-sat/paddlespeech/server/engine/asr/online/asr_engine.py new file mode 100644 index 0000000..9029aa6 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/online/asr_engine.py @@ -0,0 +1,352 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from typing import Optional + +import numpy as np +import paddle +from numpy import float32 +from yacs.config import CfgNode + +from paddlespeech.cli.asr.infer import ASRExecutor +from paddlespeech.cli.log import logger +from paddlespeech.cli.utils import MODEL_HOME +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.frontend.speech import SpeechSegment +from paddlespeech.s2t.modules.ctc import CTCDecoder +from paddlespeech.s2t.utils.utility import UpdateConfig +from paddlespeech.server.engine.base_engine import BaseEngine +from paddlespeech.server.utils.paddle_predictor import init_predictor + +__all__ = ['ASREngine'] + +pretrained_models = { + "deepspeech2online_aishell-zh-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/aishell/asr0/asr0_deepspeech2_online_aishell_ckpt_0.1.1.model.tar.gz', + 'md5': + '23e16c69730a1cb5d735c98c83c21e16', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/deepspeech2_online/checkpoints/avg_1', + 'model': + 'exp/deepspeech2_online/checkpoints/avg_1.jit.pdmodel', + 'params': + 'exp/deepspeech2_online/checkpoints/avg_1.jit.pdiparams', + 'lm_url': + 'https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm', + 'lm_md5': + '29e02312deb2e59b3c8686c7966d4fe3' + }, +} + + +class ASRServerExecutor(ASRExecutor): + def __init__(self): + super().__init__() + pass + + def _init_from_path(self, + model_type: str='wenetspeech', + am_model: Optional[os.PathLike]=None, + am_params: Optional[os.PathLike]=None, + lang: str='zh', + sample_rate: int=16000, + cfg_path: Optional[os.PathLike]=None, + decode_method: str='attention_rescoring', + am_predictor_conf: dict=None): + """ + Init model and other resources from a specific path. + """ + + if cfg_path is None or am_model is None or am_params is None: + sample_rate_str = '16k' if sample_rate == 16000 else '8k' + tag = model_type + '-' + lang + '-' + sample_rate_str + res_path = self._get_pretrained_path(tag) # wenetspeech_zh + self.res_path = res_path + self.cfg_path = os.path.join(res_path, + pretrained_models[tag]['cfg_path']) + + self.am_model = os.path.join(res_path, + pretrained_models[tag]['model']) + self.am_params = os.path.join(res_path, + pretrained_models[tag]['params']) + logger.info(res_path) + logger.info(self.cfg_path) + logger.info(self.am_model) + logger.info(self.am_params) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.am_model = os.path.abspath(am_model) + self.am_params = os.path.abspath(am_params) + self.res_path = os.path.dirname( + os.path.dirname(os.path.abspath(self.cfg_path))) + + #Init body. + self.config = CfgNode(new_allowed=True) + self.config.merge_from_file(self.cfg_path) + + with UpdateConfig(self.config): + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + from paddlespeech.s2t.io.collator import SpeechCollator + self.vocab = self.config.vocab_filepath + self.config.decode.lang_model_path = os.path.join( + MODEL_HOME, 'language_model', + self.config.decode.lang_model_path) + self.collate_fn_test = SpeechCollator.from_config(self.config) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, vocab=self.vocab) + + lm_url = pretrained_models[tag]['lm_url'] + lm_md5 = pretrained_models[tag]['lm_md5'] + self.download_lm( + lm_url, + os.path.dirname(self.config.decode.lang_model_path), lm_md5) + elif "conformer" in model_type or "transformer" in model_type or "wenetspeech" in model_type: + raise Exception("wrong type") + else: + raise Exception("wrong type") + + # AM predictor + self.am_predictor_conf = am_predictor_conf + self.am_predictor = init_predictor( + model_file=self.am_model, + params_file=self.am_params, + predictor_conf=self.am_predictor_conf) + + # decoder + self.decoder = CTCDecoder( + odim=self.config.output_dim, # is in vocab + enc_n_units=self.config.rnn_layer_size * 2, + blank_id=self.config.blank_id, + dropout_rate=0.0, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=self.config.get('ctc_grad_norm_type', None)) + + # init decoder + cfg = self.config.decode + decode_batch_size = 1 # for online + self.decoder.init_decoder( + decode_batch_size, self.text_feature.vocab_list, + cfg.decoding_method, cfg.lang_model_path, cfg.alpha, cfg.beta, + cfg.beam_size, cfg.cutoff_prob, cfg.cutoff_top_n, + cfg.num_proc_bsearch) + + # init state box + self.chunk_state_h_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=float32) + self.chunk_state_c_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=float32) + + def reset_decoder_and_chunk(self): + """reset decoder and chunk state for an new audio + """ + self.decoder.reset_decoder(batch_size=1) + # init state box, for new audio request + self.chunk_state_h_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=float32) + self.chunk_state_c_box = np.zeros( + (self.config.num_rnn_layers, 1, self.config.rnn_layer_size), + dtype=float32) + + def decode_one_chunk(self, x_chunk, x_chunk_lens, model_type: str): + """decode one chunk + + Args: + x_chunk (numpy.array): shape[B, T, D] + x_chunk_lens (numpy.array): shape[B] + model_type (str): online model type + + Returns: + [type]: [description] + """ + if "deepspeech2online" in model_type: + input_names = self.am_predictor.get_input_names() + audio_handle = self.am_predictor.get_input_handle(input_names[0]) + audio_len_handle = self.am_predictor.get_input_handle( + input_names[1]) + h_box_handle = self.am_predictor.get_input_handle(input_names[2]) + c_box_handle = self.am_predictor.get_input_handle(input_names[3]) + + audio_handle.reshape(x_chunk.shape) + audio_handle.copy_from_cpu(x_chunk) + + audio_len_handle.reshape(x_chunk_lens.shape) + audio_len_handle.copy_from_cpu(x_chunk_lens) + + h_box_handle.reshape(self.chunk_state_h_box.shape) + h_box_handle.copy_from_cpu(self.chunk_state_h_box) + + c_box_handle.reshape(self.chunk_state_c_box.shape) + c_box_handle.copy_from_cpu(self.chunk_state_c_box) + + output_names = self.am_predictor.get_output_names() + output_handle = self.am_predictor.get_output_handle(output_names[0]) + output_lens_handle = self.am_predictor.get_output_handle( + output_names[1]) + output_state_h_handle = self.am_predictor.get_output_handle( + output_names[2]) + output_state_c_handle = self.am_predictor.get_output_handle( + output_names[3]) + + self.am_predictor.run() + + output_chunk_probs = output_handle.copy_to_cpu() + output_chunk_lens = output_lens_handle.copy_to_cpu() + self.chunk_state_h_box = output_state_h_handle.copy_to_cpu() + self.chunk_state_c_box = output_state_c_handle.copy_to_cpu() + + self.decoder.next(output_chunk_probs, output_chunk_lens) + trans_best, trans_beam = self.decoder.decode() + + return trans_best[0] + + elif "conformer" in model_type or "transformer" in model_type: + raise Exception("invalid model name") + else: + raise Exception("invalid model name") + + def _pcm16to32(self, audio): + """pcm int16 to float32 + + Args: + audio(numpy.array): numpy.int16 + + Returns: + audio(numpy.array): numpy.float32 + """ + if audio.dtype == np.int16: + audio = audio.astype("float32") + bits = np.iinfo(np.int16).bits + audio = audio / (2**(bits - 1)) + return audio + + def extract_feat(self, samples, sample_rate): + """extract feat + + Args: + samples (numpy.array): numpy.float32 + sample_rate (int): sample rate + + Returns: + x_chunk (numpy.array): shape[B, T, D] + x_chunk_lens (numpy.array): shape[B] + """ + # pcm16 -> pcm 32 + samples = self._pcm16to32(samples) + + # read audio + speech_segment = SpeechSegment.from_pcm( + samples, sample_rate, transcript=" ") + # audio augment + self.collate_fn_test.augmentation.transform_audio(speech_segment) + + # extract speech feature + spectrum, transcript_part = self.collate_fn_test._speech_featurizer.featurize( + speech_segment, self.collate_fn_test.keep_transcription_text) + # CMVN spectrum + if self.collate_fn_test._normalizer: + spectrum = self.collate_fn_test._normalizer.apply(spectrum) + + # spectrum augment + audio = self.collate_fn_test.augmentation.transform_feature(spectrum) + + audio_len = audio.shape[0] + audio = paddle.to_tensor(audio, dtype='float32') + # audio_len = paddle.to_tensor(audio_len) + audio = paddle.unsqueeze(audio, axis=0) + + x_chunk = audio.numpy() + x_chunk_lens = np.array([audio_len]) + + return x_chunk, x_chunk_lens + + +class ASREngine(BaseEngine): + """ASR server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + super(ASREngine, self).__init__() + + def init(self, config: dict) -> bool: + """init engine resource + + Args: + config_file (str): config file + + Returns: + bool: init failed or success + """ + self.input = None + self.output = "" + self.executor = ASRServerExecutor() + self.config = config + + self.executor._init_from_path( + model_type=self.config.model_type, + am_model=self.config.am_model, + am_params=self.config.am_params, + lang=self.config.lang, + sample_rate=self.config.sample_rate, + cfg_path=self.config.cfg_path, + decode_method=self.config.decode_method, + am_predictor_conf=self.config.am_predictor_conf) + + logger.info("Initialize ASR server engine successfully.") + return True + + def preprocess(self, samples, sample_rate): + """preprocess + + Args: + samples (numpy.array): numpy.float32 + sample_rate (int): sample rate + + Returns: + x_chunk (numpy.array): shape[B, T, D] + x_chunk_lens (numpy.array): shape[B] + """ + x_chunk, x_chunk_lens = self.executor.extract_feat(samples, sample_rate) + return x_chunk, x_chunk_lens + + def run(self, x_chunk, x_chunk_lens, decoder_chunk_size=1): + """run online engine + + Args: + x_chunk (numpy.array): shape[B, T, D] + x_chunk_lens (numpy.array): shape[B] + decoder_chunk_size(int) + """ + self.output = self.executor.decode_one_chunk(x_chunk, x_chunk_lens, + self.config.model_type) + + def postprocess(self): + """postprocess + """ + return self.output + + def reset(self): + """reset engine decoder and inference state + """ + self.executor.reset_decoder_and_chunk() + self.output = "" diff --git a/ernie-sat/paddlespeech/server/engine/asr/paddleinference/__init__.py b/ernie-sat/paddlespeech/server/engine/asr/paddleinference/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/paddleinference/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/asr/paddleinference/asr_engine.py b/ernie-sat/paddlespeech/server/engine/asr/paddleinference/asr_engine.py new file mode 100644 index 0000000..1925bf1 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/paddleinference/asr_engine.py @@ -0,0 +1,240 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import io +import os +import time +from typing import Optional + +import paddle +from yacs.config import CfgNode + +from paddlespeech.cli.asr.infer import ASRExecutor +from paddlespeech.cli.log import logger +from paddlespeech.cli.utils import MODEL_HOME +from paddlespeech.s2t.frontend.featurizer.text_featurizer import TextFeaturizer +from paddlespeech.s2t.modules.ctc import CTCDecoder +from paddlespeech.s2t.utils.utility import UpdateConfig +from paddlespeech.server.engine.base_engine import BaseEngine +from paddlespeech.server.utils.paddle_predictor import init_predictor +from paddlespeech.server.utils.paddle_predictor import run_model + +__all__ = ['ASREngine'] + +pretrained_models = { + "deepspeech2offline_aishell-zh-16k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/s2t/aishell/asr0/asr0_deepspeech2_aishell_ckpt_0.1.1.model.tar.gz', + 'md5': + '932c3593d62fe5c741b59b31318aa314', + 'cfg_path': + 'model.yaml', + 'ckpt_path': + 'exp/deepspeech2/checkpoints/avg_1', + 'model': + 'exp/deepspeech2/checkpoints/avg_1.jit.pdmodel', + 'params': + 'exp/deepspeech2/checkpoints/avg_1.jit.pdiparams', + 'lm_url': + 'https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm', + 'lm_md5': + '29e02312deb2e59b3c8686c7966d4fe3' + }, +} + + +class ASRServerExecutor(ASRExecutor): + def __init__(self): + super().__init__() + pass + + def _init_from_path(self, + model_type: str='wenetspeech', + am_model: Optional[os.PathLike]=None, + am_params: Optional[os.PathLike]=None, + lang: str='zh', + sample_rate: int=16000, + cfg_path: Optional[os.PathLike]=None, + decode_method: str='attention_rescoring', + am_predictor_conf: dict=None): + """ + Init model and other resources from a specific path. + """ + + if cfg_path is None or am_model is None or am_params is None: + sample_rate_str = '16k' if sample_rate == 16000 else '8k' + tag = model_type + '-' + lang + '-' + sample_rate_str + res_path = self._get_pretrained_path(tag) # wenetspeech_zh + self.res_path = res_path + self.cfg_path = os.path.join(res_path, + pretrained_models[tag]['cfg_path']) + + self.am_model = os.path.join(res_path, + pretrained_models[tag]['model']) + self.am_params = os.path.join(res_path, + pretrained_models[tag]['params']) + logger.info(res_path) + logger.info(self.cfg_path) + logger.info(self.am_model) + logger.info(self.am_params) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.am_model = os.path.abspath(am_model) + self.am_params = os.path.abspath(am_params) + self.res_path = os.path.dirname( + os.path.dirname(os.path.abspath(self.cfg_path))) + + #Init body. + self.config = CfgNode(new_allowed=True) + self.config.merge_from_file(self.cfg_path) + + with UpdateConfig(self.config): + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + from paddlespeech.s2t.io.collator import SpeechCollator + self.vocab = self.config.vocab_filepath + self.config.decode.lang_model_path = os.path.join( + MODEL_HOME, 'language_model', + self.config.decode.lang_model_path) + self.collate_fn_test = SpeechCollator.from_config(self.config) + self.text_feature = TextFeaturizer( + unit_type=self.config.unit_type, vocab=self.vocab) + + lm_url = pretrained_models[tag]['lm_url'] + lm_md5 = pretrained_models[tag]['lm_md5'] + self.download_lm( + lm_url, + os.path.dirname(self.config.decode.lang_model_path), lm_md5) + elif "conformer" in model_type or "transformer" in model_type or "wenetspeech" in model_type: + raise Exception("wrong type") + else: + raise Exception("wrong type") + + # AM predictor + self.am_predictor_conf = am_predictor_conf + self.am_predictor = init_predictor( + model_file=self.am_model, + params_file=self.am_params, + predictor_conf=self.am_predictor_conf) + + # decoder + self.decoder = CTCDecoder( + odim=self.config.output_dim, # is in vocab + enc_n_units=self.config.rnn_layer_size * 2, + blank_id=self.config.blank_id, + dropout_rate=0.0, + reduction=True, # sum + batch_average=True, # sum / batch_size + grad_norm_type=self.config.get('ctc_grad_norm_type', None)) + + @paddle.no_grad() + def infer(self, model_type: str): + """ + Model inference and result stored in self.output. + """ + cfg = self.config.decode + audio = self._inputs["audio"] + audio_len = self._inputs["audio_len"] + if "deepspeech2online" in model_type or "deepspeech2offline" in model_type: + decode_batch_size = audio.shape[0] + # init once + self.decoder.init_decoder( + decode_batch_size, self.text_feature.vocab_list, + cfg.decoding_method, cfg.lang_model_path, cfg.alpha, cfg.beta, + cfg.beam_size, cfg.cutoff_prob, cfg.cutoff_top_n, + cfg.num_proc_bsearch) + + output_data = run_model(self.am_predictor, + [audio.numpy(), audio_len.numpy()]) + + probs = output_data[0] + eouts_len = output_data[1] + + batch_size = probs.shape[0] + self.decoder.reset_decoder(batch_size=batch_size) + self.decoder.next(probs, eouts_len) + trans_best, trans_beam = self.decoder.decode() + + # self.model.decoder.del_decoder() + self._outputs["result"] = trans_best[0] + + elif "conformer" in model_type or "transformer" in model_type: + raise Exception("invalid model name") + else: + raise Exception("invalid model name") + + +class ASREngine(BaseEngine): + """ASR server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + super(ASREngine, self).__init__() + + def init(self, config: dict) -> bool: + """init engine resource + + Args: + config_file (str): config file + + Returns: + bool: init failed or success + """ + self.input = None + self.output = None + self.executor = ASRServerExecutor() + self.config = config + + self.executor._init_from_path( + model_type=self.config.model_type, + am_model=self.config.am_model, + am_params=self.config.am_params, + lang=self.config.lang, + sample_rate=self.config.sample_rate, + cfg_path=self.config.cfg_path, + decode_method=self.config.decode_method, + am_predictor_conf=self.config.am_predictor_conf) + + logger.info("Initialize ASR server engine successfully.") + return True + + def run(self, audio_data): + """engine run + + Args: + audio_data (bytes): base64.b64decode + """ + if self.executor._check( + io.BytesIO(audio_data), self.config.sample_rate, + self.config.force_yes): + logger.info("start running asr engine") + self.executor.preprocess(self.config.model_type, + io.BytesIO(audio_data)) + st = time.time() + self.executor.infer(self.config.model_type) + infer_time = time.time() - st + self.output = self.executor.postprocess() # Retrieve result of asr. + logger.info("end inferring asr engine") + else: + logger.info("file check failed!") + self.output = None + + logger.info("inference time: {}".format(infer_time)) + logger.info("asr engine type: paddle inference") + + def postprocess(self): + """postprocess + """ + return self.output diff --git a/ernie-sat/paddlespeech/server/engine/asr/python/__init__.py b/ernie-sat/paddlespeech/server/engine/asr/python/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/python/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/asr/python/asr_engine.py b/ernie-sat/paddlespeech/server/engine/asr/python/asr_engine.py new file mode 100644 index 0000000..e76c49a --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/asr/python/asr_engine.py @@ -0,0 +1,100 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import io +import time + +import paddle + +from paddlespeech.cli.asr.infer import ASRExecutor +from paddlespeech.cli.log import logger +from paddlespeech.server.engine.base_engine import BaseEngine + +__all__ = ['ASREngine'] + + +class ASRServerExecutor(ASRExecutor): + def __init__(self): + super().__init__() + pass + + +class ASREngine(BaseEngine): + """ASR server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + super(ASREngine, self).__init__() + + def init(self, config: dict) -> bool: + """init engine resource + + Args: + config_file (str): config file + + Returns: + bool: init failed or success + """ + self.input = None + self.output = None + self.executor = ASRServerExecutor() + self.config = config + try: + if self.config.device: + self.device = self.config.device + else: + self.device = paddle.get_device() + paddle.set_device(self.device) + except BaseException: + logger.error( + "Set device failed, please check if device is already used and the parameter 'device' in the yaml file" + ) + + self.executor._init_from_path( + self.config.model, self.config.lang, self.config.sample_rate, + self.config.cfg_path, self.config.decode_method, + self.config.ckpt_path) + + logger.info("Initialize ASR server engine successfully on device: %s." % + (self.device)) + return True + + def run(self, audio_data): + """engine run + + Args: + audio_data (bytes): base64.b64decode + """ + if self.executor._check( + io.BytesIO(audio_data), self.config.sample_rate, + self.config.force_yes): + logger.info("start run asr engine") + self.executor.preprocess(self.config.model, io.BytesIO(audio_data)) + st = time.time() + self.executor.infer(self.config.model) + infer_time = time.time() - st + self.output = self.executor.postprocess() # Retrieve result of asr. + else: + logger.info("file check failed!") + self.output = None + + logger.info("inference time: {}".format(infer_time)) + logger.info("asr engine type: python") + + def postprocess(self): + """postprocess + """ + return self.output diff --git a/ernie-sat/paddlespeech/server/engine/base_engine.py b/ernie-sat/paddlespeech/server/engine/base_engine.py new file mode 100644 index 0000000..0f020d1 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/base_engine.py @@ -0,0 +1,58 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from typing import Union + +from pattern_singleton import Singleton + +__all__ = ['BaseEngine'] + + +class BaseEngine(metaclass=Singleton): + """ + An base engine class + """ + + def __init__(self): + self._inputs = dict() + self._outputs = dict() + + def init(self, *args, **kwargs): + """ + init the engine + + Returns: + bool: true or false + """ + pass + + def postprocess(self, *args, **kwargs) -> Union[str, os.PathLike]: + """ + Output postprocess and return results. + This method get model output from self._outputs and convert it into human-readable results. + + Returns: + Union[str, os.PathLike]: Human-readable results such as texts and audio files. + """ + pass + + def run(self, *args, **kwargs) -> Union[str, os.PathLike]: + """ + Output postprocess and return results. + This method get model output from self._outputs and convert it into human-readable results. + + Returns: + Union[str, os.PathLike]: Human-readable results such as texts and audio files. + """ + pass diff --git a/ernie-sat/paddlespeech/server/engine/cls/__init__.py b/ernie-sat/paddlespeech/server/engine/cls/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/cls/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/cls/paddleinference/__init__.py b/ernie-sat/paddlespeech/server/engine/cls/paddleinference/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/cls/paddleinference/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/cls/paddleinference/cls_engine.py b/ernie-sat/paddlespeech/server/engine/cls/paddleinference/cls_engine.py new file mode 100644 index 0000000..3982eff --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/cls/paddleinference/cls_engine.py @@ -0,0 +1,224 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import io +import os +import time +from typing import Optional + +import numpy as np +import paddle +import yaml + +from paddlespeech.cli.cls.infer import CLSExecutor +from paddlespeech.cli.log import logger +from paddlespeech.cli.utils import download_and_decompress +from paddlespeech.cli.utils import MODEL_HOME +from paddlespeech.server.engine.base_engine import BaseEngine +from paddlespeech.server.utils.paddle_predictor import init_predictor +from paddlespeech.server.utils.paddle_predictor import run_model + +__all__ = ['CLSEngine'] + +pretrained_models = { + "panns_cnn6-32k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/cls/inference_model/panns_cnn6_static.tar.gz', + 'md5': + 'da087c31046d23281d8ec5188c1967da', + 'cfg_path': + 'panns.yaml', + 'model_path': + 'inference.pdmodel', + 'params_path': + 'inference.pdiparams', + 'label_file': + 'audioset_labels.txt', + }, + "panns_cnn10-32k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/cls/inference_model/panns_cnn10_static.tar.gz', + 'md5': + '5460cc6eafbfaf0f261cc75b90284ae1', + 'cfg_path': + 'panns.yaml', + 'model_path': + 'inference.pdmodel', + 'params_path': + 'inference.pdiparams', + 'label_file': + 'audioset_labels.txt', + }, + "panns_cnn14-32k": { + 'url': + 'https://paddlespeech.bj.bcebos.com/cls/inference_model/panns_cnn14_static.tar.gz', + 'md5': + 'ccc80b194821274da79466862b2ab00f', + 'cfg_path': + 'panns.yaml', + 'model_path': + 'inference.pdmodel', + 'params_path': + 'inference.pdiparams', + 'label_file': + 'audioset_labels.txt', + }, +} + + +class CLSServerExecutor(CLSExecutor): + def __init__(self): + super().__init__() + pass + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + support_models = list(pretrained_models.keys()) + assert tag in pretrained_models, 'The model "{}" you want to use has not been supported, please choose other models.\nThe support models includes:\n\t\t{}\n'.format( + tag, '\n\t\t'.join(support_models)) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + + return decompressed_path + + def _init_from_path( + self, + model_type: str='panns_cnn14', + cfg_path: Optional[os.PathLike]=None, + model_path: Optional[os.PathLike]=None, + params_path: Optional[os.PathLike]=None, + label_file: Optional[os.PathLike]=None, + predictor_conf: dict=None, ): + """ + Init model and other resources from a specific path. + """ + + if cfg_path is None or model_path is None or params_path is None or label_file is None: + tag = model_type + '-' + '32k' + self.res_path = self._get_pretrained_path(tag) + self.cfg_path = os.path.join(self.res_path, + pretrained_models[tag]['cfg_path']) + self.model_path = os.path.join(self.res_path, + pretrained_models[tag]['model_path']) + self.params_path = os.path.join( + self.res_path, pretrained_models[tag]['params_path']) + self.label_file = os.path.join(self.res_path, + pretrained_models[tag]['label_file']) + else: + self.cfg_path = os.path.abspath(cfg_path) + self.model_path = os.path.abspath(model_path) + self.params_path = os.path.abspath(params_path) + self.label_file = os.path.abspath(label_file) + + logger.info(self.cfg_path) + logger.info(self.model_path) + logger.info(self.params_path) + logger.info(self.label_file) + + # config + with open(self.cfg_path, 'r') as f: + self._conf = yaml.safe_load(f) + logger.info("Read cfg file successfully.") + + # labels + self._label_list = [] + with open(self.label_file, 'r') as f: + for line in f: + self._label_list.append(line.strip()) + logger.info("Read label file successfully.") + + # Create predictor + self.predictor_conf = predictor_conf + self.predictor = init_predictor( + model_file=self.model_path, + params_file=self.params_path, + predictor_conf=self.predictor_conf) + logger.info("Create predictor successfully.") + + @paddle.no_grad() + def infer(self): + """ + Model inference and result stored in self.output. + """ + output = run_model(self.predictor, [self._inputs['feats'].numpy()]) + self._outputs['logits'] = output[0] + + +class CLSEngine(BaseEngine): + """CLS server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + super(CLSEngine, self).__init__() + + def init(self, config: dict) -> bool: + """init engine resource + + Args: + config_file (str): config file + + Returns: + bool: init failed or success + """ + self.executor = CLSServerExecutor() + self.config = config + self.executor._init_from_path( + self.config.model_type, self.config.cfg_path, + self.config.model_path, self.config.params_path, + self.config.label_file, self.config.predictor_conf) + + logger.info("Initialize CLS server engine successfully.") + return True + + def run(self, audio_data): + """engine run + + Args: + audio_data (bytes): base64.b64decode + """ + + self.executor.preprocess(io.BytesIO(audio_data)) + st = time.time() + self.executor.infer() + infer_time = time.time() - st + + logger.info("inference time: {}".format(infer_time)) + logger.info("cls engine type: inference") + + def postprocess(self, topk: int): + """postprocess + """ + assert topk <= len(self.executor._label_list + ), 'Value of topk is larger than number of labels.' + + result = np.squeeze(self.executor._outputs['logits'], axis=0) + topk_idx = (-result).argsort()[:topk] + topk_results = [] + for idx in topk_idx: + res = {} + label, score = self.executor._label_list[idx], result[idx] + res['class_name'] = label + res['prob'] = score + topk_results.append(res) + + return topk_results diff --git a/ernie-sat/paddlespeech/server/engine/cls/python/__init__.py b/ernie-sat/paddlespeech/server/engine/cls/python/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/cls/python/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/cls/python/cls_engine.py b/ernie-sat/paddlespeech/server/engine/cls/python/cls_engine.py new file mode 100644 index 0000000..1a975b0 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/cls/python/cls_engine.py @@ -0,0 +1,124 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import io +import time +from typing import List + +import paddle + +from paddlespeech.cli.cls.infer import CLSExecutor +from paddlespeech.cli.log import logger +from paddlespeech.server.engine.base_engine import BaseEngine + +__all__ = ['CLSEngine'] + + +class CLSServerExecutor(CLSExecutor): + def __init__(self): + super().__init__() + pass + + def get_topk_results(self, topk: int) -> List: + assert topk <= len( + self._label_list), 'Value of topk is larger than number of labels.' + + result = self._outputs['logits'].squeeze(0).numpy() + topk_idx = (-result).argsort()[:topk] + res = {} + topk_results = [] + for idx in topk_idx: + label, score = self._label_list[idx], result[idx] + res['class'] = label + res['prob'] = score + topk_results.append(res) + return topk_results + + +class CLSEngine(BaseEngine): + """CLS server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + super(CLSEngine, self).__init__() + + def init(self, config: dict) -> bool: + """init engine resource + + Args: + config_file (str): config file + + Returns: + bool: init failed or success + """ + self.input = None + self.output = None + self.executor = CLSServerExecutor() + self.config = config + try: + if self.config.device: + self.device = self.config.device + else: + self.device = paddle.get_device() + paddle.set_device(self.device) + except BaseException: + logger.error( + "Set device failed, please check if device is already used and the parameter 'device' in the yaml file" + ) + + try: + self.executor._init_from_path( + self.config.model, self.config.cfg_path, self.config.ckpt_path, + self.config.label_file) + except BaseException: + logger.error("Initialize CLS server engine Failed.") + return False + + logger.info("Initialize CLS server engine successfully on device: %s." % + (self.device)) + return True + + def run(self, audio_data): + """engine run + + Args: + audio_data (bytes): base64.b64decode + """ + self.executor.preprocess(io.BytesIO(audio_data)) + st = time.time() + self.executor.infer() + infer_time = time.time() - st + + logger.info("inference time: {}".format(infer_time)) + logger.info("cls engine type: python") + + def postprocess(self, topk: int): + """postprocess + """ + assert topk <= len(self.executor._label_list + ), 'Value of topk is larger than number of labels.' + + result = self.executor._outputs['logits'].squeeze(0).numpy() + topk_idx = (-result).argsort()[:topk] + topk_results = [] + for idx in topk_idx: + res = {} + label, score = self.executor._label_list[idx], result[idx] + res['class_name'] = label + res['prob'] = score + topk_results.append(res) + + return topk_results diff --git a/ernie-sat/paddlespeech/server/engine/engine_factory.py b/ernie-sat/paddlespeech/server/engine/engine_factory.py new file mode 100644 index 0000000..2a39fb7 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/engine_factory.py @@ -0,0 +1,44 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Text + +__all__ = ['EngineFactory'] + + +class EngineFactory(object): + @staticmethod + def get_engine(engine_name: Text, engine_type: Text): + if engine_name == 'asr' and engine_type == 'inference': + from paddlespeech.server.engine.asr.paddleinference.asr_engine import ASREngine + return ASREngine() + elif engine_name == 'asr' and engine_type == 'python': + from paddlespeech.server.engine.asr.python.asr_engine import ASREngine + return ASREngine() + elif engine_name == 'asr' and engine_type == 'online': + from paddlespeech.server.engine.asr.online.asr_engine import ASREngine + return ASREngine() + elif engine_name == 'tts' and engine_type == 'inference': + from paddlespeech.server.engine.tts.paddleinference.tts_engine import TTSEngine + return TTSEngine() + elif engine_name == 'tts' and engine_type == 'python': + from paddlespeech.server.engine.tts.python.tts_engine import TTSEngine + return TTSEngine() + elif engine_name == 'cls' and engine_type == 'inference': + from paddlespeech.server.engine.cls.paddleinference.cls_engine import CLSEngine + return CLSEngine() + elif engine_name == 'cls' and engine_type == 'python': + from paddlespeech.server.engine.cls.python.cls_engine import CLSEngine + return CLSEngine() + else: + return None diff --git a/ernie-sat/paddlespeech/server/engine/engine_pool.py b/ernie-sat/paddlespeech/server/engine/engine_pool.py new file mode 100644 index 0000000..9de7356 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/engine_pool.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.server.engine.engine_factory import EngineFactory + +# global value +ENGINE_POOL = {} + + +def get_engine_pool() -> dict: + """ Get engine pool + """ + global ENGINE_POOL + return ENGINE_POOL + + +def init_engine_pool(config) -> bool: + """ Init engine pool + """ + global ENGINE_POOL + + for engine_and_type in config.engine_list: + engine = engine_and_type.split("_")[0] + engine_type = engine_and_type.split("_")[1] + ENGINE_POOL[engine] = EngineFactory.get_engine( + engine_name=engine, engine_type=engine_type) + if not ENGINE_POOL[engine].init(config=config[engine_and_type]): + return False + + return True diff --git a/ernie-sat/paddlespeech/server/engine/tts/__init__.py b/ernie-sat/paddlespeech/server/engine/tts/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/tts/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/tts/paddleinference/__init__.py b/ernie-sat/paddlespeech/server/engine/tts/paddleinference/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/tts/paddleinference/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/tts/paddleinference/tts_engine.py b/ernie-sat/paddlespeech/server/engine/tts/paddleinference/tts_engine.py new file mode 100644 index 0000000..db8813b --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/tts/paddleinference/tts_engine.py @@ -0,0 +1,534 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import io +import os +import time +from typing import Optional + +import librosa +import numpy as np +import paddle +import soundfile as sf +from scipy.io import wavfile + +from paddlespeech.cli.log import logger +from paddlespeech.cli.tts.infer import TTSExecutor +from paddlespeech.cli.utils import download_and_decompress +from paddlespeech.cli.utils import MODEL_HOME +from paddlespeech.server.engine.base_engine import BaseEngine +from paddlespeech.server.utils.audio_process import change_speed +from paddlespeech.server.utils.errors import ErrorCode +from paddlespeech.server.utils.exception import ServerBaseException +from paddlespeech.server.utils.paddle_predictor import init_predictor +from paddlespeech.server.utils.paddle_predictor import run_model +from paddlespeech.t2s.frontend import English +from paddlespeech.t2s.frontend.zh_frontend import Frontend + +__all__ = ['TTSEngine'] + +# Static model applied on paddle inference +pretrained_models = { + # speedyspeech + "speedyspeech_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/speedyspeech/speedyspeech_nosil_baker_static_0.5.zip', + 'md5': + 'f10cbdedf47dc7a9668d2264494e1823', + 'model': + 'speedyspeech_csmsc.pdmodel', + 'params': + 'speedyspeech_csmsc.pdiparams', + 'phones_dict': + 'phone_id_map.txt', + 'tones_dict': + 'tone_id_map.txt', + 'sample_rate': + 24000, + }, + # fastspeech2 + "fastspeech2_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_nosil_baker_static_0.4.zip', + 'md5': + '9788cd9745e14c7a5d12d32670b2a5a7', + 'model': + 'fastspeech2_csmsc.pdmodel', + 'params': + 'fastspeech2_csmsc.pdiparams', + 'phones_dict': + 'phone_id_map.txt', + 'sample_rate': + 24000, + }, + # pwgan + "pwgan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/pwgan/pwg_baker_static_0.4.zip', + 'md5': + 'e3504aed9c5a290be12d1347836d2742', + 'model': + 'pwgan_csmsc.pdmodel', + 'params': + 'pwgan_csmsc.pdiparams', + 'sample_rate': + 24000, + }, + # mb_melgan + "mb_melgan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/mb_melgan/mb_melgan_csmsc_static_0.1.1.zip', + 'md5': + 'ac6eee94ba483421d750433f4c3b8d36', + 'model': + 'mb_melgan_csmsc.pdmodel', + 'params': + 'mb_melgan_csmsc.pdiparams', + 'sample_rate': + 24000, + }, + # hifigan + "hifigan_csmsc-zh": { + 'url': + 'https://paddlespeech.bj.bcebos.com/Parakeet/released_models/hifigan/hifigan_csmsc_static_0.1.1.zip', + 'md5': + '7edd8c436b3a5546b3a7cb8cff9d5a0c', + 'model': + 'hifigan_csmsc.pdmodel', + 'params': + 'hifigan_csmsc.pdiparams', + 'sample_rate': + 24000, + }, +} + + +class TTSServerExecutor(TTSExecutor): + def __init__(self): + super().__init__() + pass + + def _get_pretrained_path(self, tag: str) -> os.PathLike: + """ + Download and returns pretrained resources path of current task. + """ + assert tag in pretrained_models, 'Can not find pretrained resources of {}.'.format( + tag) + + res_path = os.path.join(MODEL_HOME, tag) + decompressed_path = download_and_decompress(pretrained_models[tag], + res_path) + decompressed_path = os.path.abspath(decompressed_path) + logger.info( + 'Use pretrained model stored in: {}'.format(decompressed_path)) + return decompressed_path + + def _init_from_path( + self, + am: str='fastspeech2_csmsc', + am_model: Optional[os.PathLike]=None, + am_params: Optional[os.PathLike]=None, + am_sample_rate: int=24000, + phones_dict: Optional[os.PathLike]=None, + tones_dict: Optional[os.PathLike]=None, + speaker_dict: Optional[os.PathLike]=None, + voc: str='pwgan_csmsc', + voc_model: Optional[os.PathLike]=None, + voc_params: Optional[os.PathLike]=None, + voc_sample_rate: int=24000, + lang: str='zh', + am_predictor_conf: dict=None, + voc_predictor_conf: dict=None, ): + """ + Init model and other resources from a specific path. + """ + if hasattr(self, 'am_predictor') and hasattr(self, 'voc_predictor'): + logger.info('Models had been initialized.') + return + # am + am_tag = am + '-' + lang + if am_model is None or am_params is None or phones_dict is None: + am_res_path = self._get_pretrained_path(am_tag) + self.am_res_path = am_res_path + self.am_model = os.path.join(am_res_path, + pretrained_models[am_tag]['model']) + self.am_params = os.path.join(am_res_path, + pretrained_models[am_tag]['params']) + # must have phones_dict in acoustic + self.phones_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['phones_dict']) + self.am_sample_rate = pretrained_models[am_tag]['sample_rate'] + + logger.info(am_res_path) + logger.info(self.am_model) + logger.info(self.am_params) + else: + self.am_model = os.path.abspath(am_model) + self.am_params = os.path.abspath(am_params) + self.phones_dict = os.path.abspath(phones_dict) + self.am_sample_rate = am_sample_rate + self.am_res_path = os.path.dirname(os.path.abspath(self.am_model)) + logger.info("self.phones_dict: {}".format(self.phones_dict)) + + # for speedyspeech + self.tones_dict = None + if 'tones_dict' in pretrained_models[am_tag]: + self.tones_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['tones_dict']) + if tones_dict: + self.tones_dict = tones_dict + + # for multi speaker fastspeech2 + self.speaker_dict = None + if 'speaker_dict' in pretrained_models[am_tag]: + self.speaker_dict = os.path.join( + am_res_path, pretrained_models[am_tag]['speaker_dict']) + if speaker_dict: + self.speaker_dict = speaker_dict + + # voc + voc_tag = voc + '-' + lang + if voc_model is None or voc_params is None: + voc_res_path = self._get_pretrained_path(voc_tag) + self.voc_res_path = voc_res_path + self.voc_model = os.path.join(voc_res_path, + pretrained_models[voc_tag]['model']) + self.voc_params = os.path.join(voc_res_path, + pretrained_models[voc_tag]['params']) + self.voc_sample_rate = pretrained_models[voc_tag]['sample_rate'] + logger.info(voc_res_path) + logger.info(self.voc_model) + logger.info(self.voc_params) + else: + self.voc_model = os.path.abspath(voc_model) + self.voc_params = os.path.abspath(voc_params) + self.voc_sample_rate = voc_sample_rate + self.voc_res_path = os.path.dirname(os.path.abspath(self.voc_model)) + + assert ( + self.voc_sample_rate == self.am_sample_rate + ), "The sample rate of AM and Vocoder model are different, please check model." + + # Init body. + with open(self.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + logger.info("vocab_size: {}".format(vocab_size)) + + tone_size = None + if self.tones_dict: + with open(self.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + logger.info("tone_size: {}".format(tone_size)) + + spk_num = None + if self.speaker_dict: + with open(self.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + logger.info("spk_num: {}".format(spk_num)) + + # frontend + if lang == 'zh': + self.frontend = Frontend( + phone_vocab_path=self.phones_dict, + tone_vocab_path=self.tones_dict) + + elif lang == 'en': + self.frontend = English(phone_vocab_path=self.phones_dict) + logger.info("frontend done!") + + # Create am predictor + self.am_predictor_conf = am_predictor_conf + self.am_predictor = init_predictor( + model_file=self.am_model, + params_file=self.am_params, + predictor_conf=self.am_predictor_conf) + logger.info("Create AM predictor successfully.") + + # Create voc predictor + self.voc_predictor_conf = voc_predictor_conf + self.voc_predictor = init_predictor( + model_file=self.voc_model, + params_file=self.voc_params, + predictor_conf=self.voc_predictor_conf) + logger.info("Create Vocoder predictor successfully.") + + @paddle.no_grad() + def infer(self, + text: str, + lang: str='zh', + am: str='fastspeech2_csmsc', + spk_id: int=0): + """ + Model inference and result stored in self.output. + """ + am_name = am[:am.rindex('_')] + am_dataset = am[am.rindex('_') + 1:] + get_tone_ids = False + merge_sentences = False + frontend_st = time.time() + if am_name == 'speedyspeech': + get_tone_ids = True + if lang == 'zh': + input_ids = self.frontend.get_input_ids( + text, + merge_sentences=merge_sentences, + get_tone_ids=get_tone_ids) + phone_ids = input_ids["phone_ids"] + if get_tone_ids: + tone_ids = input_ids["tone_ids"] + elif lang == 'en': + input_ids = self.frontend.get_input_ids( + text, merge_sentences=merge_sentences) + phone_ids = input_ids["phone_ids"] + else: + logger.error("lang should in {'zh', 'en'}!") + self.frontend_time = time.time() - frontend_st + + self.am_time = 0 + self.voc_time = 0 + flags = 0 + for i in range(len(phone_ids)): + am_st = time.time() + part_phone_ids = phone_ids[i] + # am + if am_name == 'speedyspeech': + part_tone_ids = tone_ids[i] + am_result = run_model( + self.am_predictor, + [part_phone_ids.numpy(), part_tone_ids.numpy()]) + mel = am_result[0] + + # fastspeech2 + else: + # multi speaker do not have static model + if am_dataset in {"aishell3", "vctk"}: + pass + else: + am_result = run_model(self.am_predictor, + [part_phone_ids.numpy()]) + mel = am_result[0] + self.am_time += (time.time() - am_st) + + # voc + voc_st = time.time() + voc_result = run_model(self.voc_predictor, [mel]) + wav = voc_result[0] + wav = paddle.to_tensor(wav) + + if flags == 0: + wav_all = wav + flags = 1 + else: + wav_all = paddle.concat([wav_all, wav]) + self.voc_time += (time.time() - voc_st) + self._outputs['wav'] = wav_all + + +class TTSEngine(BaseEngine): + """TTS server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self): + """Initialize TTS server engine + """ + super(TTSEngine, self).__init__() + + def init(self, config: dict) -> bool: + self.executor = TTSServerExecutor() + + self.config = config + self.executor._init_from_path( + am=self.config.am, + am_model=self.config.am_model, + am_params=self.config.am_params, + am_sample_rate=self.config.am_sample_rate, + phones_dict=self.config.phones_dict, + tones_dict=self.config.tones_dict, + speaker_dict=self.config.speaker_dict, + voc=self.config.voc, + voc_model=self.config.voc_model, + voc_params=self.config.voc_params, + voc_sample_rate=self.config.voc_sample_rate, + lang=self.config.lang, + am_predictor_conf=self.config.am_predictor_conf, + voc_predictor_conf=self.config.voc_predictor_conf, ) + + logger.info("Initialize TTS server engine successfully.") + return True + + def postprocess(self, + wav, + original_fs: int, + target_fs: int=0, + volume: float=1.0, + speed: float=1.0, + audio_path: str=None): + """Post-processing operations, including speech, volume, sample rate, save audio file + + Args: + wav (numpy(float)): Synthesized audio sample points + original_fs (int): original audio sample rate + target_fs (int): target audio sample rate + volume (float): target volume + speed (float): target speed + + Raises: + ServerBaseException: Throws an exception if the change speed unsuccessfully. + + Returns: + target_fs: target sample rate for synthesized audio. + wav_base64: The base64 format of the synthesized audio. + """ + + # transform sample_rate + if target_fs == 0 or target_fs > original_fs: + target_fs = original_fs + wav_tar_fs = wav + logger.info( + "The sample rate of synthesized audio is the same as model, which is {}Hz". + format(original_fs)) + else: + wav_tar_fs = librosa.resample( + np.squeeze(wav), original_fs, target_fs) + logger.info( + "The sample rate of model is {}Hz and the target sample rate is {}Hz. Converting the sample rate of the synthesized audio successfully.". + format(original_fs, target_fs)) + # transform volume + wav_vol = wav_tar_fs * volume + logger.info("Transform the volume of the audio successfully.") + + # transform speed + try: # windows not support soxbindings + wav_speed = change_speed(wav_vol, speed, target_fs) + logger.info("Transform the speed of the audio successfully.") + except ServerBaseException: + raise ServerBaseException( + ErrorCode.SERVER_INTERNAL_ERR, + "Failed to transform speed. Can not install soxbindings on your system. \ + You need to set speed value 1.0.") + except BaseException: + logger.error("Failed to transform speed.") + + # wav to base64 + buf = io.BytesIO() + wavfile.write(buf, target_fs, wav_speed) + base64_bytes = base64.b64encode(buf.read()) + wav_base64 = base64_bytes.decode('utf-8') + logger.info("Audio to string successfully.") + + # save audio + if audio_path is not None: + if audio_path.endswith(".wav"): + sf.write(audio_path, wav_speed, target_fs) + elif audio_path.endswith(".pcm"): + wav_norm = wav_speed * (32767 / max(0.001, + np.max(np.abs(wav_speed)))) + with open(audio_path, "wb") as f: + f.write(wav_norm.astype(np.int16)) + logger.info("Save audio to {} successfully.".format(audio_path)) + else: + logger.info("There is no need to save audio.") + + return target_fs, wav_base64 + + def run(self, + sentence: str, + spk_id: int=0, + speed: float=1.0, + volume: float=1.0, + sample_rate: int=0, + save_path: str=None): + """get the result of the server response + + Args: + sentence (str): sentence to be synthesized + spk_id (int, optional): speaker id. Defaults to 0. + speed (float, optional): audio speed, 0 < speed <=3.0. Defaults to 1.0. + volume (float, optional): The volume relative to the audio synthesized by the model, + 0 < volume <=3.0. Defaults to 1.0. + sample_rate (int, optional): Set the sample rate of the synthesized audio. + 0 represents the sample rate for model synthesis. Defaults to 0. + save_path (str, optional): The save path of the synthesized audio. Defaults to None. + + Raises: + ServerBaseException: Throws an exception if tts inference unsuccessfully. + ServerBaseException: Throws an exception if postprocess unsuccessfully. + + Returns: + lang: model language + target_sample_rate: target sample rate for synthesized audio. + wav_base64: The base64 format of the synthesized audio. + """ + + lang = self.config.lang + + try: + infer_st = time.time() + self.executor.infer( + text=sentence, lang=lang, am=self.config.am, spk_id=spk_id) + infer_et = time.time() + infer_time = infer_et - infer_st + + except ServerBaseException: + raise ServerBaseException(ErrorCode.SERVER_INTERNAL_ERR, + "tts infer failed.") + except BaseException: + logger.error("tts infer failed.") + + try: + postprocess_st = time.time() + target_sample_rate, wav_base64 = self.postprocess( + wav=self.executor._outputs['wav'].numpy(), + original_fs=self.executor.am_sample_rate, + target_fs=sample_rate, + volume=volume, + speed=speed, + audio_path=save_path) + postprocess_et = time.time() + postprocess_time = postprocess_et - postprocess_st + duration = len(self.executor._outputs['wav'] + .numpy()) / self.executor.am_sample_rate + rtf = infer_time / duration + + except ServerBaseException: + raise ServerBaseException(ErrorCode.SERVER_INTERNAL_ERR, + "tts postprocess failed.") + except BaseException: + logger.error("tts postprocess failed.") + + logger.info("AM model: {}".format(self.config.am)) + logger.info("Vocoder model: {}".format(self.config.voc)) + logger.info("Language: {}".format(lang)) + logger.info("tts engine type: paddle inference") + + logger.info("audio duration: {}".format(duration)) + logger.info( + "frontend inference time: {}".format(self.executor.frontend_time)) + logger.info("AM inference time: {}".format(self.executor.am_time)) + logger.info("Vocoder inference time: {}".format(self.executor.voc_time)) + logger.info("total inference time: {}".format(infer_time)) + logger.info( + "postprocess (change speed, volume, target sample rate) time: {}". + format(postprocess_time)) + logger.info("total generate audio time: {}".format(infer_time + + postprocess_time)) + logger.info("RTF: {}".format(rtf)) + + return lang, target_sample_rate, duration, wav_base64 diff --git a/ernie-sat/paddlespeech/server/engine/tts/python/__init__.py b/ernie-sat/paddlespeech/server/engine/tts/python/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/tts/python/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/engine/tts/python/tts_engine.py b/ernie-sat/paddlespeech/server/engine/tts/python/tts_engine.py new file mode 100644 index 0000000..f153f60 --- /dev/null +++ b/ernie-sat/paddlespeech/server/engine/tts/python/tts_engine.py @@ -0,0 +1,253 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import io +import time + +import librosa +import numpy as np +import paddle +import soundfile as sf +from scipy.io import wavfile + +from paddlespeech.cli.log import logger +from paddlespeech.cli.tts.infer import TTSExecutor +from paddlespeech.server.engine.base_engine import BaseEngine +from paddlespeech.server.utils.audio_process import change_speed +from paddlespeech.server.utils.errors import ErrorCode +from paddlespeech.server.utils.exception import ServerBaseException + +__all__ = ['TTSEngine'] + + +class TTSServerExecutor(TTSExecutor): + def __init__(self): + super().__init__() + pass + + +class TTSEngine(BaseEngine): + """TTS server engine + + Args: + metaclass: Defaults to Singleton. + """ + + def __init__(self, name=None): + """Initialize TTS server engine + """ + super(TTSEngine, self).__init__() + + def init(self, config: dict) -> bool: + self.executor = TTSServerExecutor() + + try: + self.config = config + if self.config.device: + self.device = self.config.device + else: + self.device = paddle.get_device() + paddle.set_device(self.device) + except BaseException: + logger.error( + "Set device failed, please check if device is already used and the parameter 'device' in the yaml file" + ) + logger.error("Initialize TTS server engine Failed on device: %s." % + (self.device)) + return False + + try: + self.executor._init_from_path( + am=self.config.am, + am_config=self.config.am_config, + am_ckpt=self.config.am_ckpt, + am_stat=self.config.am_stat, + phones_dict=self.config.phones_dict, + tones_dict=self.config.tones_dict, + speaker_dict=self.config.speaker_dict, + voc=self.config.voc, + voc_config=self.config.voc_config, + voc_ckpt=self.config.voc_ckpt, + voc_stat=self.config.voc_stat, + lang=self.config.lang) + except BaseException: + logger.error("Failed to get model related files.") + logger.error("Initialize TTS server engine Failed on device: %s." % + (self.device)) + return False + + logger.info("Initialize TTS server engine successfully on device: %s." % + (self.device)) + return True + + def postprocess(self, + wav, + original_fs: int, + target_fs: int=0, + volume: float=1.0, + speed: float=1.0, + audio_path: str=None): + """Post-processing operations, including speech, volume, sample rate, save audio file + + Args: + wav (numpy(float)): Synthesized audio sample points + original_fs (int): original audio sample rate + target_fs (int): target audio sample rate + volume (float): target volume + speed (float): target speed + + Raises: + ServerBaseException: Throws an exception if the change speed unsuccessfully. + + Returns: + target_fs: target sample rate for synthesized audio. + wav_base64: The base64 format of the synthesized audio. + """ + + # transform sample_rate + if target_fs == 0 or target_fs > original_fs: + target_fs = original_fs + wav_tar_fs = wav + logger.info( + "The sample rate of synthesized audio is the same as model, which is {}Hz". + format(original_fs)) + else: + wav_tar_fs = librosa.resample( + np.squeeze(wav), original_fs, target_fs) + logger.info( + "The sample rate of model is {}Hz and the target sample rate is {}Hz. Converting the sample rate of the synthesized audio successfully.". + format(original_fs, target_fs)) + # transform volume + wav_vol = wav_tar_fs * volume + logger.info("Transform the volume of the audio successfully.") + + # transform speed + try: # windows not support soxbindings + wav_speed = change_speed(wav_vol, speed, target_fs) + logger.info("Transform the speed of the audio successfully.") + except ServerBaseException: + raise ServerBaseException( + ErrorCode.SERVER_INTERNAL_ERR, + "Failed to transform speed. Can not install soxbindings on your system. \ + You need to set speed value 1.0.") + except BaseException: + logger.error("Failed to transform speed.") + + # wav to base64 + buf = io.BytesIO() + wavfile.write(buf, target_fs, wav_speed) + base64_bytes = base64.b64encode(buf.read()) + wav_base64 = base64_bytes.decode('utf-8') + logger.info("Audio to string successfully.") + + # save audio + if audio_path is not None: + if audio_path.endswith(".wav"): + sf.write(audio_path, wav_speed, target_fs) + elif audio_path.endswith(".pcm"): + wav_norm = wav_speed * (32767 / max(0.001, + np.max(np.abs(wav_speed)))) + with open(audio_path, "wb") as f: + f.write(wav_norm.astype(np.int16)) + logger.info("Save audio to {} successfully.".format(audio_path)) + else: + logger.info("There is no need to save audio.") + + return target_fs, wav_base64 + + def run(self, + sentence: str, + spk_id: int=0, + speed: float=1.0, + volume: float=1.0, + sample_rate: int=0, + save_path: str=None): + """ run include inference and postprocess. + + Args: + sentence (str): text to be synthesized + spk_id (int, optional): speaker id for multi-speaker speech synthesis. Defaults to 0. + speed (float, optional): speed. Defaults to 1.0. + volume (float, optional): volume. Defaults to 1.0. + sample_rate (int, optional): target sample rate for synthesized audio, + 0 means the same as the model sampling rate. Defaults to 0. + save_path (str, optional): The save path of the synthesized audio. + None means do not save audio. Defaults to None. + + Raises: + ServerBaseException: Throws an exception if tts inference unsuccessfully. + ServerBaseException: Throws an exception if postprocess unsuccessfully. + + Returns: + lang: model language + target_sample_rate: target sample rate for synthesized audio. + wav_base64: The base64 format of the synthesized audio. + """ + + lang = self.config.lang + + try: + infer_st = time.time() + self.executor.infer( + text=sentence, lang=lang, am=self.config.am, spk_id=spk_id) + infer_et = time.time() + infer_time = infer_et - infer_st + duration = len(self.executor._outputs['wav'] + .numpy()) / self.executor.am_config.fs + rtf = infer_time / duration + + except ServerBaseException: + raise ServerBaseException(ErrorCode.SERVER_INTERNAL_ERR, + "tts infer failed.") + except BaseException: + logger.error("tts infer failed.") + + try: + postprocess_st = time.time() + target_sample_rate, wav_base64 = self.postprocess( + wav=self.executor._outputs['wav'].numpy(), + original_fs=self.executor.am_config.fs, + target_fs=sample_rate, + volume=volume, + speed=speed, + audio_path=save_path) + postprocess_et = time.time() + postprocess_time = postprocess_et - postprocess_st + + except ServerBaseException: + raise ServerBaseException(ErrorCode.SERVER_INTERNAL_ERR, + "tts postprocess failed.") + except BaseException: + logger.error("tts postprocess failed.") + + logger.info("AM model: {}".format(self.config.am)) + logger.info("Vocoder model: {}".format(self.config.voc)) + logger.info("Language: {}".format(lang)) + logger.info("tts engine type: python") + + logger.info("audio duration: {}".format(duration)) + logger.info( + "frontend inference time: {}".format(self.executor.frontend_time)) + logger.info("AM inference time: {}".format(self.executor.am_time)) + logger.info("Vocoder inference time: {}".format(self.executor.voc_time)) + logger.info("total inference time: {}".format(infer_time)) + logger.info( + "postprocess (change speed, volume, target sample rate) time: {}". + format(postprocess_time)) + logger.info("total generate audio time: {}".format(infer_time + + postprocess_time)) + logger.info("RTF: {}".format(rtf)) + logger.info("device: {}".format(self.device)) + + return lang, target_sample_rate, duration, wav_base64 diff --git a/ernie-sat/paddlespeech/server/entry.py b/ernie-sat/paddlespeech/server/entry.py new file mode 100644 index 0000000..f817321 --- /dev/null +++ b/ernie-sat/paddlespeech/server/entry.py @@ -0,0 +1,57 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from collections import defaultdict + +__all__ = ['server_commands', 'client_commands'] + + +def _CommandDict(): + return defaultdict(_CommandDict) + + +def server_execute(): + com = server_commands + idx = 0 + for _argv in (['paddlespeech_server'] + sys.argv[1:]): + if _argv not in com: + break + idx += 1 + com = com[_argv] + + # The method 'execute' of a command instance returns 'True' for a success + # while 'False' for a failure. Here converts this result into a exit status + # in bash: 0 for a success and 1 for a failure. + status = 0 if com['_entry']().execute(sys.argv[idx:]) else 1 + return status + + +def client_execute(): + com = client_commands + idx = 0 + for _argv in (['paddlespeech_client'] + sys.argv[1:]): + if _argv not in com: + break + idx += 1 + com = com[_argv] + + # The method 'execute' of a command instance returns 'True' for a success + # while 'False' for a failure. Here converts this result into a exit status + # in bash: 0 for a success and 1 for a failure. + status = 0 if com['_entry']().execute(sys.argv[idx:]) else 1 + return status + + +server_commands = _CommandDict() +client_commands = _CommandDict() diff --git a/ernie-sat/paddlespeech/server/executor.py b/ernie-sat/paddlespeech/server/executor.py new file mode 100644 index 0000000..fa2d01a --- /dev/null +++ b/ernie-sat/paddlespeech/server/executor.py @@ -0,0 +1,46 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from abc import ABC +from abc import abstractmethod +from typing import List + + +class BaseExecutor(ABC): + """ + An abstract executor of paddlespeech server tasks. + """ + + def __init__(self): + self.parser = argparse.ArgumentParser() + + @abstractmethod + def execute(self, argv: List[str]) -> bool: + """ + Command line entry. This method can only be accessed by a command line such as `paddlespeech asr`. + + Args: + argv (List[str]): Arguments from command line. + + Returns: + int: Result of the command execution. `True` for a success and `False` for a failure. + """ + pass + + @abstractmethod + def __call__(self, *arg, **kwargs): + """ + Python API to call an executor. + """ + pass diff --git a/ernie-sat/paddlespeech/server/restful/__init__.py b/ernie-sat/paddlespeech/server/restful/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/restful/api.py b/ernie-sat/paddlespeech/server/restful/api.py new file mode 100644 index 0000000..3f91a03 --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/api.py @@ -0,0 +1,44 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from fastapi import APIRouter + +from paddlespeech.server.restful.asr_api import router as asr_router +from paddlespeech.server.restful.cls_api import router as cls_router +from paddlespeech.server.restful.tts_api import router as tts_router + +_router = APIRouter() + + +def setup_router(api_list: List): + """setup router for fastapi + + Args: + api_list (List): [asr, tts, cls] + + Returns: + APIRouter + """ + for api_name in api_list: + if api_name == 'asr': + _router.include_router(asr_router) + elif api_name == 'tts': + _router.include_router(tts_router) + elif api_name == 'cls': + _router.include_router(cls_router) + else: + pass + + return _router diff --git a/ernie-sat/paddlespeech/server/restful/asr_api.py b/ernie-sat/paddlespeech/server/restful/asr_api.py new file mode 100644 index 0000000..cf46735 --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/asr_api.py @@ -0,0 +1,91 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import traceback +from typing import Union + +from fastapi import APIRouter + +from paddlespeech.server.engine.engine_pool import get_engine_pool +from paddlespeech.server.restful.request import ASRRequest +from paddlespeech.server.restful.response import ASRResponse +from paddlespeech.server.restful.response import ErrorResponse +from paddlespeech.server.utils.errors import ErrorCode +from paddlespeech.server.utils.errors import failed_response +from paddlespeech.server.utils.exception import ServerBaseException + +router = APIRouter() + + +@router.get('/paddlespeech/asr/help') +def help(): + """help + + Returns: + json: [description] + """ + response = { + "success": "True", + "code": 200, + "message": { + "global": "success" + }, + "result": { + "description": "asr server", + "input": "base64 string of wavfile", + "output": "transcription" + } + } + return response + + +@router.post( + "/paddlespeech/asr", response_model=Union[ASRResponse, ErrorResponse]) +def asr(request_body: ASRRequest): + """asr api + + Args: + request_body (ASRRequest): [description] + + Returns: + json: [description] + """ + try: + audio_data = base64.b64decode(request_body.audio) + + # get single engine from engine pool + engine_pool = get_engine_pool() + asr_engine = engine_pool['asr'] + + asr_engine.run(audio_data) + asr_results = asr_engine.postprocess() + + response = { + "success": True, + "code": 200, + "message": { + "description": "success" + }, + "result": { + "transcription": asr_results + } + } + + except ServerBaseException as e: + response = failed_response(e.error_code, e.msg) + except BaseException: + response = failed_response(ErrorCode.SERVER_UNKOWN_ERR) + traceback.print_exc() + + return response diff --git a/ernie-sat/paddlespeech/server/restful/cls_api.py b/ernie-sat/paddlespeech/server/restful/cls_api.py new file mode 100644 index 0000000..306d9ca --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/cls_api.py @@ -0,0 +1,92 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import traceback +from typing import Union + +from fastapi import APIRouter + +from paddlespeech.server.engine.engine_pool import get_engine_pool +from paddlespeech.server.restful.request import CLSRequest +from paddlespeech.server.restful.response import CLSResponse +from paddlespeech.server.restful.response import ErrorResponse +from paddlespeech.server.utils.errors import ErrorCode +from paddlespeech.server.utils.errors import failed_response +from paddlespeech.server.utils.exception import ServerBaseException + +router = APIRouter() + + +@router.get('/paddlespeech/cls/help') +def help(): + """help + + Returns: + json: [description] + """ + response = { + "success": "True", + "code": 200, + "message": { + "global": "success" + }, + "result": { + "description": "cls server", + "input": "base64 string of wavfile", + "output": "classification result" + } + } + return response + + +@router.post( + "/paddlespeech/cls", response_model=Union[CLSResponse, ErrorResponse]) +def cls(request_body: CLSRequest): + """cls api + + Args: + request_body (CLSRequest): [description] + + Returns: + json: [description] + """ + try: + audio_data = base64.b64decode(request_body.audio) + + # get single engine from engine pool + engine_pool = get_engine_pool() + cls_engine = engine_pool['cls'] + + cls_engine.run(audio_data) + cls_results = cls_engine.postprocess(request_body.topk) + + response = { + "success": True, + "code": 200, + "message": { + "description": "success" + }, + "result": { + "topk": request_body.topk, + "results": cls_results + } + } + + except ServerBaseException as e: + response = failed_response(e.error_code, e.msg) + except BaseException: + response = failed_response(ErrorCode.SERVER_UNKOWN_ERR) + traceback.print_exc() + + return response diff --git a/ernie-sat/paddlespeech/server/restful/request.py b/ernie-sat/paddlespeech/server/restful/request.py new file mode 100644 index 0000000..dbac9da --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/request.py @@ -0,0 +1,80 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +from pydantic import BaseModel + +__all__ = ['ASRRequest', 'TTSRequest', 'CLSRequest'] + + +#****************************************************************************************/ +#************************************ ASR request ***************************************/ +#****************************************************************************************/ +class ASRRequest(BaseModel): + """ + request body example + { + "audio": "exSI6ICJlbiIsCgkgICAgInBvc2l0aW9uIjogImZhbHNlIgoJf...", + "audio_format": "wav", + "sample_rate": 16000, + "lang": "zh_cn", + "punc":false + } + """ + audio: str + audio_format: str + sample_rate: int + lang: str + punc: Optional[bool] = None + + +#****************************************************************************************/ +#************************************ TTS request ***************************************/ +#****************************************************************************************/ +class TTSRequest(BaseModel): + """TTS request + + request body example + { + "text": "你好,欢迎使用百度飞桨语音合成服务。", + "spk_id": 0, + "speed": 1.0, + "volume": 1.0, + "sample_rate": 0, + "tts_audio_path": "./tts.wav" + } + + """ + + text: str + spk_id: int = 0 + speed: float = 1.0 + volume: float = 1.0 + sample_rate: int = 0 + save_path: str = None + + +#****************************************************************************************/ +#************************************ CLS request ***************************************/ +#****************************************************************************************/ +class CLSRequest(BaseModel): + """ + request body example + { + "audio": "exSI6ICJlbiIsCgkgICAgInBvc2l0aW9uIjogImZhbHNlIgoJf...", + "topk": 1 + } + """ + audio: str + topk: int = 1 diff --git a/ernie-sat/paddlespeech/server/restful/response.py b/ernie-sat/paddlespeech/server/restful/response.py new file mode 100644 index 0000000..a2a207e --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/response.py @@ -0,0 +1,148 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from pydantic import BaseModel + +__all__ = ['ASRResponse', 'TTSResponse', 'CLSResponse'] + + +class Message(BaseModel): + description: str + + +#****************************************************************************************/ +#************************************ ASR response **************************************/ +#****************************************************************************************/ +class AsrResult(BaseModel): + transcription: str + + +class ASRResponse(BaseModel): + """ + response example + { + "success": true, + "code": 0, + "message": { + "description": "success" + }, + "result": { + "transcription": "你好,飞桨" + } + } + """ + success: bool + code: int + message: Message + result: AsrResult + + +#****************************************************************************************/ +#************************************ TTS response **************************************/ +#****************************************************************************************/ +class TTSResult(BaseModel): + lang: str = "zh" + spk_id: int = 0 + speed: float = 1.0 + volume: float = 1.0 + sample_rate: int + duration: float + save_path: str = None + audio: str + + +class TTSResponse(BaseModel): + """ + response example + { + "success": true, + "code": 200, + "message": { + "description": "success" + }, + "result": { + "lang": "zh", + "spk_id": 0, + "speed": 1.0, + "volume": 1.0, + "sample_rate": 24000, + "duration": 3.6125, + "audio": "LTI1OTIuNjI1OTUwMzQsOTk2OS41NDk4...", + "save_path": "./tts.wav" + } + } + """ + success: bool + code: int + message: Message + result: TTSResult + + +#****************************************************************************************/ +#************************************ CLS response **************************************/ +#****************************************************************************************/ +class CLSResults(BaseModel): + class_name: str + prob: float + + +class CLSResult(BaseModel): + topk: int + results: List[CLSResults] + + +class CLSResponse(BaseModel): + """ + response example + { + "success": true, + "code": 0, + "message": { + "description": "success" + }, + "result": { + topk: 1 + results: [ + { + "class":"Speech", + "prob": 0.9027184844017029 + } + ] + } + } + """ + success: bool + code: int + message: Message + result: CLSResult + + +#****************************************************************************************/ +#********************************** Error response **************************************/ +#****************************************************************************************/ +class ErrorResponse(BaseModel): + """ + response example + { + "success": false, + "code": 0, + "message": { + "description": "Unknown error occurred." + } + } + """ + success: bool + code: int + message: Message diff --git a/ernie-sat/paddlespeech/server/restful/tts_api.py b/ernie-sat/paddlespeech/server/restful/tts_api.py new file mode 100644 index 0000000..4e9bbe2 --- /dev/null +++ b/ernie-sat/paddlespeech/server/restful/tts_api.py @@ -0,0 +1,127 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import traceback +from typing import Union + +from fastapi import APIRouter + +from paddlespeech.cli.log import logger +from paddlespeech.server.engine.engine_pool import get_engine_pool +from paddlespeech.server.restful.request import TTSRequest +from paddlespeech.server.restful.response import ErrorResponse +from paddlespeech.server.restful.response import TTSResponse +from paddlespeech.server.utils.errors import ErrorCode +from paddlespeech.server.utils.errors import failed_response +from paddlespeech.server.utils.exception import ServerBaseException + +router = APIRouter() + + +@router.get('/paddlespeech/tts/help') +def help(): + """help + + Returns: + json: [description] + """ + response = { + "success": "True", + "code": 200, + "message": { + "global": "success" + }, + "result": { + "description": "tts server", + "text": "sentence to be synthesized", + "audio": "the base64 of audio" + } + } + return response + + +@router.post( + "/paddlespeech/tts", response_model=Union[TTSResponse, ErrorResponse]) +def tts(request_body: TTSRequest): + """tts api + + Args: + request_body (TTSRequest): [description] + + Returns: + json: [description] + """ + + logger.info("request: {}".format(request_body)) + + # get params + text = request_body.text + spk_id = request_body.spk_id + speed = request_body.speed + volume = request_body.volume + sample_rate = request_body.sample_rate + save_path = request_body.save_path + + # Check parameters + if speed <= 0 or speed > 3: + return failed_response( + ErrorCode.SERVER_PARAM_ERR, + "invalid speed value, the value should be between 0 and 3.") + if volume <= 0 or volume > 3: + return failed_response( + ErrorCode.SERVER_PARAM_ERR, + "invalid volume value, the value should be between 0 and 3.") + if sample_rate not in [0, 16000, 8000]: + return failed_response( + ErrorCode.SERVER_PARAM_ERR, + "invalid sample_rate value, the choice of value is 0, 8000, 16000.") + if save_path is not None and not save_path.endswith( + "pcm") and not save_path.endswith("wav"): + return failed_response( + ErrorCode.SERVER_PARAM_ERR, + "invalid save_path, saved audio formats support pcm and wav") + + # run + try: + # get single engine from engine pool + engine_pool = get_engine_pool() + tts_engine = engine_pool['tts'] + logger.info("Get tts engine successfully.") + + lang, target_sample_rate, duration, wav_base64 = tts_engine.run( + text, spk_id, speed, volume, sample_rate, save_path) + + response = { + "success": True, + "code": 200, + "message": { + "description": "success." + }, + "result": { + "lang": lang, + "spk_id": spk_id, + "speed": speed, + "volume": volume, + "sample_rate": target_sample_rate, + "duration": duration, + "save_path": save_path, + "audio": wav_base64 + } + } + except ServerBaseException as e: + response = failed_response(e.error_code, e.msg) + except BaseException: + response = failed_response(ErrorCode.SERVER_UNKOWN_ERR) + traceback.print_exc() + + return response diff --git a/ernie-sat/paddlespeech/server/tests/asr/http_client.py b/ernie-sat/paddlespeech/server/tests/asr/http_client.py new file mode 100644 index 0000000..49f2adf --- /dev/null +++ b/ernie-sat/paddlespeech/server/tests/asr/http_client.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the +import base64 +import json +import time + +import requests + + +def readwav2base64(wav_file): + """ + read wave file and covert to base64 string + """ + with open(wav_file, 'rb') as f: + base64_bytes = base64.b64encode(f.read()) + base64_string = base64_bytes.decode('utf-8') + return base64_string + + +def main(): + """ + main func + """ + url = "http://127.0.0.1:8090/paddlespeech/asr" + + # start Timestamp + time_start = time.time() + + test_audio_dir = "./16_audio.wav" + audio = readwav2base64(test_audio_dir) + + data = { + "audio": audio, + "audio_format": "wav", + "sample_rate": 16000, + "lang": "zh_cn", + } + + r = requests.post(url=url, data=json.dumps(data)) + + # ending Timestamp + time_end = time.time() + print('time cost', time_end - time_start, 's') + + print(r.json()) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/server/tests/asr/online/microphone_client.py b/ernie-sat/paddlespeech/server/tests/asr/online/microphone_client.py new file mode 100644 index 0000000..2ceaf6d --- /dev/null +++ b/ernie-sat/paddlespeech/server/tests/asr/online/microphone_client.py @@ -0,0 +1,161 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +record wave from the mic +""" +import asyncio +import json +import logging +import threading +import wave +from signal import SIGINT +from signal import SIGTERM + +import pyaudio +import websockets + + +class ASRAudioHandler(threading.Thread): + def __init__(self, url="127.0.0.1", port=8091): + threading.Thread.__init__(self) + self.url = url + self.port = port + self.url = "ws://" + self.url + ":" + str(self.port) + "/ws/asr" + self.fileName = "./output.wav" + self.chunk = 5120 + self.format = pyaudio.paInt16 + self.channels = 1 + self.rate = 16000 + self._running = True + self._frames = [] + self.data_backup = [] + + def startrecord(self): + """ + start a new thread to record wave + """ + threading._start_new_thread(self.recording, ()) + + def recording(self): + """ + recording wave + """ + self._running = True + self._frames = [] + p = pyaudio.PyAudio() + stream = p.open( + format=self.format, + channels=self.channels, + rate=self.rate, + input=True, + frames_per_buffer=self.chunk) + while (self._running): + data = stream.read(self.chunk) + self._frames.append(data) + self.data_backup.append(data) + + stream.stop_stream() + stream.close() + p.terminate() + + def save(self): + """ + save wave data + """ + p = pyaudio.PyAudio() + wf = wave.open(self.fileName, 'wb') + wf.setnchannels(self.channels) + wf.setsampwidth(p.get_sample_size(self.format)) + wf.setframerate(self.rate) + wf.writeframes(b''.join(self.data_backup)) + wf.close() + p.terminate() + + def stoprecord(self): + """ + stop recording + """ + self._running = False + + async def run(self): + aa = input("是否开始录音? (y/n)") + if aa.strip() == "y": + self.startrecord() + logging.info("*" * 10 + "开始录音,请输入语音") + + async with websockets.connect(self.url) as ws: + # 发送开始指令 + audio_info = json.dumps( + { + "name": "test.wav", + "signal": "start", + "nbest": 5 + }, + sort_keys=True, + indent=4, + separators=(',', ': ')) + await ws.send(audio_info) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + + # send bytes data + logging.info("结束录音请: Ctrl + c。继续请按回车。") + try: + while True: + while len(self._frames) > 0: + await ws.send(self._frames.pop(0)) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + except asyncio.CancelledError: + # quit + # send finished + audio_info = json.dumps( + { + "name": "test.wav", + "signal": "end", + "nbest": 5 + }, + sort_keys=True, + indent=4, + separators=(',', ': ')) + await ws.send(audio_info) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + + self.stoprecord() + logging.info("*" * 10 + "录音结束") + self.save() + elif aa.strip() == "n": + exit() + else: + print("无效输入!") + exit() + + +if __name__ == "__main__": + + logging.basicConfig(level=logging.INFO) + logging.info("asr websocket client start") + + handler = ASRAudioHandler("127.0.0.1", 8091) + loop = asyncio.get_event_loop() + main_task = asyncio.ensure_future(handler.run()) + for signal in [SIGINT, SIGTERM]: + loop.add_signal_handler(signal, main_task.cancel) + try: + loop.run_until_complete(main_task) + finally: + loop.close() + + logging.info("asr websocket client finished") diff --git a/ernie-sat/paddlespeech/server/tests/asr/online/websocket_client.py b/ernie-sat/paddlespeech/server/tests/asr/online/websocket_client.py new file mode 100644 index 0000000..58b1a45 --- /dev/null +++ b/ernie-sat/paddlespeech/server/tests/asr/online/websocket_client.py @@ -0,0 +1,115 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import argparse +import asyncio +import json +import logging + +import numpy as np +import soundfile +import websockets + + +class ASRAudioHandler: + def __init__(self, url="127.0.0.1", port=8090): + self.url = url + self.port = port + self.url = "ws://" + self.url + ":" + str(self.port) + "/ws/asr" + + def read_wave(self, wavfile_path: str): + samples, sample_rate = soundfile.read(wavfile_path, dtype='int16') + x_len = len(samples) + chunk_stride = 40 * 16 #40ms, sample_rate = 16kHz + chunk_size = 80 * 16 #80ms, sample_rate = 16kHz + + if (x_len - chunk_size) % chunk_stride != 0: + padding_len_x = chunk_stride - (x_len - chunk_size) % chunk_stride + else: + padding_len_x = 0 + + padding = np.zeros((padding_len_x), dtype=samples.dtype) + padded_x = np.concatenate([samples, padding], axis=0) + + num_chunk = (x_len + padding_len_x - chunk_size) / chunk_stride + 1 + num_chunk = int(num_chunk) + + for i in range(0, num_chunk): + start = i * chunk_stride + end = start + chunk_size + x_chunk = padded_x[start:end] + yield x_chunk + + async def run(self, wavfile_path: str): + logging.info("send a message to the server") + # 读取音频 + # self.read_wave() + # 发送 websocket 的 handshake 协议头 + async with websockets.connect(self.url) as ws: + # server 端已经接收到 handshake 协议头 + # 发送开始指令 + audio_info = json.dumps( + { + "name": "test.wav", + "signal": "start", + "nbest": 5 + }, + sort_keys=True, + indent=4, + separators=(',', ': ')) + await ws.send(audio_info) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + + # send chunk audio data to engine + for chunk_data in self.read_wave(wavfile_path): + await ws.send(chunk_data.tobytes()) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + + # finished + audio_info = json.dumps( + { + "name": "test.wav", + "signal": "end", + "nbest": 5 + }, + sort_keys=True, + indent=4, + separators=(',', ': ')) + await ws.send(audio_info) + msg = await ws.recv() + logging.info("receive msg={}".format(msg)) + + +def main(args): + logging.basicConfig(level=logging.INFO) + logging.info("asr websocket client start") + handler = ASRAudioHandler("127.0.0.1", 8091) + loop = asyncio.get_event_loop() + loop.run_until_complete(handler.run(args.wavfile)) + logging.info("asr websocket client finished") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--wavfile", + action="store", + help="wav file path ", + default="./16_audio.wav") + args = parser.parse_args() + + main(args) diff --git a/ernie-sat/paddlespeech/server/tests/tts/test_client.py b/ernie-sat/paddlespeech/server/tests/tts/test_client.py new file mode 100644 index 0000000..e42c9bc --- /dev/null +++ b/ernie-sat/paddlespeech/server/tests/tts/test_client.py @@ -0,0 +1,104 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import base64 +import io +import json +import os +import random +import time + +import numpy as np +import requests +import soundfile + +from paddlespeech.server.utils.audio_process import wav2pcm + + +# Request and response +def tts_client(args): + """ Request and response + Args: + text: A sentence to be synthesized + outfile: Synthetic audio file + """ + url = 'http://127.0.0.1:8090/paddlespeech/tts' + request = { + "text": args.text, + "spk_id": args.spk_id, + "speed": args.speed, + "volume": args.volume, + "sample_rate": args.sample_rate, + "save_path": args.output + } + + response = requests.post(url, json.dumps(request)) + response_dict = response.json() + wav_base64 = response_dict["result"]["audio"] + + audio_data_byte = base64.b64decode(wav_base64) + # from byte + samples, sample_rate = soundfile.read( + io.BytesIO(audio_data_byte), dtype='float32') + + # transform audio + outfile = args.output + if outfile.endswith(".wav"): + soundfile.write(outfile, samples, sample_rate) + elif outfile.endswith(".pcm"): + temp_wav = str(random.getrandbits(128)) + ".wav" + soundfile.write(temp_wav, samples, sample_rate) + wav2pcm(temp_wav, outfile, data_type=np.int16) + os.system("rm %s" % (temp_wav)) + else: + print("The format for saving audio only supports wav or pcm") + + return len(samples), sample_rate + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + '--text', + type=str, + default="你好,欢迎使用语音合成服务", + help='A sentence to be synthesized') + parser.add_argument('--spk_id', type=int, default=0, help='Speaker id') + parser.add_argument('--speed', type=float, default=1.0, help='Audio speed') + parser.add_argument( + '--volume', type=float, default=1.0, help='Audio volume') + parser.add_argument( + '--sample_rate', + type=int, + default=0, + help='Sampling rate, the default is the same as the model') + parser.add_argument( + '--output', + type=str, + default="./out.wav", + help='Synthesized audio file') + args = parser.parse_args() + + st = time.time() + try: + samples_length, sample_rate = tts_client(args) + time_consume = time.time() - st + duration = samples_length / sample_rate + rtf = time_consume / duration + print("Synthesized audio successfully.") + print("Inference time: %f" % (time_consume)) + print("The duration of synthesized audio: %f" % (duration)) + print("The RTF is: %f" % (rtf)) + except BaseException: + print("Failed to synthesized audio.") diff --git a/ernie-sat/paddlespeech/server/util.py b/ernie-sat/paddlespeech/server/util.py new file mode 100644 index 0000000..1f1b0be --- /dev/null +++ b/ernie-sat/paddlespeech/server/util.py @@ -0,0 +1,367 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import hashlib +import inspect +import json +import os +import tarfile +import threading +import time +import uuid +import zipfile +from typing import Any +from typing import Dict + +import paddle +import requests +import yaml +from paddle.framework import load + +import paddleaudio +from . import download +from .entry import client_commands +from .entry import server_commands +try: + from .. import __version__ +except ImportError: + __version__ = "0.0.0" # for develop branch + +requests.adapters.DEFAULT_RETRIES = 3 + +__all__ = [ + 'cli_server_register', + 'get_server_command', + 'cli_client_register', + 'get_client_command', + 'download_and_decompress', + 'load_state_dict_from_url', + 'stats_wrapper', +] + + +def cli_server_register(name: str, description: str='') -> Any: + def _warpper(command): + items = name.split('.') + + com = server_commands + for item in items: + com = com[item] + com['_entry'] = command + if description: + com['_description'] = description + return command + + return _warpper + + +def get_server_command(name: str) -> Any: + items = name.split('.') + com = server_commands + for item in items: + com = com[item] + + return com['_entry'] + + +def cli_client_register(name: str, description: str='') -> Any: + def _warpper(command): + items = name.split('.') + + com = client_commands + for item in items: + com = com[item] + com['_entry'] = command + if description: + com['_description'] = description + return command + + return _warpper + + +def get_client_command(name: str) -> Any: + items = name.split('.') + com = client_commands + for item in items: + com = com[item] + + return com['_entry'] + + +def _get_uncompress_path(filepath: os.PathLike) -> os.PathLike: + file_dir = os.path.dirname(filepath) + is_zip_file = False + if tarfile.is_tarfile(filepath): + files = tarfile.open(filepath, "r:*") + file_list = files.getnames() + elif zipfile.is_zipfile(filepath): + files = zipfile.ZipFile(filepath, 'r') + file_list = files.namelist() + is_zip_file = True + else: + return file_dir + + if download._is_a_single_file(file_list): + rootpath = file_list[0] + uncompressed_path = os.path.join(file_dir, rootpath) + elif download._is_a_single_dir(file_list): + if is_zip_file: + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[0] + else: + rootpath = os.path.splitext(file_list[0])[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + else: + rootpath = os.path.splitext(filepath)[0].split(os.sep)[-1] + uncompressed_path = os.path.join(file_dir, rootpath) + + files.close() + return uncompressed_path + + +def download_and_decompress(archive: Dict[str, str], path: str) -> os.PathLike: + """ + Download archieves and decompress to specific path. + """ + if not os.path.isdir(path): + os.makedirs(path) + + assert 'url' in archive and 'md5' in archive, \ + 'Dictionary keys of "url" and "md5" are required in the archive, but got: {}'.format(list(archive.keys())) + + filepath = os.path.join(path, os.path.basename(archive['url'])) + if os.path.isfile(filepath) and download._md5check(filepath, + archive['md5']): + uncompress_path = _get_uncompress_path(filepath) + if not os.path.isdir(uncompress_path): + download._decompress(filepath) + else: + StatsWorker( + task='download', + version=__version__, + extra_info={ + 'download_url': archive['url'], + 'paddle_version': paddle.__version__ + }).start() + uncompress_path = download.get_path_from_url(archive['url'], path, + archive['md5']) + + return uncompress_path + + +def load_state_dict_from_url(url: str, path: str, md5: str=None) -> os.PathLike: + """ + Download and load a state dict from url + """ + if not os.path.isdir(path): + os.makedirs(path) + + download.get_path_from_url(url, path, md5) + return load(os.path.join(path, os.path.basename(url))) + + +def _get_user_home(): + return os.path.expanduser('~') + + +def _get_paddlespcceh_home(): + if 'PPSPEECH_HOME' in os.environ: + home_path = os.environ['PPSPEECH_HOME'] + if os.path.exists(home_path): + if os.path.isdir(home_path): + return home_path + else: + raise RuntimeError( + 'The environment variable PPSPEECH_HOME {} is not a directory.'. + format(home_path)) + else: + return home_path + return os.path.join(_get_user_home(), '.paddlespeech') + + +def _get_sub_home(directory): + home = os.path.join(_get_paddlespcceh_home(), directory) + if not os.path.exists(home): + os.makedirs(home) + return home + + +PPSPEECH_HOME = _get_paddlespcceh_home() +MODEL_HOME = _get_sub_home('models') +CONF_HOME = _get_sub_home('conf') + + +def _md5(text: str): + '''Calculate the md5 value of the input text.''' + md5code = hashlib.md5(text.encode()) + return md5code.hexdigest() + + +class ConfigCache: + def __init__(self): + self._data = {} + self._initialize() + self.file = os.path.join(CONF_HOME, 'cache.yaml') + if not os.path.exists(self.file): + self.flush() + return + + with open(self.file, 'r') as file: + try: + cfg = yaml.load(file, Loader=yaml.FullLoader) + self._data.update(cfg) + except BaseException: + self.flush() + + @property + def cache_info(self): + return self._data['cache_info'] + + def _initialize(self): + # Set default configuration values. + cache_info = _md5(str(uuid.uuid1())[-12:]) + "-" + str(int(time.time())) + self._data['cache_info'] = cache_info + + def flush(self): + '''Flush the current configuration into the configuration file.''' + with open(self.file, 'w') as file: + cfg = json.loads(json.dumps(self._data)) + yaml.dump(cfg, file) + + +stats_api = "http://paddlepaddle.org.cn/paddlehub/stat" +cache_info = ConfigCache().cache_info + + +class StatsWorker(threading.Thread): + def __init__(self, + task="asr", + model=None, + version=__version__, + extra_info={}): + threading.Thread.__init__(self) + self._task = task + self._model = model + self._version = version + self._extra_info = extra_info + + def run(self): + params = { + 'task': self._task, + 'version': self._version, + 'from': 'ppspeech' + } + if self._model: + params['model'] = self._model + + self._extra_info.update({ + 'cache_info': cache_info, + }) + params.update({"extra": json.dumps(self._extra_info)}) + + try: + requests.get(stats_api, params) + except Exception: + pass + + return + + +def _note_one_stat(cls_name, params={}): + task = cls_name.replace('Executor', '').lower() # XXExecutor + extra_info = { + 'paddle_version': paddle.__version__, + } + + if 'model' in params: + model = params['model'] + else: + model = None + + if 'audio_file' in params: + try: + _, sr = paddleaudio.load(params['audio_file']) + except Exception: + sr = -1 + + if task == 'asr': + extra_info.update({ + 'lang': params['lang'], + 'inp_sr': sr, + 'model_sr': params['sample_rate'], + }) + elif task == 'st': + extra_info.update({ + 'lang': + params['src_lang'] + '-' + params['tgt_lang'], + 'inp_sr': + sr, + 'model_sr': + params['sample_rate'], + }) + elif task == 'tts': + model = params['am'] + extra_info.update({ + 'lang': params['lang'], + 'vocoder': params['voc'], + }) + elif task == 'cls': + extra_info.update({ + 'inp_sr': sr, + }) + elif task == 'text': + extra_info.update({ + 'sub_task': params['task'], + 'lang': params['lang'], + }) + else: + return + + StatsWorker( + task=task, + model=model, + version=__version__, + extra_info=extra_info, ).start() + + +def _parse_args(func, *args, **kwargs): + # FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) + argspec = inspect.getfullargspec(func) + + keys = argspec[0] + if keys[0] == 'self': # Remove self pointer. + keys = keys[1:] + + default_values = argspec[3] + values = [None] * (len(keys) - len(default_values)) + values.extend(list(default_values)) + params = dict(zip(keys, values)) + + for idx, v in enumerate(args): + params[keys[idx]] = v + for k, v in kwargs.items(): + params[k] = v + + return params + + +def stats_wrapper(executor_func): + def _warpper(self, *args, **kwargs): + try: + _note_one_stat( + type(self).__name__, _parse_args(executor_func, *args, + **kwargs)) + except Exception: + pass + return executor_func(self, *args, **kwargs) + + return _warpper diff --git a/ernie-sat/paddlespeech/server/utils/__init__.py b/ernie-sat/paddlespeech/server/utils/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/utils/audio_process.py b/ernie-sat/paddlespeech/server/utils/audio_process.py new file mode 100644 index 0000000..3cbb495 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/audio_process.py @@ -0,0 +1,105 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import wave + +import numpy as np + +from paddlespeech.cli.log import logger + + +def wav2pcm(wavfile, pcmfile, data_type=np.int16): + """ Save the wav file as a pcm file + + Args: + wavfile (str): wav file path + pcmfile (str): pcm file save path + data_type (type, optional): pcm sample type. Defaults to np.int16. + """ + with open(wavfile, "rb") as f: + f.seek(0) + f.read(44) + data = np.fromfile(f, dtype=data_type) + data.tofile(pcmfile) + + +def pcm2wav(pcm_file, wav_file, channels=1, bits=16, sample_rate=16000): + """Save the pcm file as a wav file + + Args: + pcm_file (str): pcm file path + wav_file (str): wav file save path + channels (int, optional): audio channel. Defaults to 1. + bits (int, optional): Bit depth. Defaults to 16. + sample_rate (int, optional): sample rate. Defaults to 16000. + """ + pcmf = open(pcm_file, 'rb') + pcmdata = pcmf.read() + pcmf.close() + + if bits % 8 != 0: + logger.error("bits % 8 must == 0. now bits:" + str(bits)) + + wavfile = wave.open(wav_file, 'wb') + wavfile.setnchannels(channels) + wavfile.setsampwidth(bits // 8) + wavfile.setframerate(sample_rate) + wavfile.writeframes(pcmdata) + wavfile.close() + + +def change_speed(sample_raw, speed_rate, sample_rate): + """Change the audio speed by linear interpolation. + Note that this is an in-place transformation. + :param speed_rate: Rate of speed change: + speed_rate > 1.0, speed up the audio; + speed_rate = 1.0, unchanged; + speed_rate < 1.0, slow down the audio; + speed_rate <= 0.0, not allowed, raise ValueError. + :type speed_rate: float + :raises ValueError: If speed_rate <= 0.0. + """ + if speed_rate == 1.0: + return sample_raw + if speed_rate <= 0: + raise ValueError("speed_rate should be greater than zero.") + + # numpy + # old_length = self._samples.shape[0] + # new_length = int(old_length / speed_rate) + # old_indices = np.arange(old_length) + # new_indices = np.linspace(start=0, stop=old_length, num=new_length) + # self._samples = np.interp(new_indices, old_indices, self._samples) + + # sox, slow + try: + import soxbindings as sox + except ImportError: + try: + from paddlespeech.s2t.utils import dynamic_pip_install + package = "sox" + dynamic_pip_install.install(package) + package = "soxbindings" + dynamic_pip_install.install(package) + import soxbindings as sox + except Exception: + raise RuntimeError("Can not install soxbindings on your system.") + + tfm = sox.Transformer() + tfm.set_globals(multithread=False) + tfm.tempo(speed_rate) + sample_speed = tfm.build_array( + input_array=sample_raw, + sample_rate_in=sample_rate).squeeze(-1).astype(np.float32).copy() + + return sample_speed diff --git a/ernie-sat/paddlespeech/server/utils/buffer.py b/ernie-sat/paddlespeech/server/utils/buffer.py new file mode 100644 index 0000000..682357b --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/buffer.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class Frame(object): + """Represents a "frame" of audio data.""" + + def __init__(self, bytes, timestamp, duration): + self.bytes = bytes + self.timestamp = timestamp + self.duration = duration + + +class ChunkBuffer(object): + def __init__(self, + frame_duration_ms=80, + shift_ms=40, + sample_rate=16000, + sample_width=2): + self.sample_rate = sample_rate + self.frame_duration_ms = frame_duration_ms + self.shift_ms = shift_ms + self.remained_audio = b'' + self.sample_width = sample_width # int16 = 2; float32 = 4 + + def frame_generator(self, audio): + """Generates audio frames from PCM audio data. + Takes the desired frame duration in milliseconds, the PCM data, and + the sample rate. + Yields Frames of the requested duration. + """ + audio = self.remained_audio + audio + self.remained_audio = b'' + + n = int(self.sample_rate * (self.frame_duration_ms / 1000.0) * + self.sample_width) + shift_n = int(self.sample_rate * (self.shift_ms / 1000.0) * + self.sample_width) + offset = 0 + timestamp = 0.0 + duration = (float(n) / self.sample_rate) / self.sample_width + shift_duration = (float(shift_n) / self.sample_rate) / self.sample_width + while offset + n <= len(audio): + yield Frame(audio[offset:offset + n], timestamp, duration) + timestamp += shift_duration + offset += shift_n + + self.remained_audio += audio[offset:] diff --git a/ernie-sat/paddlespeech/server/utils/config.py b/ernie-sat/paddlespeech/server/utils/config.py new file mode 100644 index 0000000..8c75f53 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/config.py @@ -0,0 +1,30 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import yaml +from yacs.config import CfgNode + + +def get_config(config_file: str): + """[summary] + + Args: + config_file (str): config_file + + Returns: + CfgNode: + """ + with open(config_file, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + return config diff --git a/ernie-sat/paddlespeech/server/utils/errors.py b/ernie-sat/paddlespeech/server/utils/errors.py new file mode 100644 index 0000000..17ff755 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/errors.py @@ -0,0 +1,57 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from enum import IntEnum + +from fastapi import Response + + +class ErrorCode(IntEnum): + SERVER_OK = 200 # success. + + SERVER_PARAM_ERR = 400 # Input parameters are not valid. + SERVER_TASK_NOT_EXIST = 404 # Task is not exist. + + SERVER_INTERNAL_ERR = 500 # Internal error. + SERVER_NETWORK_ERR = 502 # Network exception. + SERVER_UNKOWN_ERR = 509 # Unknown error occurred. + + +ErrorMsg = { + ErrorCode.SERVER_OK: "success.", + ErrorCode.SERVER_PARAM_ERR: "Input parameters are not valid.", + ErrorCode.SERVER_TASK_NOT_EXIST: "Task is not exist.", + ErrorCode.SERVER_INTERNAL_ERR: "Internal error.", + ErrorCode.SERVER_NETWORK_ERR: "Network exception.", + ErrorCode.SERVER_UNKOWN_ERR: "Unknown error occurred." +} + + +def failed_response(code, msg=""): + """Interface call failure response + + Args: + code (int): error code number + msg (str, optional): Interface call failure information. Defaults to "". + + Returns: + Response (json): failure json information. + """ + + if not msg: + msg = ErrorMsg.get(code, "Unknown error occurred.") + + res = {"success": False, "code": int(code), "message": {"description": msg}} + + return Response(content=json.dumps(res), media_type="application/json") diff --git a/ernie-sat/paddlespeech/server/utils/exception.py b/ernie-sat/paddlespeech/server/utils/exception.py new file mode 100644 index 0000000..58ea777 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/exception.py @@ -0,0 +1,30 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import traceback + +from paddlespeech.server.utils.errors import ErrorMsg + + +class ServerBaseException(Exception): + """ Server Base exception + """ + + def __init__(self, error_code, msg=None): + #if msg: + #log.error(msg) + msg = msg if msg else ErrorMsg.get(error_code, "") + super(ServerBaseException, self).__init__(error_code, msg) + self.error_code = error_code + self.msg = msg + traceback.print_exc() diff --git a/ernie-sat/paddlespeech/server/utils/log.py b/ernie-sat/paddlespeech/server/utils/log.py new file mode 100644 index 0000000..8644064 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/log.py @@ -0,0 +1,59 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import functools +import logging + +__all__ = [ + 'logger', +] + + +class Logger(object): + def __init__(self, name: str=None): + name = 'PaddleSpeech' if not name else name + self.logger = logging.getLogger(name) + + log_config = { + 'DEBUG': 10, + 'INFO': 20, + 'TRAIN': 21, + 'EVAL': 22, + 'WARNING': 30, + 'ERROR': 40, + 'CRITICAL': 50, + 'EXCEPTION': 100, + } + for key, level in log_config.items(): + logging.addLevelName(level, key) + if key == 'EXCEPTION': + self.__dict__[key.lower()] = self.logger.exception + else: + self.__dict__[key.lower()] = functools.partial(self.__call__, + level) + + self.format = logging.Formatter( + fmt='[%(asctime)-15s] [%(levelname)8s] - %(message)s') + + self.handler = logging.StreamHandler() + self.handler.setFormatter(self.format) + + self.logger.addHandler(self.handler) + self.logger.setLevel(logging.DEBUG) + self.logger.propagate = False + + def __call__(self, log_level: str, msg: str): + self.logger.log(log_level, msg) + + +logger = Logger() diff --git a/ernie-sat/paddlespeech/server/utils/paddle_predictor.py b/ernie-sat/paddlespeech/server/utils/paddle_predictor.py new file mode 100644 index 0000000..16653cf --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/paddle_predictor.py @@ -0,0 +1,98 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +from typing import List +from typing import Optional + +import paddle +from paddle.inference import Config +from paddle.inference import create_predictor + + +def init_predictor(model_dir: Optional[os.PathLike]=None, + model_file: Optional[os.PathLike]=None, + params_file: Optional[os.PathLike]=None, + predictor_conf: dict=None): + """Create predictor with Paddle inference + + Args: + model_dir (Optional[os.PathLike], optional): The path of the static model saved in the model layer. Defaults to None. + model_file (Optional[os.PathLike], optional): *.pdmodel file path. Defaults to None. + params_file (Optional[os.PathLike], optional): *.pdiparams file path.. Defaults to None. + predictor_conf (dict, optional): The configuration parameters of predictor. Defaults to None. + + Returns: + predictor (PaddleInferPredictor): created predictor + """ + if model_dir is not None: + assert os.path.isdir(model_dir), 'Please check model dir.' + config = Config(args.model_dir) + else: + assert os.path.isfile(model_file) and os.path.isfile( + params_file), 'Please check model and parameter files.' + config = Config(model_file, params_file) + + # set device + if predictor_conf["device"]: + device = predictor_conf["device"] + else: + device = paddle.get_device() + if "gpu" in device: + gpu_id = device.split(":")[-1] + config.enable_use_gpu(1000, int(gpu_id)) + + # IR optim + if predictor_conf["switch_ir_optim"]: + config.switch_ir_optim() + + # glog + if not predictor_conf["glog_info"]: + config.disable_glog_info() + + # config summary + if predictor_conf["summary"]: + print(config.summary()) + + # memory optim + config.enable_memory_optim() + + predictor = create_predictor(config) + return predictor + + +def run_model(predictor, input: List) -> List: + """ run predictor + + Args: + predictor: paddle inference predictor + input (list): The input of predictor + + Returns: + list: result list + """ + input_names = predictor.get_input_names() + for i, name in enumerate(input_names): + input_handle = predictor.get_input_handle(name) + input_handle.copy_from_cpu(input[i]) + # do the inference + predictor.run() + results = [] + # get out data from output tensor + output_names = predictor.get_output_names() + for i, name in enumerate(output_names): + output_handle = predictor.get_output_handle(name) + output_data = output_handle.copy_to_cpu() + results.append(output_data) + + return results diff --git a/ernie-sat/paddlespeech/server/utils/util.py b/ernie-sat/paddlespeech/server/utils/util.py new file mode 100644 index 0000000..e9104fa --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/util.py @@ -0,0 +1,33 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the +import base64 + + +def wav2base64(wav_file: str): + """ + read wave file and covert to base64 string + """ + with open(wav_file, 'rb') as f: + base64_bytes = base64.b64encode(f.read()) + base64_string = base64_bytes.decode('utf-8') + return base64_string + + +def base64towav(base64_string: str): + pass + + +def self_check(): + """ self check resource + """ + return True diff --git a/ernie-sat/paddlespeech/server/utils/vad.py b/ernie-sat/paddlespeech/server/utils/vad.py new file mode 100644 index 0000000..a2dcf68 --- /dev/null +++ b/ernie-sat/paddlespeech/server/utils/vad.py @@ -0,0 +1,78 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import collections + +import webrtcvad + + +class VADAudio(): + def __init__(self, + aggressiveness=2, + rate=16000, + frame_duration_ms=20, + sample_width=2, + padding_ms=200, + padding_ratio=0.9): + """Initializes VAD with given aggressivenes and sets up internal queues""" + self.vad = webrtcvad.Vad(aggressiveness) + self.rate = rate + self.sample_width = sample_width + self.frame_duration_ms = frame_duration_ms + self._frame_length = int(rate * (frame_duration_ms / 1000.0) * + self.sample_width) + self._buffer_queue = collections.deque() + self.ring_buffer = collections.deque(maxlen=padding_ms // + frame_duration_ms) + self._ratio = padding_ratio + self.triggered = False + + def add_audio(self, audio): + """Adds new audio to internal queue""" + for x in audio: + self._buffer_queue.append(x) + + def frame_generator(self): + """Generator that yields audio frames of frame_duration_ms""" + while len(self._buffer_queue) > self._frame_length: + frame = bytearray() + for _ in range(self._frame_length): + frame.append(self._buffer_queue.popleft()) + yield bytes(frame) + + def vad_collector(self): + """Generator that yields series of consecutive audio frames comprising each utterence, separated by yielding a single None. + Determines voice activity by ratio of frames in padding_ms. Uses a buffer to include padding_ms prior to being triggered. + Example: (frame, ..., frame, None, frame, ..., frame, None, ...) + |---utterence---| |---utterence---| + """ + for frame in self.frame_generator(): + is_speech = self.vad.is_speech(frame, self.rate) + if not self.triggered: + self.ring_buffer.append((frame, is_speech)) + num_voiced = len( + [f for f, speech in self.ring_buffer if speech]) + if num_voiced > self._ratio * self.ring_buffer.maxlen: + self.triggered = True + for f, s in self.ring_buffer: + yield f + self.ring_buffer.clear() + else: + yield frame + self.ring_buffer.append((frame, is_speech)) + num_unvoiced = len( + [f for f, speech in self.ring_buffer if not speech]) + if num_unvoiced > self._ratio * self.ring_buffer.maxlen: + self.triggered = False + yield None + self.ring_buffer.clear() diff --git a/ernie-sat/paddlespeech/server/ws/__init__.py b/ernie-sat/paddlespeech/server/ws/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/server/ws/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/server/ws/api.py b/ernie-sat/paddlespeech/server/ws/api.py new file mode 100644 index 0000000..10664d1 --- /dev/null +++ b/ernie-sat/paddlespeech/server/ws/api.py @@ -0,0 +1,38 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from fastapi import APIRouter + +from paddlespeech.server.ws.asr_socket import router as asr_router + +_router = APIRouter() + + +def setup_router(api_list: List): + """setup router for fastapi + Args: + api_list (List): [asr, tts] + Returns: + APIRouter + """ + for api_name in api_list: + if api_name == 'asr': + _router.include_router(asr_router) + elif api_name == 'tts': + pass + else: + pass + + return _router diff --git a/ernie-sat/paddlespeech/server/ws/asr_socket.py b/ernie-sat/paddlespeech/server/ws/asr_socket.py new file mode 100644 index 0000000..ea19816 --- /dev/null +++ b/ernie-sat/paddlespeech/server/ws/asr_socket.py @@ -0,0 +1,100 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json + +import numpy as np +from fastapi import APIRouter +from fastapi import WebSocket +from fastapi import WebSocketDisconnect +from starlette.websockets import WebSocketState as WebSocketState + +from paddlespeech.server.engine.engine_pool import get_engine_pool +from paddlespeech.server.utils.buffer import ChunkBuffer +from paddlespeech.server.utils.vad import VADAudio + +router = APIRouter() + + +@router.websocket('/ws/asr') +async def websocket_endpoint(websocket: WebSocket): + + await websocket.accept() + + engine_pool = get_engine_pool() + asr_engine = engine_pool['asr'] + # init buffer + chunk_buffer_conf = asr_engine.config.chunk_buffer_conf + chunk_buffer = ChunkBuffer( + sample_rate=chunk_buffer_conf['sample_rate'], + sample_width=chunk_buffer_conf['sample_width']) + # init vad + vad_conf = asr_engine.config.vad_conf + vad = VADAudio( + aggressiveness=vad_conf['aggressiveness'], + rate=vad_conf['sample_rate'], + frame_duration_ms=vad_conf['frame_duration_ms']) + + try: + while True: + # careful here, changed the source code from starlette.websockets + assert websocket.application_state == WebSocketState.CONNECTED + message = await websocket.receive() + websocket._raise_on_disconnect(message) + if "text" in message: + message = json.loads(message["text"]) + if 'signal' not in message: + resp = {"status": "ok", "message": "no valid json data"} + await websocket.send_json(resp) + + if message['signal'] == 'start': + resp = {"status": "ok", "signal": "server_ready"} + # do something at begining here + await websocket.send_json(resp) + elif message['signal'] == 'end': + engine_pool = get_engine_pool() + asr_engine = engine_pool['asr'] + # reset single engine for an new connection + asr_engine.reset() + resp = {"status": "ok", "signal": "finished"} + await websocket.send_json(resp) + break + else: + resp = {"status": "ok", "message": "no valid json data"} + await websocket.send_json(resp) + elif "bytes" in message: + message = message["bytes"] + + # vad for input bytes audio + vad.add_audio(message) + message = b''.join(f for f in vad.vad_collector() + if f is not None) + + engine_pool = get_engine_pool() + asr_engine = engine_pool['asr'] + asr_results = "" + frames = chunk_buffer.frame_generator(message) + for frame in frames: + samples = np.frombuffer(frame.bytes, dtype=np.int16) + sample_rate = asr_engine.config.sample_rate + x_chunk, x_chunk_lens = asr_engine.preprocess(samples, + sample_rate) + asr_engine.run(x_chunk, x_chunk_lens) + asr_results = asr_engine.postprocess() + + asr_results = asr_engine.postprocess() + resp = {'asr_results': asr_results} + + await websocket.send_json(resp) + except WebSocketDisconnect: + pass diff --git a/ernie-sat/paddlespeech/t2s/__init__.py b/ernie-sat/paddlespeech/t2s/__init__.py new file mode 100644 index 0000000..7d93c02 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from . import datasets +from . import exps +from . import frontend +from . import models +from . import modules +from . import training +from . import utils diff --git a/ernie-sat/paddlespeech/t2s/audio/__init__.py b/ernie-sat/paddlespeech/t2s/audio/__init__.py new file mode 100644 index 0000000..0deefc8 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/audio/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .audio import AudioProcessor +from .codec import * +from .spec_normalizer import LogMagnitude +from .spec_normalizer import NormalizerBase diff --git a/ernie-sat/paddlespeech/t2s/audio/audio.py b/ernie-sat/paddlespeech/t2s/audio/audio.py new file mode 100644 index 0000000..59ea8c8 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/audio/audio.py @@ -0,0 +1,102 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import librosa +import numpy as np +import soundfile as sf + +__all__ = ["AudioProcessor"] + + +class AudioProcessor(object): + def __init__(self, + sample_rate: int, + n_fft: int, + win_length: int, + hop_length: int, + n_mels: int=80, + fmin: int=0, + fmax: int=None, + window="hann", + center=True, + pad_mode="reflect", + normalize=True): + # read & write + self.sample_rate = sample_rate + self.normalize = normalize + + # stft + self.n_fft = n_fft + self.win_length = win_length + self.hop_length = hop_length + self.window = window + self.center = center + self.pad_mode = pad_mode + + # mel + self.n_mels = n_mels + self.fmin = fmin + self.fmax = fmax + + self.mel_filter = self._create_mel_filter() + self.inv_mel_filter = np.linalg.pinv(self.mel_filter) + + def _create_mel_filter(self): + mel_filter = librosa.filters.mel( + sr=self.sample_rate, + n_fft=self.n_fft, + n_mels=self.n_mels, + fmin=self.fmin, + fmax=self.fmax) + return mel_filter + + def read_wav(self, filename): + # resampling may occur + wav, _ = librosa.load(filename, sr=self.sample_rate) + + # normalize the volume + if self.normalize: + wav = wav / np.max(np.abs(wav)) * 0.999 + return wav + + def write_wav(self, path, wav): + sf.write(path, wav, samplerate=self.sample_rate) + + def stft(self, wav): + D = librosa.core.stft( + wav, + n_fft=self.n_fft, + hop_length=self.hop_length, + win_length=self.win_length, + window=self.window, + center=self.center, + pad_mode=self.pad_mode) + return D + + def istft(self, D): + wav = librosa.core.istft( + D, + hop_length=self.hop_length, + win_length=self.win_length, + window=self.window, + center=self.center) + return wav + + def spectrogram(self, wav): + D = self.stft(wav) + return np.abs(D) + + def mel_spectrogram(self, wav): + S = self.spectrogram(wav) + mel = np.dot(self.mel_filter, S) + return mel diff --git a/ernie-sat/paddlespeech/t2s/audio/codec.py b/ernie-sat/paddlespeech/t2s/audio/codec.py new file mode 100644 index 0000000..2a759ce --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/audio/codec.py @@ -0,0 +1,51 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import numpy as np +import paddle + + +# x: [0: 2**bit-1], return: [-1, 1] +def label_2_float(x, bits): + return 2 * x / (2**bits - 1.) - 1. + + +#x: [-1, 1], return: [0, 2**bits-1] +def float_2_label(x, bits): + assert abs(x).max() <= 1.0 + x = (x + 1.) * (2**bits - 1) / 2 + return x.clip(0, 2**bits - 1) + + +# y: [-1, 1], mu: 2**bits, return: [0, 2**bits-1] +# see https://en.wikipedia.org/wiki/%CE%9C-law_algorithm +# be careful the input `mu` here, which is +1 than that of the link above +def encode_mu_law(x, mu): + mu = mu - 1 + fx = np.sign(x) * np.log(1 + mu * np.abs(x)) / np.log(1 + mu) + return np.floor((fx + 1) / 2 * mu + 0.5) + + +# from_labels = True: +# y: [0: 2**bit-1], mu: 2**bits, return: [-1,1] +# from_labels = False: +# y: [-1, 1], return: [-1, 1] +def decode_mu_law(y, mu, from_labels=True): + # TODO: get rid of log2 - makes no sense + if from_labels: + y = label_2_float(y, math.log2(mu)) + mu = mu - 1 + x = paddle.sign(y) / mu * ((1 + mu)**paddle.abs(y) - 1) + return x diff --git a/ernie-sat/paddlespeech/t2s/audio/spec_normalizer.py b/ernie-sat/paddlespeech/t2s/audio/spec_normalizer.py new file mode 100644 index 0000000..d8cd67a --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/audio/spec_normalizer.py @@ -0,0 +1,74 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This modules contains normalizers for spectrogram magnitude. +Normalizers are invertible transformations. They can be used to process +magnitude of spectrogram before training and can also be used to recover from +the generated spectrogram so as to be used with vocoders like griffin lim. + +The base class describe the interface. `transform` is used to perform +transformation and `inverse` is used to perform the inverse transformation. + +check issues: +https://github.com/mozilla/TTS/issues/377 +""" +import numpy as np + +__all__ = ["NormalizerBase", "LogMagnitude", "UnitMagnitude"] + + +class NormalizerBase(object): + def transform(self, spec): + raise NotImplementedError("transform must be implemented") + + def inverse(self, normalized): + raise NotImplementedError("inverse must be implemented") + + +class LogMagnitude(NormalizerBase): + """ + This is a simple normalizer used in Waveglow, Waveflow, tacotron2... + """ + + def __init__(self, min=1e-5): + self.min = min + + def transform(self, x): + x = np.maximum(x, self.min) + x = np.log(x) + return x + + def inverse(self, x): + return np.exp(x) + + +class UnitMagnitude(NormalizerBase): + # dbscale and (0, 1) normalization + """ + This is the normalizer used in the + """ + + def __init__(self, min=1e-5): + self.min = min + + def transform(self, x): + db_scale = 20 * np.log10(np.maximum(self.min, x)) - 20 + normalized = (db_scale + 100) / 100 + clipped = np.clip(normalized, 0, 1) + return clipped + + def inverse(self, x): + denormalized = np.clip(x, 0, 1) * 100 - 100 + out = np.exp((denormalized + 20) / 20 * np.log(10)) + return out diff --git a/ernie-sat/paddlespeech/t2s/datasets/__init__.py b/ernie-sat/paddlespeech/t2s/datasets/__init__.py new file mode 100644 index 0000000..caf20aa --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .ljspeech import * diff --git a/ernie-sat/paddlespeech/t2s/datasets/am_batch_fn.py b/ernie-sat/paddlespeech/t2s/datasets/am_batch_fn.py new file mode 100644 index 0000000..4e3ad3c --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/am_batch_fn.py @@ -0,0 +1,295 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle + +from paddlespeech.t2s.datasets.batch import batch_sequences + + +def tacotron2_single_spk_batch_fn(examples): + # fields = ["text", "text_lengths", "speech", "speech_lengths"] + text = [np.array(item["text"], dtype=np.int64) for item in examples] + speech = [np.array(item["speech"], dtype=np.float32) for item in examples] + text_lengths = [ + np.array(item["text_lengths"], dtype=np.int64) for item in examples + ] + speech_lengths = [ + np.array(item["speech_lengths"], dtype=np.int64) for item in examples + ] + + text = batch_sequences(text) + speech = batch_sequences(speech) + + # convert each batch to paddle.Tensor + text = paddle.to_tensor(text) + speech = paddle.to_tensor(speech) + text_lengths = paddle.to_tensor(text_lengths) + speech_lengths = paddle.to_tensor(speech_lengths) + + batch = { + "text": text, + "text_lengths": text_lengths, + "speech": speech, + "speech_lengths": speech_lengths, + } + return batch + + +def tacotron2_multi_spk_batch_fn(examples): + # fields = ["text", "text_lengths", "speech", "speech_lengths"] + text = [np.array(item["text"], dtype=np.int64) for item in examples] + speech = [np.array(item["speech"], dtype=np.float32) for item in examples] + text_lengths = [ + np.array(item["text_lengths"], dtype=np.int64) for item in examples + ] + speech_lengths = [ + np.array(item["speech_lengths"], dtype=np.int64) for item in examples + ] + + text = batch_sequences(text) + speech = batch_sequences(speech) + + # convert each batch to paddle.Tensor + text = paddle.to_tensor(text) + speech = paddle.to_tensor(speech) + text_lengths = paddle.to_tensor(text_lengths) + speech_lengths = paddle.to_tensor(speech_lengths) + + batch = { + "text": text, + "text_lengths": text_lengths, + "speech": speech, + "speech_lengths": speech_lengths, + } + # spk_emb has a higher priority than spk_id + if "spk_emb" in examples[0]: + spk_emb = [ + np.array(item["spk_emb"], dtype=np.float32) for item in examples + ] + spk_emb = batch_sequences(spk_emb) + spk_emb = paddle.to_tensor(spk_emb) + batch["spk_emb"] = spk_emb + elif "spk_id" in examples[0]: + spk_id = [np.array(item["spk_id"], dtype=np.int64) for item in examples] + spk_id = paddle.to_tensor(spk_id) + batch["spk_id"] = spk_id + return batch + + +def speedyspeech_single_spk_batch_fn(examples): + # fields = ["phones", "tones", "num_phones", "num_frames", "feats", "durations"] + phones = [np.array(item["phones"], dtype=np.int64) for item in examples] + tones = [np.array(item["tones"], dtype=np.int64) for item in examples] + feats = [np.array(item["feats"], dtype=np.float32) for item in examples] + durations = [ + np.array(item["durations"], dtype=np.int64) for item in examples + ] + num_phones = [ + np.array(item["num_phones"], dtype=np.int64) for item in examples + ] + num_frames = [ + np.array(item["num_frames"], dtype=np.int64) for item in examples + ] + + phones = batch_sequences(phones) + tones = batch_sequences(tones) + feats = batch_sequences(feats) + durations = batch_sequences(durations) + + # convert each batch to paddle.Tensor + phones = paddle.to_tensor(phones) + tones = paddle.to_tensor(tones) + feats = paddle.to_tensor(feats) + durations = paddle.to_tensor(durations) + num_phones = paddle.to_tensor(num_phones) + num_frames = paddle.to_tensor(num_frames) + batch = { + "phones": phones, + "tones": tones, + "num_phones": num_phones, + "num_frames": num_frames, + "feats": feats, + "durations": durations, + } + return batch + + +def speedyspeech_multi_spk_batch_fn(examples): + # fields = ["phones", "tones", "num_phones", "num_frames", "feats", "durations", "spk_id"] + phones = [np.array(item["phones"], dtype=np.int64) for item in examples] + tones = [np.array(item["tones"], dtype=np.int64) for item in examples] + feats = [np.array(item["feats"], dtype=np.float32) for item in examples] + durations = [ + np.array(item["durations"], dtype=np.int64) for item in examples + ] + num_phones = [ + np.array(item["num_phones"], dtype=np.int64) for item in examples + ] + num_frames = [ + np.array(item["num_frames"], dtype=np.int64) for item in examples + ] + + phones = batch_sequences(phones) + tones = batch_sequences(tones) + feats = batch_sequences(feats) + durations = batch_sequences(durations) + + # convert each batch to paddle.Tensor + phones = paddle.to_tensor(phones) + tones = paddle.to_tensor(tones) + feats = paddle.to_tensor(feats) + durations = paddle.to_tensor(durations) + num_phones = paddle.to_tensor(num_phones) + num_frames = paddle.to_tensor(num_frames) + batch = { + "phones": phones, + "tones": tones, + "num_phones": num_phones, + "num_frames": num_frames, + "feats": feats, + "durations": durations, + } + if "spk_id" in examples[0]: + spk_id = [np.array(item["spk_id"], dtype=np.int64) for item in examples] + spk_id = paddle.to_tensor(spk_id) + batch["spk_id"] = spk_id + return batch + + +def fastspeech2_single_spk_batch_fn(examples): + # fields = ["text", "text_lengths", "speech", "speech_lengths", "durations", "pitch", "energy"] + text = [np.array(item["text"], dtype=np.int64) for item in examples] + speech = [np.array(item["speech"], dtype=np.float32) for item in examples] + pitch = [np.array(item["pitch"], dtype=np.float32) for item in examples] + energy = [np.array(item["energy"], dtype=np.float32) for item in examples] + durations = [ + np.array(item["durations"], dtype=np.int64) for item in examples + ] + + text_lengths = [ + np.array(item["text_lengths"], dtype=np.int64) for item in examples + ] + speech_lengths = [ + np.array(item["speech_lengths"], dtype=np.int64) for item in examples + ] + + text = batch_sequences(text) + pitch = batch_sequences(pitch) + speech = batch_sequences(speech) + durations = batch_sequences(durations) + energy = batch_sequences(energy) + + # convert each batch to paddle.Tensor + text = paddle.to_tensor(text) + pitch = paddle.to_tensor(pitch) + speech = paddle.to_tensor(speech) + durations = paddle.to_tensor(durations) + energy = paddle.to_tensor(energy) + text_lengths = paddle.to_tensor(text_lengths) + speech_lengths = paddle.to_tensor(speech_lengths) + + batch = { + "text": text, + "text_lengths": text_lengths, + "durations": durations, + "speech": speech, + "speech_lengths": speech_lengths, + "pitch": pitch, + "energy": energy + } + return batch + + +def fastspeech2_multi_spk_batch_fn(examples): + # fields = ["text", "text_lengths", "speech", "speech_lengths", "durations", "pitch", "energy", "spk_id"/"spk_emb"] + text = [np.array(item["text"], dtype=np.int64) for item in examples] + speech = [np.array(item["speech"], dtype=np.float32) for item in examples] + pitch = [np.array(item["pitch"], dtype=np.float32) for item in examples] + energy = [np.array(item["energy"], dtype=np.float32) for item in examples] + durations = [ + np.array(item["durations"], dtype=np.int64) for item in examples + ] + text_lengths = [ + np.array(item["text_lengths"], dtype=np.int64) for item in examples + ] + speech_lengths = [ + np.array(item["speech_lengths"], dtype=np.int64) for item in examples + ] + + text = batch_sequences(text) + pitch = batch_sequences(pitch) + speech = batch_sequences(speech) + durations = batch_sequences(durations) + energy = batch_sequences(energy) + + # convert each batch to paddle.Tensor + text = paddle.to_tensor(text) + pitch = paddle.to_tensor(pitch) + speech = paddle.to_tensor(speech) + durations = paddle.to_tensor(durations) + energy = paddle.to_tensor(energy) + text_lengths = paddle.to_tensor(text_lengths) + speech_lengths = paddle.to_tensor(speech_lengths) + + batch = { + "text": text, + "text_lengths": text_lengths, + "durations": durations, + "speech": speech, + "speech_lengths": speech_lengths, + "pitch": pitch, + "energy": energy + } + # spk_emb has a higher priority than spk_id + if "spk_emb" in examples[0]: + spk_emb = [ + np.array(item["spk_emb"], dtype=np.float32) for item in examples + ] + spk_emb = batch_sequences(spk_emb) + spk_emb = paddle.to_tensor(spk_emb) + batch["spk_emb"] = spk_emb + elif "spk_id" in examples[0]: + spk_id = [np.array(item["spk_id"], dtype=np.int64) for item in examples] + spk_id = paddle.to_tensor(spk_id) + batch["spk_id"] = spk_id + return batch + + +def transformer_single_spk_batch_fn(examples): + # fields = ["text", "text_lengths", "speech", "speech_lengths"] + text = [np.array(item["text"], dtype=np.int64) for item in examples] + speech = [np.array(item["speech"], dtype=np.float32) for item in examples] + text_lengths = [ + np.array(item["text_lengths"], dtype=np.int64) for item in examples + ] + speech_lengths = [ + np.array(item["speech_lengths"], dtype=np.int64) for item in examples + ] + + text = batch_sequences(text) + speech = batch_sequences(speech) + + # convert each batch to paddle.Tensor + text = paddle.to_tensor(text) + speech = paddle.to_tensor(speech) + text_lengths = paddle.to_tensor(text_lengths) + speech_lengths = paddle.to_tensor(speech_lengths) + + batch = { + "text": text, + "text_lengths": text_lengths, + "speech": speech, + "speech_lengths": speech_lengths, + } + return batch diff --git a/ernie-sat/paddlespeech/t2s/datasets/batch.py b/ernie-sat/paddlespeech/t2s/datasets/batch.py new file mode 100644 index 0000000..9d83bbe --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/batch.py @@ -0,0 +1,188 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Utility functions to create batch for arrays which satisfy some conditions. +Batch functions for text sequences, audio and spectrograms are provided. +""" +import numpy as np + +__all__ = [ + "batch_text_id", + "batch_wav", + "batch_spec", + "TextIDBatcher", + "WavBatcher", + "SpecBatcher", +] + + +class TextIDBatcher(object): + """A wrapper class for `batch_text_id`.""" + + def __init__(self, pad_id=0, dtype=np.int64): + self.pad_id = pad_id + self.dtype = dtype + + def __call__(self, minibatch): + out = batch_text_id(minibatch, pad_id=self.pad_id, dtype=self.dtype) + return out + + +def batch_text_id(minibatch, pad_id=0, dtype=np.int64): + """Pad sequences to text_ids to the largest length and batch them. + + Args: + minibatch (List[np.ndarray]): list of rank-1 arrays, shape(T,), dtype np.int64, text_ids. + pad_id (int, optional): the id which correspond to the special pad token. Defaults to 0. + dtype (np.dtype, optional): the data dtype of the output. Defaults to np.int64. + + Returns: + np.ndarray: rank-2 array of text_ids, shape(B, T), B stands for batch_size, T stands for length. The output batch. + """ + peek_example = minibatch[0] + assert len(peek_example.shape) == 1, "text example is an 1D tensor" + # assume (channel, n_samples) or (n_samples, ) + lengths = [example.shape[0] for example in minibatch] + max_len = np.max(lengths) + + batch = [] + for example in minibatch: + pad_len = max_len - example.shape[0] + batch.append( + np.pad( + example, [(0, pad_len)], + mode='constant', + constant_values=pad_id)) + + return np.array(batch, dtype=dtype), np.array(lengths, dtype=np.int64) + + +class WavBatcher(object): + """A wrapper class for `batch_wav`.""" + + def __init__(self, pad_value=0., dtype=np.float32): + self.pad_value = pad_value + self.dtype = dtype + + def __call__(self, minibatch): + out = batch_wav(minibatch, pad_value=self.pad_value, dtype=self.dtype) + return out + + +def batch_wav(minibatch, pad_value=0., dtype=np.float32): + """pad audios to the largest length and batch them. + + Args: + minibatch (List[np.ndarray]): list of rank-1 float arrays(mono-channel audio, shape(T,)), dtype float. + pad_value (float, optional): the pad value. Defaults to 0.. + dtype (np.dtype, optional): the data type of the output. Defaults to np.float32. + + Returns: + np.ndarray: shape(B, T), the output batch. + """ + + peek_example = minibatch[0] + assert len(peek_example.shape) == 1, "we only handles mono-channel wav" + + # assume (channel, n_samples) or (n_samples, ) + lengths = [example.shape[-1] for example in minibatch] + max_len = np.max(lengths) + + batch = [] + for example in minibatch: + pad_len = max_len - example.shape[-1] + batch.append( + np.pad( + example, [(0, pad_len)], + mode='constant', + constant_values=pad_value)) + return np.array(batch, dtype=dtype), np.array(lengths, dtype=np.int64) + + +class SpecBatcher(object): + """A wrapper class for `batch_spec`""" + + def __init__(self, pad_value=0., time_major=False, dtype=np.float32): + self.pad_value = pad_value + self.dtype = dtype + self.time_major = time_major + + def __call__(self, minibatch): + out = batch_spec( + minibatch, + pad_value=self.pad_value, + time_major=self.time_major, + dtype=self.dtype) + return out + + +def batch_spec(minibatch, pad_value=0., time_major=False, dtype=np.float32): + """Pad spectra to the largest length and batch them. + + Args: + minibatch (List[np.ndarray]): list of rank-2 arrays of shape(F, T) for mono-channel spectrograms, or list of rank-3 arrays of shape(C, F, T) for multi-channel spectrograms(F stands for frequency bands.), dtype float. + pad_value (float, optional): the pad value. Defaults to 0.. + dtype (np.dtype, optional): data type of the output. Defaults to np.float32. + + Returns: + np.ndarray: a rank-3 array of shape(B, F, T) or (B, T, F). + """ + # assume (F, T) or (T, F) + peek_example = minibatch[0] + assert len( + peek_example.shape) == 2, "we only handles mono channel spectrogram" + + # assume (F, n_frame) or (n_frame, F) + time_idx = 0 if time_major else -1 + lengths = [example.shape[time_idx] for example in minibatch] + max_len = np.max(lengths) + + batch = [] + for example in minibatch: + pad_len = max_len - example.shape[time_idx] + if time_major: + batch.append( + np.pad( + example, [(0, pad_len), (0, 0)], + mode='constant', + constant_values=pad_value)) + else: + batch.append( + np.pad( + example, [(0, 0), (0, pad_len)], + mode='constant', + constant_values=pad_value)) + return np.array(batch, dtype=dtype), np.array(lengths, dtype=np.int64) + + +def batch_sequences(sequences, axis=0, pad_value=0): + # import pdb; pdb.set_trace() + seq = sequences[0] + ndim = seq.ndim + if axis < 0: + axis += ndim + dtype = seq.dtype + pad_value = dtype.type(pad_value) + seq_lengths = [seq.shape[axis] for seq in sequences] + max_length = np.max(seq_lengths) + + padded_sequences = [] + for seq, length in zip(sequences, seq_lengths): + padding = [(0, 0)] * axis + [(0, max_length - length)] + [(0, 0)] * ( + ndim - axis - 1) + padded_seq = np.pad( + seq, padding, mode='constant', constant_values=pad_value) + padded_sequences.append(padded_seq) + batch = np.stack(padded_sequences) + return batch diff --git a/ernie-sat/paddlespeech/t2s/datasets/data_table.py b/ernie-sat/paddlespeech/t2s/datasets/data_table.py new file mode 100644 index 0000000..c9815af --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/data_table.py @@ -0,0 +1,133 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from multiprocessing import Manager +from typing import Any +from typing import Callable +from typing import Dict +from typing import List + +from paddle.io import Dataset + + +class DataTable(Dataset): + """Dataset to load and convert data for general purpose. + Args: + data (List[Dict[str, Any]]): Metadata, a list of meta datum, each of which is composed of several fields + fields (List[str], optional): Fields to use, if not specified, all the fields in the data are used, by default None + converters (Dict[str, Callable], optional): Converters used to process each field, by default None + use_cache (bool, optional): Whether to use cache, by default False + + Raises: + ValueError: + If there is some field that does not exist in data. + ValueError: + If there is some field in converters that does not exist in fields. + """ + + def __init__(self, + data: List[Dict[str, Any]], + fields: List[str]=None, + converters: Dict[str, Callable]=None, + use_cache: bool=False): + # metadata + self.data = data + assert len(data) > 0, "This dataset has no examples" + + # peak an example to get existing fields. + first_example = self.data[0] + fields_in_data = first_example.keys() + + # check all the requested fields exist + if fields is None: + self.fields = fields_in_data + else: + for field in fields: + if field not in fields_in_data: + raise ValueError( + f"The requested field ({field}) is not found" + f"in the data. Fields in the data is {fields_in_data}") + self.fields = fields + + # check converters + if converters is None: + self.converters = {} + else: + for field in converters.keys(): + if field not in self.fields: + raise ValueError( + f"The converter has a non existing field ({field})") + self.converters = converters + + self.use_cache = use_cache + if use_cache: + self._initialize_cache() + + def _initialize_cache(self): + self.manager = Manager() + self.caches = self.manager.list() + self.caches += [None for _ in range(len(self))] + + def _get_metadata(self, idx: int) -> Dict[str, Any]: + """Return a meta-datum given an index.""" + return self.data[idx] + + def _convert(self, meta_datum: Dict[str, Any]) -> Dict[str, Any]: + """Convert a meta datum to an example by applying the corresponding + converters to each fields requested. + + Args: + meta_datum (Dict[str, Any]): Meta datum + + Returns: + Dict[str, Any]: Converted example + """ + example = {} + for field in self.fields: + converter = self.converters.get(field, None) + meta_datum_field = meta_datum[field] + if converter is not None: + converted_field = converter(meta_datum_field) + else: + converted_field = meta_datum_field + example[field] = converted_field + return example + + def __getitem__(self, idx: int) -> Dict[str, Any]: + """Get an example given an index. + Args: + idx (int): Index of the example to get + + Returns: + Dict[str, Any]: A converted example + """ + if self.use_cache and self.caches[idx] is not None: + return self.caches[idx] + + meta_datum = self._get_metadata(idx) + example = self._convert(meta_datum) + + if self.use_cache: + self.caches[idx] = example + + return example + + def __len__(self) -> int: + """Returns the size of the dataset. + + Returns + ------- + int + The length of the dataset + """ + return len(self.data) diff --git a/ernie-sat/paddlespeech/t2s/datasets/dataset.py b/ernie-sat/paddlespeech/t2s/datasets/dataset.py new file mode 100644 index 0000000..2d6c03c --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/dataset.py @@ -0,0 +1,261 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import six +from paddle.io import Dataset + +__all__ = [ + "split", + "TransformDataset", + "CacheDataset", + "TupleDataset", + "DictDataset", + "SliceDataset", + "SubsetDataset", + "FilterDataset", + "ChainDataset", +] + + +def split(dataset, first_size): + """A utility function to split a dataset into two datasets.""" + first = SliceDataset(dataset, 0, first_size) + second = SliceDataset(dataset, first_size, len(dataset)) + return first, second + + +class TransformDataset(Dataset): + def __init__(self, dataset, transform): + """Dataset which is transformed from another with a transform. + + Args: + dataset (Dataset): the base dataset. + transform (callable): the transform which takes an example of the base dataset as parameter and return a new example. + """ + self._dataset = dataset + self._transform = transform + + def __len__(self): + return len(self._dataset) + + def __getitem__(self, i): + in_data = self._dataset[i] + return self._transform(in_data) + + +class CacheDataset(Dataset): + def __init__(self, dataset): + """A lazy cache of the base dataset. + + Args: + dataset (Dataset): the base dataset to cache. + """ + self._dataset = dataset + self._cache = dict() + + def __len__(self): + return len(self._dataset) + + def __getitem__(self, i): + if i not in self._cache: + self._cache[i] = self._dataset[i] + return self._cache[i] + + +class TupleDataset(Dataset): + def __init__(self, *datasets): + """A compound dataset made from several datasets of the same length. An example of the `TupleDataset` is a tuple of examples from the constituent datasets. + + Args: + datasets: tuple[Dataset], the constituent datasets. + """ + if not datasets: + raise ValueError("no datasets are given") + length = len(datasets[0]) + for i, dataset in enumerate(datasets): + if len(dataset) != length: + raise ValueError("all the datasets should have the same length." + "dataset {} has a different length".format(i)) + self._datasets = datasets + self._length = length + + def __getitem__(self, index): + # SOA + batches = [dataset[index] for dataset in self._datasets] + if isinstance(index, slice): + length = len(batches[0]) + # AOS + return [ + tuple([batch[i] for batch in batches]) + for i in six.moves.range(length) + ] + else: + return tuple(batches) + + def __len__(self): + return self._length + + +class DictDataset(Dataset): + def __init__(self, **datasets): + """ + A compound dataset made from several datasets of the same length. An + example of the `DictDataset` is a dict of examples from the constituent + datasets. + + WARNING: paddle does not have a good support for DictDataset, because + every batch yield from a DataLoader is a list, but it cannot be a dict. + So you have to provide a collate function because you cannot use the + default one. + + Args: + datasets: Dict[Dataset], the constituent datasets. + """ + if not datasets: + raise ValueError("no datasets are given") + length = None + for key, dataset in six.iteritems(datasets): + if length is None: + length = len(dataset) + elif len(dataset) != length: + raise ValueError( + "all the datasets should have the same length." + "dataset {} has a different length".format(key)) + self._datasets = datasets + self._length = length + + def __getitem__(self, index): + batches = { + key: dataset[index] + for key, dataset in six.iteritems(self._datasets) + } + if isinstance(index, slice): + length = len(six.next(six.itervalues(batches))) + return [{key: batch[i] + for key, batch in six.iteritems(batches)} + for i in six.moves.range(length)] + else: + return batches + + def __len__(self): + return self._length + + +class SliceDataset(Dataset): + def __init__(self, dataset, start, finish, order=None): + """A Dataset which is a slice of the base dataset. + + Args: + dataset (Dataset): the base dataset. + start (int): the start of the slice. + finish (int): the end of the slice, not inclusive. + order (List[int], optional): the order, it is a permutation of the valid example ids of the base dataset. If `order` is provided, the slice is taken in `order`. Defaults to None. + """ + if start < 0 or finish > len(dataset): + raise ValueError("subset overruns the dataset.") + self._dataset = dataset + self._start = start + self._finish = finish + self._size = finish - start + + if order is not None and len(order) != len(dataset): + raise ValueError( + "order should have the same length as the dataset" + "len(order) = {} which does not euqals len(dataset) = {} ". + format(len(order), len(dataset))) + self._order = order + + def __len__(self): + return self._size + + def __getitem__(self, i): + if i >= 0: + if i >= self._size: + raise IndexError('dataset index out of range') + index = self._start + i + else: + if i < -self._size: + raise IndexError('dataset index out of range') + index = self._finish + i + + if self._order is not None: + index = self._order[index] + return self._dataset[index] + + +class SubsetDataset(Dataset): + def __init__(self, dataset, indices): + """A Dataset which is a subset of the base dataset. + + Args: + dataset (Dataset): the base dataset. + indices (Iterable[int]): the indices of the examples to pick. + """ + self._dataset = dataset + if len(indices) > len(dataset): + raise ValueError("subset's size larger that dataset's size!") + self._indices = indices + self._size = len(indices) + + def __len__(self): + return self._size + + def __getitem__(self, i): + index = self._indices[i] + return self._dataset[index] + + +class FilterDataset(Dataset): + def __init__(self, dataset, filter_fn): + """A filtered dataset. + + Args: + dataset (Dataset): the base dataset. + filter_fn (callable): a callable which takes an example of the base dataset and return a boolean. + """ + self._dataset = dataset + self._indices = [ + i for i in range(len(dataset)) if filter_fn(dataset[i]) + ] + self._size = len(self._indices) + + def __len__(self): + return self._size + + def __getitem__(self, i): + index = self._indices[i] + return self._dataset[index] + + +class ChainDataset(Dataset): + def __init__(self, *datasets): + """A concatenation of the several datasets which the same structure. + + Args: + datasets (Iterable[Dataset]): datasets to concat. + """ + self._datasets = datasets + + def __len__(self): + return sum(len(dataset) for dataset in self._datasets) + + def __getitem__(self, i): + if i < 0: + raise IndexError("ChainDataset doesnot support negative indexing.") + + for dataset in self._datasets: + if i < len(dataset): + return dataset[i] + i -= len(dataset) + + raise IndexError("dataset index out of range") diff --git a/ernie-sat/paddlespeech/t2s/datasets/get_feats.py b/ernie-sat/paddlespeech/t2s/datasets/get_feats.py new file mode 100644 index 0000000..a38cfff --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/get_feats.py @@ -0,0 +1,226 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import librosa +import numpy as np +import pyworld +from scipy.interpolate import interp1d + + +class LogMelFBank(): + def __init__(self, + sr=24000, + n_fft=2048, + hop_length=300, + win_length=None, + window="hann", + n_mels=80, + fmin=80, + fmax=7600, + eps=1e-10): + self.sr = sr + # stft + self.n_fft = n_fft + self.win_length = win_length + self.hop_length = hop_length + self.window = window + self.center = True + self.pad_mode = "reflect" + + # mel + self.n_mels = n_mels + self.fmin = 0 if fmin is None else fmin + self.fmax = sr / 2 if fmax is None else fmax + + self.mel_filter = self._create_mel_filter() + + def _create_mel_filter(self): + mel_filter = librosa.filters.mel( + sr=self.sr, + n_fft=self.n_fft, + n_mels=self.n_mels, + fmin=self.fmin, + fmax=self.fmax) + return mel_filter + + def _stft(self, wav): + D = librosa.core.stft( + wav, + n_fft=self.n_fft, + hop_length=self.hop_length, + win_length=self.win_length, + window=self.window, + center=self.center, + pad_mode=self.pad_mode) + f = open('/mnt/home/xiaoran/projects/wave_summit/espnet_dual_mask/tmp_var_stft.out.1', 'w') + print('stft shape is', D.size()) + # for item in [round(item, 6) for item in output["speech"][0].tolist()]: + # f.write(str(item)+'\n') + # f.close() + return D + + def _spectrogram(self, wav): + D = self._stft(wav) + return np.abs(D) + + def _mel_spectrogram(self, wav): + S = self._spectrogram(wav) + mel = np.dot(self.mel_filter, S) + return mel + + # We use different definition for log-spec between TTS and ASR + # TTS: log_10(abs(stft)) + # ASR: log_e(power(stft)) + + def get_log_mel_fbank(self, wav, base='10'): + mel = self._mel_spectrogram(wav) + mel = np.clip(mel, a_min=1e-10, a_max=float("inf")) + if base == '10': + mel = np.log10(mel.T) + elif base == 'e': + mel = np.log(mel.T) + # (num_frames, n_mels) + return mel + + +class Pitch(): + def __init__(self, sr=24000, hop_length=300, f0min=80, f0max=7600): + + self.sr = sr + self.hop_length = hop_length + self.f0min = f0min + self.f0max = f0max + + def _convert_to_continuous_f0(self, f0: np.array) -> np.array: + if (f0 == 0).all(): + print("All frames seems to be unvoiced.") + return f0 + + # padding start and end of f0 sequence + start_f0 = f0[f0 != 0][0] + end_f0 = f0[f0 != 0][-1] + start_idx = np.where(f0 == start_f0)[0][0] + end_idx = np.where(f0 == end_f0)[0][-1] + f0[:start_idx] = start_f0 + f0[end_idx:] = end_f0 + + # get non-zero frame index + nonzero_idxs = np.where(f0 != 0)[0] + + # perform linear interpolation + interp_fn = interp1d(nonzero_idxs, f0[nonzero_idxs]) + f0 = interp_fn(np.arange(0, f0.shape[0])) + + return f0 + + def _calculate_f0(self, + input: np.array, + use_continuous_f0=True, + use_log_f0=True) -> np.array: + input = input.astype(np.float) + frame_period = 1000 * self.hop_length / self.sr + f0, timeaxis = pyworld.dio( + input, + fs=self.sr, + f0_floor=self.f0min, + f0_ceil=self.f0max, + frame_period=frame_period) + f0 = pyworld.stonemask(input, f0, timeaxis, self.sr) + if use_continuous_f0: + f0 = self._convert_to_continuous_f0(f0) + if use_log_f0: + nonzero_idxs = np.where(f0 != 0)[0] + f0[nonzero_idxs] = np.log(f0[nonzero_idxs]) + return f0.reshape(-1) + + def _average_by_duration(self, input: np.array, d: np.array) -> np.array: + d_cumsum = np.pad(d.cumsum(0), (1, 0), 'constant') + arr_list = [] + for start, end in zip(d_cumsum[:-1], d_cumsum[1:]): + arr = input[start:end] + mask = arr == 0 + arr[mask] = 0 + avg_arr = np.mean(arr, axis=0) if len(arr) != 0 else np.array(0) + arr_list.append(avg_arr) + # shape (T,1) + arr_list = np.expand_dims(np.array(arr_list), 0).T + + return arr_list + + def get_pitch(self, + wav, + use_continuous_f0=True, + use_log_f0=True, + use_token_averaged_f0=True, + duration=None): + f0 = self._calculate_f0(wav, use_continuous_f0, use_log_f0) + if use_token_averaged_f0 and duration is not None: + f0 = self._average_by_duration(f0, duration) + return f0 + + +class Energy(): + def __init__(self, + sr=24000, + n_fft=2048, + hop_length=300, + win_length=None, + window="hann", + center=True, + pad_mode="reflect"): + + self.sr = sr + self.n_fft = n_fft + self.win_length = win_length + self.hop_length = hop_length + self.window = window + self.center = center + self.pad_mode = pad_mode + + def _stft(self, wav): + D = librosa.core.stft( + wav, + n_fft=self.n_fft, + hop_length=self.hop_length, + win_length=self.win_length, + window=self.window, + center=self.center, + pad_mode=self.pad_mode) + return D + + def _calculate_energy(self, input): + input = input.astype(np.float32) + input_stft = self._stft(input) + input_power = np.abs(input_stft)**2 + energy = np.sqrt( + np.clip( + np.sum(input_power, axis=0), a_min=1.0e-10, a_max=float('inf'))) + return energy + + def _average_by_duration(self, input: np.array, d: np.array) -> np.array: + d_cumsum = np.pad(d.cumsum(0), (1, 0), 'constant') + arr_list = [] + for start, end in zip(d_cumsum[:-1], d_cumsum[1:]): + arr = input[start:end] + avg_arr = np.mean(arr, axis=0) if len(arr) != 0 else np.array(0) + arr_list.append(avg_arr) + # shape (T,1) + arr_list = np.expand_dims(np.array(arr_list), 0).T + return arr_list + + def get_energy(self, wav, use_token_averaged_energy=True, duration=None): + energy = self._calculate_energy(wav) + if use_token_averaged_energy and duration is not None: + energy = self._average_by_duration(energy, duration) + return energy diff --git a/ernie-sat/paddlespeech/t2s/datasets/ljspeech.py b/ernie-sat/paddlespeech/t2s/datasets/ljspeech.py new file mode 100644 index 0000000..85cc3c1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/ljspeech.py @@ -0,0 +1,39 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from pathlib import Path + +from paddle.io import Dataset + +__all__ = ["LJSpeechMetaData"] + + +class LJSpeechMetaData(Dataset): + def __init__(self, root): + self.root = Path(root).expanduser() + wav_dir = self.root / "wavs" + csv_path = self.root / "metadata.csv" + records = [] + speaker_name = "ljspeech" + with open(str(csv_path), 'rt', encoding='utf-8') as f: + for line in f: + filename, _, normalized_text = line.strip().split("|") + filename = str(wav_dir / (filename + ".wav")) + records.append([filename, normalized_text, speaker_name]) + self.records = records + + def __getitem__(self, i): + return self.records[i] + + def __len__(self): + return len(self.records) diff --git a/ernie-sat/paddlespeech/t2s/datasets/preprocess_utils.py b/ernie-sat/paddlespeech/t2s/datasets/preprocess_utils.py new file mode 100644 index 0000000..445b69b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/preprocess_utils.py @@ -0,0 +1,169 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + + +# speaker|utt_id|phn dur phn dur ... +def get_phn_dur(file_name): + ''' + read MFA duration.txt + Args: + file_name (str or Path): path of gen_duration_from_textgrid.py's result + Returns: + Dict: sentence: {'utt': ([char], [int])} + ''' + f = open(file_name, 'r') + sentence = {} + speaker_set = set() + for line in f: + line_list = line.strip().split('|') + utt = line_list[0] + speaker = line_list[1] + p_d = line_list[-1] + speaker_set.add(speaker) + phn_dur = p_d.split() + phn = phn_dur[::2] + dur = phn_dur[1::2] + assert len(phn) == len(dur) + sentence[utt] = (phn, [int(i) for i in dur], speaker) + f.close() + return sentence, speaker_set + + +def merge_silence(sentence): + ''' + merge silences + Args: + sentence (Dict): sentence: {'utt': (([char], [int]), str)} + ''' + for utt in sentence: + cur_phn, cur_dur, speaker = sentence[utt] + new_phn = [] + new_dur = [] + + # merge sp and sil + for i, p in enumerate(cur_phn): + if i > 0 and 'sil' == p and cur_phn[i - 1] in {"sil", "sp"}: + new_dur[-1] += cur_dur[i] + new_phn[-1] = 'sil' + else: + new_phn.append(p) + new_dur.append(cur_dur[i]) + + for i, (p, d) in enumerate(zip(new_phn, new_dur)): + if p in {"sp"}: + if d < 14: + new_phn[i] = 'sp' + else: + new_phn[i] = 'spl' + + assert len(new_phn) == len(new_dur) + sentence[utt] = [new_phn, new_dur, speaker] + + +def get_input_token(sentence, output_path, dataset="baker"): + ''' + get phone set from training data and save it + Args: + sentence (Dict): sentence: {'utt': ([char], [int])} + output_path (str or path):path to save phone_id_map + ''' + phn_token = set() + for utt in sentence: + for phn in sentence[utt][0]: + phn_token.add(phn) + phn_token = list(phn_token) + phn_token.sort() + phn_token = ["", ""] + phn_token + if dataset in {"baker", "aishell3"}: + phn_token += [",", "。", "?", "!"] + else: + phn_token += [",", ".", "?", "!"] + phn_token += [""] + + with open(output_path, 'w') as f: + for i, phn in enumerate(phn_token): + f.write(phn + ' ' + str(i) + '\n') + + +def get_phones_tones(sentence, + phones_output_path, + tones_output_path, + dataset="baker"): + ''' + get phone set and tone set from training data and save it + Args: + sentence (Dict): sentence: {'utt': ([char], [int])} + phones_output_path (str or path): path to save phone_id_map + tones_output_path (str or path): path to save tone_id_map + ''' + phn_token = set() + tone_token = set() + for utt in sentence: + for label in sentence[utt][0]: + # split tone from finals + match = re.match(r'^(\w+)([012345])$', label) + if match: + phn_token.add(match.group(1)) + tone_token.add(match.group(2)) + else: + phn_token.add(label) + tone_token.add('0') + phn_token = list(phn_token) + tone_token = list(tone_token) + phn_token.sort() + tone_token.sort() + phn_token = ["", ""] + phn_token + if dataset in {"baker", "aishell3"}: + phn_token += [",", "。", "?", "!"] + else: + phn_token += [",", ".", "?", "!"] + phn_token += [""] + + with open(phones_output_path, 'w') as f: + for i, phn in enumerate(phn_token): + f.write(phn + ' ' + str(i) + '\n') + with open(tones_output_path, 'w') as f: + for i, tone in enumerate(tone_token): + f.write(tone + ' ' + str(i) + '\n') + + +def get_spk_id_map(speaker_set, output_path): + speakers = sorted(list(speaker_set)) + with open(output_path, 'w') as f: + for i, spk in enumerate(speakers): + f.write(spk + ' ' + str(i) + '\n') + + +def compare_duration_and_mel_length(sentences, utt, mel): + ''' + check duration error, correct sentences[utt] if possible, else pop sentences[utt] + Args: + sentences (Dict): sentences[utt] = [phones_list ,durations_list] + utt (str): utt_id + mel (np.ndarry): features (num_frames, n_mels) + ''' + + if utt in sentences: + len_diff = mel.shape[0] - sum(sentences[utt][1]) + if len_diff != 0: + if len_diff > 0: + sentences[utt][1][-1] += len_diff + elif sentences[utt][1][-1] + len_diff > 0: + sentences[utt][1][-1] += len_diff + elif sentences[utt][1][0] + len_diff > 0: + sentences[utt][1][0] += len_diff + else: + print("the len_diff is unable to correct:", len_diff) + sentences.pop(utt) diff --git a/ernie-sat/paddlespeech/t2s/datasets/vocoder_batch_fn.py b/ernie-sat/paddlespeech/t2s/datasets/vocoder_batch_fn.py new file mode 100644 index 0000000..08748de --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/datasets/vocoder_batch_fn.py @@ -0,0 +1,220 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle + +from paddlespeech.t2s.audio.codec import encode_mu_law +from paddlespeech.t2s.audio.codec import float_2_label +from paddlespeech.t2s.audio.codec import label_2_float + + +class Clip(object): + """Collate functor for training vocoders. + """ + + def __init__( + self, + batch_max_steps=20480, + hop_size=256, + aux_context_window=0, ): + """Initialize customized collater for DataLoader. + Args: + + batch_max_steps (int): The maximum length of input signal in batch. + hop_size (int): Hop size of auxiliary features. + aux_context_window (int): Context window size for auxiliary feature conv. + + """ + if batch_max_steps % hop_size != 0: + batch_max_steps += -(batch_max_steps % hop_size) + assert batch_max_steps % hop_size == 0 + self.batch_max_steps = batch_max_steps + self.batch_max_frames = batch_max_steps // hop_size + self.hop_size = hop_size + self.aux_context_window = aux_context_window + + # set useful values in random cutting + self.start_offset = aux_context_window + self.end_offset = -(self.batch_max_frames + aux_context_window) + self.mel_threshold = self.batch_max_frames + 2 * aux_context_window + + def __call__(self, batch): + """Convert into batch tensors. + + Args: + batch (list): list of tuple of the pair of audio and features. Audio shape (T, ), features shape(T', C). + + Returns: + Tensor: + Auxiliary feature batch (B, C, T'), where + T = (T' - 2 * aux_context_window) * hop_size. + Tensor: + Target signal batch (B, 1, T). + + """ + # check length + batch = [ + self._adjust_length(b['wave'], b['feats']) for b in batch + if b['feats'].shape[0] > self.mel_threshold + ] + xs, cs = [b[0] for b in batch], [b[1] for b in batch] + + # make batch with random cut + c_lengths = [c.shape[0] for c in cs] + start_frames = np.array([ + np.random.randint(self.start_offset, cl + self.end_offset) + for cl in c_lengths + ]) + x_starts = start_frames * self.hop_size + x_ends = x_starts + self.batch_max_steps + + c_starts = start_frames - self.aux_context_window + c_ends = start_frames + self.batch_max_frames + self.aux_context_window + y_batch = np.stack( + [x[start:end] for x, start, end in zip(xs, x_starts, x_ends)]) + c_batch = np.stack( + [c[start:end] for c, start, end in zip(cs, c_starts, c_ends)]) + + # convert each batch to tensor, assume that each item in batch has the same length + y_batch = paddle.to_tensor( + y_batch, dtype=paddle.float32).unsqueeze(1) # (B, 1, T) + c_batch = paddle.to_tensor( + c_batch, dtype=paddle.float32).transpose([0, 2, 1]) # (B, C, T') + + return y_batch, c_batch + + def _adjust_length(self, x, c): + """Adjust the audio and feature lengths. + + Note: + Basically we assume that the length of x and c are adjusted + through preprocessing stage, but if we use other library processed + features, this process will be needed. + + """ + if len(x) < c.shape[0] * self.hop_size: + x = np.pad(x, (0, c.shape[0] * self.hop_size - len(x)), mode="edge") + elif len(x) > c.shape[0] * self.hop_size: + # print( + # f"wave length: ({len(x)}), mel length: ({c.shape[0]}), hop size: ({self.hop_size })" + # ) + x = x[:c.shape[0] * self.hop_size] + + # check the legnth is valid + assert len(x) == c.shape[ + 0] * self.hop_size, f"wave length: ({len(x)}), mel length: ({c.shape[0]})" + + return x, c + + +class WaveRNNClip(Clip): + def __init__(self, + mode: str='RAW', + batch_max_steps: int=4500, + hop_size: int=300, + aux_context_window: int=2, + bits: int=9, + mu_law: bool=True): + self.mode = mode + self.mel_win = batch_max_steps // hop_size + 2 * aux_context_window + self.batch_max_steps = batch_max_steps + self.hop_size = hop_size + self.aux_context_window = aux_context_window + self.mu_law = mu_law + self.batch_max_frames = batch_max_steps // hop_size + self.mel_threshold = self.batch_max_frames + 2 * aux_context_window + if self.mode == 'MOL': + self.bits = 16 + else: + self.bits = bits + + def to_quant(self, wav): + if self.mode == 'RAW': + if self.mu_law: + quant = encode_mu_law(wav, mu=2**self.bits) + else: + quant = float_2_label(wav, bits=self.bits) + elif self.mode == 'MOL': + quant = float_2_label(wav, bits=16) + quant = quant.astype(np.int64) + return quant + + def __call__(self, batch): + # voc_pad = 2 this will pad the input so that the resnet can 'see' wider than input length + # max_offsets = n_frames - 2 - (mel_win + 2 * hp.voc_pad) = n_frames - 15 + """Convert into batch tensors. + Args: + batch (list): list of tuple of the pair of audio and features. Audio shape (T, ), features shape(T', C). + + Returns: + Tensor: Input signal batch (B, 1, T). + Tensor: Target signal batch (B, 1, T). + Tensor: Auxiliary feature batch (B, C, T'), + where T = (T' - 2 * aux_context_window) * hop_size. + + """ + # check length + batch = [ + self._adjust_length(b['wave'], b['feats']) for b in batch + if b['feats'].shape[0] > self.mel_threshold + ] + wav, mel = [b[0] for b in batch], [b[1] for b in batch] + # mel 此处需要转置 + mel = [x.T for x in mel] + max_offsets = [ + x.shape[-1] - 2 - (self.mel_win + 2 * self.aux_context_window) + for x in mel + ] + # the slice point of mel selecting randomly + mel_offsets = [np.random.randint(0, offset) for offset in max_offsets] + # the slice point of wav selecting randomly, which is behind 2(=pad) frames + sig_offsets = [(offset + self.aux_context_window) * self.hop_size + for offset in mel_offsets] + # mels.shape[1] = voc_seq_len // hop_length + 2 * voc_pad + mels = [ + x[:, mel_offsets[i]:mel_offsets[i] + self.mel_win] + for i, x in enumerate(mel) + ] + # label.shape[1] = voc_seq_len + 1 + wav = [self.to_quant(x) for x in wav] + + labels = [ + x[sig_offsets[i]:sig_offsets[i] + self.batch_max_steps + 1] + for i, x in enumerate(wav) + ] + + mels = np.stack(mels).astype(np.float32) + labels = np.stack(labels).astype(np.int64) + + mels = paddle.to_tensor(mels) + labels = paddle.to_tensor(labels, dtype='int64') + # x is input, y is label + x = labels[:, :self.batch_max_steps] + y = labels[:, 1:] + ''' + mode = RAW: + mu_law = True: + quant: bits = 9 0, 1, 2, ..., 509, 510, 511 int + mu_law = False + quant bits = 9 [0, 511] float + mode = MOL: + quant: bits = 16 [0. 65536] float + ''' + # x should be normalizes in.[0, 1] in RAW mode + x = label_2_float(paddle.cast(x, dtype='float32'), self.bits) + # y should be normalizes in.[0, 1] in MOL mode + if self.mode == 'MOL': + y = label_2_float(paddle.cast(y, dtype='float32'), self.bits) + + return x, y, mels diff --git a/ernie-sat/paddlespeech/t2s/exps/__init__.py b/ernie-sat/paddlespeech/t2s/exps/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/csmsc_test.txt b/ernie-sat/paddlespeech/t2s/exps/csmsc_test.txt new file mode 100644 index 0000000..d8cf367 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/csmsc_test.txt @@ -0,0 +1,100 @@ +009901 昨日,这名伤者与医生全部被警方依法刑事拘留。 +009902 钱伟长想到上海来办学校是经过深思熟虑的。 +009903 她见我一进门就骂,吃饭时也骂,骂得我抬不起头。 +009904 李述德在离开之前,只说了一句柱驼杀父亲了。 +009905 这种车票和保险单捆绑出售属于重复性购买。 +009906 戴佩妮的男友西米露接唱情歌,让她非常开心。 +009907 观大势,谋大局,出大策始终是该院的办院方针。 +009908 他们骑着摩托回家,正好为农忙时的父母帮忙。 +009909 但是因为还没到退休年龄,只能掰着指头捱日子。 +009910 这几天雨水不断,人们恨不得待在家里不出门。 +009911 没想到徐赟,张海翔两人就此玩起了人间蒸发。 +009912 藤村此番发言可能是为了凸显野田的领导能力。 +009913 程长庚,生在清王朝嘉庆年间,安徽的潜山小县。 +009914 南海海域综合补给基地码头项目正在论证中。 +009915 也就是说今晚成都市民极有可能再次看到飘雪。 +009916 随着天气转热,各地的游泳场所开始人头攒动。 +009917 更让徐先生纳闷的是,房客的手机也打不通了。 +009918 遇到颠簸时,应听从乘务员的安全指令,回座位坐好。 +009919 他在后面呆惯了,怕自己一插身后的人会不满,不敢排进去。 +009920 傍晚七个小人回来了,白雪公主说,你们就是我命中的七个小矮人吧。 +009921 他本想说,教育局管这个,他们是一路的,这样一管岂不是妓女起嫖客? +009922 一种表示商品所有权的财物证券,也称商品证券,如提货单,交货单。 +009923 会有很丰富的东西留下来,说都说不完。 +009924 这句话像从天而降,吓得四周一片寂静。 +009925 记者所在的是受害人家属所在的右区。 +009926 不管哈大爷去哪,它都一步不离地跟着。 +009927 大家抬头望去,一只老鼠正趴在吊顶上。 +009928 我决定过年就辞职,接手我爸的废品站! +009929 最终,中国男子乒乓球队获得此奖项。 +009930 防汛抗旱两手抓,抗旱相对抓的不够。 +009931 图们江下游地区开发开放的进展如何? +009932 这要求中国必须有一个坚强的政党领导。 +009933 再说,关于利益上的事俺俩都不好开口。 +009934 明代瓦剌,鞑靼入侵明境也是通过此地。 +009935 咪咪舔着孩子,把它身上的毛舔干净。 +009936 是否这次的国标修订被大企业绑架了? +009937 判决后,姚某妻子胡某不服,提起上诉。 +009938 由此可以看出邯钢的经济效益来自何处。 +009939 琳达说,是瑜伽改变了她和马儿的生活。 +009940 楼下的保安告诉记者,这里不租也不卖。 +009941 习近平说,中斯两国人民传统友谊深厚。 +009942 传闻越来越多,后来连老汉儿自己都怕了。 +009943 我怒吼一声冲上去,举起砖头砸了过去。 +009944 我现在还不会,这就回去问问发明我的人。 +009945 显然,洛阳性奴案不具备上述两个前提。 +009946 另外,杰克逊有文唇线,眼线,眉毛的动作。 +009947 昨晚,华西都市报记者电话采访了尹琪。 +009948 涅拉季科未透露这些航空公司的名称。 +009949 从运行轨迹上来说,它也不可能是星星。 +009950 目前看,如果继续加息也存在两难问题。 +009951 曾宝仪在节目录制现场大爆观众糗事。 +009952 但任凭周某怎么叫,男子仍酣睡不醒。 +009953 老大爷说,小子,你挡我财路了,知道不? +009954 没料到,闯下大头佛的阿伟还不知悔改。 +009955 卡扎菲部落式统治已遭遇部落内讧。 +009956 这个孩子的生命一半来源于另一位女士捐赠的冷冻卵子。 +009957 出现这种泥鳅内阁的局面既是野田有意为之,也实属无奈。 +009958 济青高速济南,华山,章丘,邹平,周村,淄博,临淄站。 +009959 赵凌飞的话,反映了沈阳赛区所有奥运志愿者的共同心声。 +009960 因为,我们所发出的力量必会因难度加大而减弱。 +009961 发生事故的楼梯拐角处仍可看到血迹。 +009962 想过进公安,可能身高不够,老汉儿也不让我进去。 +009963 路上关卡很多,为了方便撤离,只好轻装前进。 +009964 原来比尔盖茨就是美国微软公司联合创始人呀。 +009965 之后他们一家三口将与双方父母往峇里岛旅游。 +009966 谢谢总理,也感谢广大网友的参与,我们明年再见。 +009967 事实上是,从来没有一个欺善怕恶的人能作出过稍大一点的成就。 +009968 我会打开邮件,你可以从那里继续。 +009969 美方对近期东海局势表示关切。 +009970 据悉,奥巴马一家人对这座冬季白宫极为满意。 +009971 打扫完你会很有成就感的,试一试,你就信了。 +009972 诺曼站在滑板车上,各就各位,准备出发啦! +009973 塔河的寒夜,气温降到了零下三十多摄氏度。 +009974 其间,连破六点六,六点五,六点四,六点三五等多个重要关口。 +009975 算命其实只是人们的一种自我安慰和自我暗示而已,我们还是要相信科学才好。 +009976 这一切都令人欢欣鼓舞,阿讷西没理由不坚持到最后。 +009977 直至公元前一万一千年,它又再次出现。 +009978 尽量少玩电脑,少看电视,少打游戏。 +009979 从五到七,前后也就是六个月的时间。 +009980 一进咖啡店,他就遇见一张熟悉的脸。 +009981 好在众弟兄看到了把她追了回来。 +009982 有一个人说,哥们儿我们跑过它才能活。 +009983 捅了她以后,模糊记得她没咋动了。 +009984 从小到大,葛启义没有收到过压岁钱。 +009985 舞台下的你会对舞台上的你说什么? +009986 但考生普遍认为,试题的怪多过难。 +009987 我希望每个人都能够尊重我们的隐私。 +009988 漫天的红霞使劲给两人增添气氛。 +009989 晚上加完班开车回家,太累了,迷迷糊糊开着车,走一半的时候,铛一声! +009990 该车将三人撞倒后,在大雾中逃窜。 +009991 这人一哆嗦,方向盘也把不稳了,差点撞上了高速边道护栏。 +009992 那女孩儿委屈的说,我一回头见你已经进去了我不敢进去啊! +009993 小明摇摇头说,不是,我只是美女看多了,想换个口味而已。 +009994 接下来,红娘要求记者交费,记者表示不知表姐身份证号码。 +009995 李东蓊表示,自己当时在法庭上发表了一次独特的公诉意见。 +009996 另一男子扑了上来,手里拿着明晃晃的长刀,向他胸口直刺。 +009997 今天,快递员拿着一个快递在办公室喊,秦王是哪个,有他快递? +009998 这场抗议活动究竟是如何发展演变的,又究竟是谁伤害了谁? +009999 因华国锋肖鸡,墓地设计根据其属相设计。 +010000 在狱中,张明宝悔恨交加,写了一份忏悔书。 diff --git a/ernie-sat/paddlespeech/t2s/exps/fastspeech2/__init__.py b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/fastspeech2/gen_gta_mel.py b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/gen_gta_mel.py new file mode 100644 index 0000000..4c92ad1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/gen_gta_mel.py @@ -0,0 +1,226 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# generate mels using durations.txt +# for mb melgan finetune +import argparse +import os +from pathlib import Path + +import numpy as np +import paddle +import yaml +from tqdm import tqdm +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2 +from paddlespeech.t2s.models.fastspeech2 import StyleFastSpeech2Inference +from paddlespeech.t2s.modules.normalizer import ZScore +from paddlespeech.t2s.utils import str2bool + + +def evaluate(args, fastspeech2_config): + rootdir = Path(args.rootdir).expanduser() + assert rootdir.is_dir() + + # construct dataset for evaluation + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + phone_dict = {} + for phn, id in phn_id: + phone_dict[phn] = int(id) + + if args.speaker_dict: + with open(args.speaker_dict, 'rt') as f: + spk_id_list = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id_list) + else: + spk_num = None + + odim = fastspeech2_config.n_mels + model = FastSpeech2( + idim=vocab_size, + odim=odim, + **fastspeech2_config["model"], + spk_num=spk_num) + + model.set_state_dict( + paddle.load(args.fastspeech2_checkpoint)["main_params"]) + model.eval() + + stat = np.load(args.fastspeech2_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + fastspeech2_normalizer = ZScore(mu, std) + + fastspeech2_inference = StyleFastSpeech2Inference(fastspeech2_normalizer, + model) + fastspeech2_inference.eval() + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + sentences, speaker_set = get_phn_dur(args.dur_file) + merge_silence(sentences) + + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + # split data into 3 sections + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "aishell3": + sub_num_dev = 5 + wav_dir = rootdir / "train" / "wav" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*.wav"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + train_wav_files = [ + os.path.basename(str(str_path)) for str_path in train_wav_files + ] + dev_wav_files = [ + os.path.basename(str(str_path)) for str_path in dev_wav_files + ] + test_wav_files = [ + os.path.basename(str(str_path)) for str_path in test_wav_files + ] + + for i, utt_id in enumerate(tqdm(sentences)): + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + # 裁剪掉开头和结尾的 sil + if args.cut_sil: + if phones[0] == "sil" and len(durations) > 1: + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + durations = durations[:-1] + phones = phones[:-1] + # sentences[utt_id][0] = phones + # sentences[utt_id][1] = durations + + phone_ids = [phone_dict[phn] for phn in phones] + phone_ids = paddle.to_tensor(np.array(phone_ids)) + + if args.speaker_dict: + speaker_id = int( + [item[1] for item in spk_id_list if speaker == item[0]][0]) + speaker_id = paddle.to_tensor(speaker_id) + else: + speaker_id = None + + durations = paddle.to_tensor(np.array(durations)) + # 生成的和真实的可能有 1, 2 帧的差距,但是 batch_fn 会修复 + # split data into 3 sections + + wav_path = utt_id + ".wav" + + if wav_path in train_wav_files: + sub_output_dir = output_dir / ("train/raw") + elif wav_path in dev_wav_files: + sub_output_dir = output_dir / ("dev/raw") + elif wav_path in test_wav_files: + sub_output_dir = output_dir / ("test/raw") + + sub_output_dir.mkdir(parents=True, exist_ok=True) + + with paddle.no_grad(): + mel = fastspeech2_inference( + phone_ids, durations=durations, spk_id=speaker_id) + np.save(sub_output_dir / (utt_id + "_feats.npy"), mel) + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with fastspeech2 & parallel wavegan.") + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker, ljspeech, vctk} now") + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + parser.add_argument( + "--fastspeech2-config", type=str, help="fastspeech2 config file.") + parser.add_argument( + "--fastspeech2-checkpoint", + type=str, + help="fastspeech2 checkpoint to load.") + parser.add_argument( + "--fastspeech2-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training fastspeech2." + ) + + parser.add_argument( + "--phones-dict", + type=str, + default="phone_id_map.txt", + help="phone vocabulary file.") + + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + + parser.add_argument( + "--dur-file", default=None, type=str, help="path to durations.txt.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.fastspeech2_config) as f: + fastspeech2_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(fastspeech2_config) + + evaluate(args, fastspeech2_config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/fastspeech2/normalize.py b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/normalize.py new file mode 100644 index 0000000..8ec20eb --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/normalize.py @@ -0,0 +1,184 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Normalize feature files and dump them.""" +import argparse +import logging +from operator import itemgetter +from pathlib import Path + +import jsonlines +import numpy as np +from sklearn.preprocessing import StandardScaler +from tqdm import tqdm + +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + """Run preprocessing process.""" + parser = argparse.ArgumentParser( + description="Normalize dumped raw features (See detail in parallel_wavegan/bin/normalize.py)." + ) + parser.add_argument( + "--metadata", + type=str, + required=True, + help="directory including feature files to be normalized. " + "you need to specify either *-scp or rootdir.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump normalized feature files.") + parser.add_argument( + "--speech-stats", + type=str, + required=True, + help="speech statistics file.") + parser.add_argument( + "--pitch-stats", type=str, required=True, help="pitch statistics file.") + parser.add_argument( + "--energy-stats", + type=str, + required=True, + help="energy statistics file.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + args = parser.parse_args() + + # set logger + if args.verbose > 1: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + elif args.verbose > 0: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + logging.warning('Skip DEBUG/INFO messages') + + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + # get dataset + with jsonlines.open(args.metadata, 'r') as reader: + metadata = list(reader) + dataset = DataTable( + metadata, + converters={ + "speech": np.load, + "pitch": np.load, + "energy": np.load, + }) + logging.info(f"The number of files = {len(dataset)}.") + + # restore scaler + speech_scaler = StandardScaler() + speech_scaler.mean_ = np.load(args.speech_stats)[0] + speech_scaler.scale_ = np.load(args.speech_stats)[1] + speech_scaler.n_features_in_ = speech_scaler.mean_.shape[0] + + pitch_scaler = StandardScaler() + pitch_scaler.mean_ = np.load(args.pitch_stats)[0] + pitch_scaler.scale_ = np.load(args.pitch_stats)[1] + pitch_scaler.n_features_in_ = pitch_scaler.mean_.shape[0] + + energy_scaler = StandardScaler() + energy_scaler.mean_ = np.load(args.energy_stats)[0] + energy_scaler.scale_ = np.load(args.energy_stats)[1] + energy_scaler.n_features_in_ = energy_scaler.mean_.shape[0] + + vocab_phones = {} + with open(args.phones_dict, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + vocab_phones[phn] = int(id) + + vocab_speaker = {} + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + for spk, id in spk_id: + vocab_speaker[spk] = int(id) + + # process each file + output_metadata = [] + + for item in tqdm(dataset): + utt_id = item['utt_id'] + speech = item['speech'] + pitch = item['pitch'] + energy = item['energy'] + # normalize + speech = speech_scaler.transform(speech) + speech_dir = dumpdir / "data_speech" + speech_dir.mkdir(parents=True, exist_ok=True) + speech_path = speech_dir / f"{utt_id}_speech.npy" + np.save(speech_path, speech.astype(np.float32), allow_pickle=False) + + pitch = pitch_scaler.transform(pitch) + pitch_dir = dumpdir / "data_pitch" + pitch_dir.mkdir(parents=True, exist_ok=True) + pitch_path = pitch_dir / f"{utt_id}_pitch.npy" + np.save(pitch_path, pitch.astype(np.float32), allow_pickle=False) + + energy = energy_scaler.transform(energy) + energy_dir = dumpdir / "data_energy" + energy_dir.mkdir(parents=True, exist_ok=True) + energy_path = energy_dir / f"{utt_id}_energy.npy" + np.save(energy_path, energy.astype(np.float32), allow_pickle=False) + phone_ids = [vocab_phones[p] for p in item['phones']] + spk_id = vocab_speaker[item["speaker"]] + record = { + "utt_id": item['utt_id'], + "spk_id": spk_id, + "text": phone_ids, + "text_lengths": item['text_lengths'], + "speech_lengths": item['speech_lengths'], + "durations": item['durations'], + "speech": str(speech_path), + "pitch": str(pitch_path), + "energy": str(energy_path) + } + # add spk_emb for voice cloning + if "spk_emb" in item: + record["spk_emb"] = str(item["spk_emb"]) + + output_metadata.append(record) + output_metadata.sort(key=itemgetter('utt_id')) + output_metadata_path = Path(args.dumpdir) / "metadata.jsonl" + with jsonlines.open(output_metadata_path, 'w') as writer: + for item in output_metadata: + writer.write(item) + logging.info(f"metadata dumped into {output_metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/fastspeech2/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/preprocess.py new file mode 100644 index 0000000..db1842b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/preprocess.py @@ -0,0 +1,370 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from concurrent.futures import ThreadPoolExecutor +from operator import itemgetter +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines +import librosa +import numpy as np +import tqdm +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.get_feats import Energy +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.datasets.get_feats import Pitch +from paddlespeech.t2s.datasets.preprocess_utils import compare_duration_and_mel_length +from paddlespeech.t2s.datasets.preprocess_utils import get_input_token +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import get_spk_id_map +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.utils import str2bool + + +def process_sentence(config: Dict[str, Any], + fp: Path, + sentences: Dict, + output_dir: Path, + mel_extractor=None, + pitch_extractor=None, + energy_extractor=None, + cut_sil: bool=True, + spk_emb_dir: Path=None): + utt_id = fp.stem + # for vctk + if utt_id.endswith("_mic2"): + utt_id = utt_id[:-5] + record = None + if utt_id in sentences: + # reading, resampling may occur + wav, _ = librosa.load(str(fp), sr=config.fs) + if len(wav.shape) != 1 or np.abs(wav).max() > 1.0: + return record + assert len(wav.shape) == 1, f"{utt_id} is not a mono-channel audio." + assert np.abs(wav).max( + ) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM." + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + d_cumsum = np.pad(np.array(durations).cumsum(0), (1, 0), 'constant') + # little imprecise than use *.TextGrid directly + times = librosa.frames_to_time( + d_cumsum, sr=config.fs, hop_length=config.n_shift) + if cut_sil: + start = 0 + end = d_cumsum[-1] + if phones[0] == "sil" and len(durations) > 1: + start = times[1] + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + end = times[-2] + durations = durations[:-1] + phones = phones[:-1] + sentences[utt_id][0] = phones + sentences[utt_id][1] = durations + start, end = librosa.time_to_samples([start, end], sr=config.fs) + wav = wav[start:end] + # extract mel feats + logmel = mel_extractor.get_log_mel_fbank(wav) + # change duration according to mel_length + compare_duration_and_mel_length(sentences, utt_id, logmel) + # utt_id may be popped in compare_duration_and_mel_length + if utt_id not in sentences: + return None + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + num_frames = logmel.shape[0] + assert sum(durations) == num_frames + mel_dir = output_dir / "data_speech" + mel_dir.mkdir(parents=True, exist_ok=True) + mel_path = mel_dir / (utt_id + "_speech.npy") + np.save(mel_path, logmel) + # extract pitch and energy + f0 = pitch_extractor.get_pitch(wav, duration=np.array(durations)) + assert f0.shape[0] == len(durations) + f0_dir = output_dir / "data_pitch" + f0_dir.mkdir(parents=True, exist_ok=True) + f0_path = f0_dir / (utt_id + "_pitch.npy") + np.save(f0_path, f0) + energy = energy_extractor.get_energy(wav, duration=np.array(durations)) + assert energy.shape[0] == len(durations) + energy_dir = output_dir / "data_energy" + energy_dir.mkdir(parents=True, exist_ok=True) + energy_path = energy_dir / (utt_id + "_energy.npy") + np.save(energy_path, energy) + record = { + "utt_id": utt_id, + "phones": phones, + "text_lengths": len(phones), + "speech_lengths": num_frames, + "durations": durations, + "speech": str(mel_path), + "pitch": str(f0_path), + "energy": str(energy_path), + "speaker": speaker + } + if spk_emb_dir: + if speaker in os.listdir(spk_emb_dir): + embed_name = utt_id + ".npy" + embed_path = spk_emb_dir / speaker / embed_name + if embed_path.is_file(): + record["spk_emb"] = str(embed_path) + else: + return None + return record + + +def process_sentences(config, + fps: List[Path], + sentences: Dict, + output_dir: Path, + mel_extractor=None, + pitch_extractor=None, + energy_extractor=None, + nprocs: int=1, + cut_sil: bool=True, + spk_emb_dir: Path=None): + if nprocs == 1: + results = [] + for fp in fps: + record = process_sentence(config, fp, sentences, output_dir, + mel_extractor, pitch_extractor, + energy_extractor, cut_sil, spk_emb_dir) + if record: + results.append(record) + else: + with ThreadPoolExecutor(nprocs) as pool: + futures = [] + with tqdm.tqdm(total=len(fps)) as progress: + for fp in fps: + future = pool.submit(process_sentence, config, fp, + sentences, output_dir, mel_extractor, + pitch_extractor, energy_extractor, + cut_sil, spk_emb_dir) + future.add_done_callback(lambda p: progress.update()) + futures.append(future) + + results = [] + for ft in futures: + record = ft.result() + if record: + results.append(record) + + results.sort(key=itemgetter("utt_id")) + with jsonlines.open(output_dir / "metadata.jsonl", 'w') as writer: + for item in results: + writer.write(item) + print("Done") + + +def main(): + # parse config and args + parser = argparse.ArgumentParser( + description="Preprocess audio and then extract features.") + + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker, aishell3, ljspeech, vctk} now") + + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump feature files.") + parser.add_argument( + "--dur-file", default=None, type=str, help="path to durations.txt.") + + parser.add_argument("--config", type=str, help="fastspeech2 config file.") + + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + parser.add_argument( + "--num-cpu", type=int, default=1, help="number of process.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + + parser.add_argument( + "--spk_emb_dir", + default=None, + type=str, + help="directory to speaker embedding files.") + args = parser.parse_args() + + rootdir = Path(args.rootdir).expanduser() + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + dur_file = Path(args.dur_file).expanduser() + + if args.spk_emb_dir: + spk_emb_dir = Path(args.spk_emb_dir).expanduser().resolve() + else: + spk_emb_dir = None + + assert rootdir.is_dir() + assert dur_file.is_file() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + if args.verbose > 1: + print(vars(args)) + print(config) + + sentences, speaker_set = get_phn_dur(dur_file) + + merge_silence(sentences) + phone_id_map_path = dumpdir / "phone_id_map.txt" + speaker_id_map_path = dumpdir / "speaker_id_map.txt" + get_input_token(sentences, phone_id_map_path, args.dataset) + get_spk_id_map(speaker_set, speaker_id_map_path) + + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + # split data into 3 sections + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "aishell3": + sub_num_dev = 5 + wav_dir = rootdir / "train" / "wav" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*.wav"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + elif args.dataset == "ljspeech": + wav_files = sorted(list((rootdir / "wavs").rglob("*.wav"))) + # split data into 3 sections + num_train = 12900 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "vctk": + sub_num_dev = 5 + wav_dir = rootdir / "wav48_silence_trimmed" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*_mic2.flac"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + else: + print("dataset should in {baker, aishell3, ljspeech, vctk} now!") + + train_dump_dir = dumpdir / "train" / "raw" + train_dump_dir.mkdir(parents=True, exist_ok=True) + dev_dump_dir = dumpdir / "dev" / "raw" + dev_dump_dir.mkdir(parents=True, exist_ok=True) + test_dump_dir = dumpdir / "test" / "raw" + test_dump_dir.mkdir(parents=True, exist_ok=True) + + # Extractor + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + pitch_extractor = Pitch( + sr=config.fs, + hop_length=config.n_shift, + f0min=config.f0min, + f0max=config.f0max) + energy_extractor = Energy( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window) + + # process for the 3 sections + if train_wav_files: + process_sentences( + config, + train_wav_files, + sentences, + train_dump_dir, + mel_extractor, + pitch_extractor, + energy_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + if dev_wav_files: + process_sentences( + config, + dev_wav_files, + sentences, + dev_dump_dir, + mel_extractor, + pitch_extractor, + energy_extractor, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + if test_wav_files: + process_sentences( + config, + test_wav_files, + sentences, + test_dump_dir, + mel_extractor, + pitch_extractor, + energy_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/fastspeech2/train.py b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/train.py new file mode 100644 index 0000000..10e023d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/fastspeech2/train.py @@ -0,0 +1,212 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.am_batch_fn import fastspeech2_multi_spk_batch_fn +from paddlespeech.t2s.datasets.am_batch_fn import fastspeech2_single_spk_batch_fn +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2 +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2Evaluator +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2Updater +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.optimizer import build_optimizers +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.t2s.utils import str2bool + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + world_size = paddle.distributed.get_world_size() + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + fields = [ + "text", "text_lengths", "speech", "speech_lengths", "durations", + "pitch", "energy" + ] + converters = {"speech": np.load, "pitch": np.load, "energy": np.load} + spk_num = None + if args.speaker_dict is not None: + print("multiple speaker fastspeech2!") + collate_fn = fastspeech2_multi_spk_batch_fn + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + fields += ["spk_id"] + elif args.voice_cloning: + print("Training voice cloning!") + collate_fn = fastspeech2_multi_spk_batch_fn + fields += ["spk_emb"] + converters["spk_emb"] = np.load + else: + print("single speaker fastspeech2!") + collate_fn = fastspeech2_single_spk_batch_fn + print("spk_num:", spk_num) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=fields, + converters=converters, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=fields, + converters=converters, ) + + # collate function and dataloader + + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + + print("samplers done!") + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=collate_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + shuffle=False, + drop_last=False, + batch_size=config.batch_size, + collate_fn=collate_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + odim = config.n_mels + model = FastSpeech2( + idim=vocab_size, odim=odim, spk_num=spk_num, **config["model"]) + if world_size > 1: + model = DataParallel(model) + print("model done!") + + optimizer = build_optimizers(model, **config["optimizer"]) + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = FastSpeech2Updater( + model=model, + optimizer=optimizer, + dataloader=train_dataloader, + output_dir=output_dir, + **config["updater"]) + + trainer = Trainer(updater, (config.max_epoch, 'epoch'), output_dir) + + evaluator = FastSpeech2Evaluator( + model, dev_dataloader, output_dir=output_dir, **config["updater"]) + + if dist.get_rank() == 0: + trainer.extend(evaluator, trigger=(1, "epoch")) + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + trainer.extend( + Snapshot(max_size=config.num_snapshots), trigger=(1, 'epoch')) + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Train a FastSpeech2 model.") + parser.add_argument("--config", type=str, help="fastspeech2 config file.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--speaker-dict", + type=str, + default=None, + help="speaker id map file for multiple speaker model.") + + parser.add_argument( + "--voice-cloning", + type=str2bool, + default=False, + help="whether training voice cloning model.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/README.md b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/README.md new file mode 100644 index 0000000..3109be1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/README.md @@ -0,0 +1 @@ +different GAN Vocoders have the same preprocess.py and normalize.py diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/__init__.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/__init__.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/train.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/train.py new file mode 100644 index 0000000..c70821e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/hifigan/train.py @@ -0,0 +1,275 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle import nn +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.optimizer import Adam +from paddle.optimizer.lr import MultiStepDecay +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.datasets.vocoder_batch_fn import Clip +from paddlespeech.t2s.models.hifigan import HiFiGANEvaluator +from paddlespeech.t2s.models.hifigan import HiFiGANGenerator +from paddlespeech.t2s.models.hifigan import HiFiGANMultiScaleMultiPeriodDiscriminator +from paddlespeech.t2s.models.hifigan import HiFiGANUpdater +from paddlespeech.t2s.modules.losses import DiscriminatorAdversarialLoss +from paddlespeech.t2s.modules.losses import FeatureMatchLoss +from paddlespeech.t2s.modules.losses import GeneratorAdversarialLoss +from paddlespeech.t2s.modules.losses import MelSpectrogramLoss +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + dev_sampler = DistributedBatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + print("samplers done!") + + if "aux_context_window" in config.generator_params: + aux_context_window = config.generator_params.aux_context_window + else: + aux_context_window = 0 + train_batch_fn = Clip( + batch_max_steps=config.batch_max_steps, + hop_size=config.n_shift, + aux_context_window=aux_context_window) + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + batch_sampler=dev_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + generator = HiFiGANGenerator(**config["generator_params"]) + discriminator = HiFiGANMultiScaleMultiPeriodDiscriminator( + **config["discriminator_params"]) + if world_size > 1: + generator = DataParallel(generator) + discriminator = DataParallel(discriminator) + print("models done!") + + criterion_feat_match = FeatureMatchLoss(**config["feat_match_loss_params"]) + criterion_mel = MelSpectrogramLoss( + fs=config.fs, + fft_size=config.n_fft, + hop_size=config.n_shift, + win_length=config.win_length, + window=config.window, + num_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax, ) + criterion_gen_adv = GeneratorAdversarialLoss( + **config["generator_adv_loss_params"]) + criterion_dis_adv = DiscriminatorAdversarialLoss( + **config["discriminator_adv_loss_params"]) + print("criterions done!") + + lr_schedule_g = MultiStepDecay(**config["generator_scheduler_params"]) + # Compared to multi_band_melgan.v1 config, Adam optimizer without gradient norm is used + generator_grad_norm = config["generator_grad_norm"] + gradient_clip_g = nn.ClipGradByGlobalNorm( + generator_grad_norm) if generator_grad_norm > 0 else None + print("gradient_clip_g:", gradient_clip_g) + + optimizer_g = Adam( + learning_rate=lr_schedule_g, + grad_clip=gradient_clip_g, + parameters=generator.parameters(), + **config["generator_optimizer_params"]) + lr_schedule_d = MultiStepDecay(**config["discriminator_scheduler_params"]) + discriminator_grad_norm = config["discriminator_grad_norm"] + gradient_clip_d = nn.ClipGradByGlobalNorm( + discriminator_grad_norm) if discriminator_grad_norm > 0 else None + print("gradient_clip_d:", gradient_clip_d) + optimizer_d = Adam( + learning_rate=lr_schedule_d, + grad_clip=gradient_clip_d, + parameters=discriminator.parameters(), + **config["discriminator_optimizer_params"]) + print("optimizers done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = HiFiGANUpdater( + models={ + "generator": generator, + "discriminator": discriminator, + }, + optimizers={ + "generator": optimizer_g, + "discriminator": optimizer_d, + }, + criterions={ + "mel": criterion_mel, + "feat_match": criterion_feat_match, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + }, + schedulers={ + "generator": lr_schedule_g, + "discriminator": lr_schedule_d, + }, + dataloader=train_dataloader, + discriminator_train_start_steps=config.discriminator_train_start_steps, + # only hifigan have generator_train_start_steps + generator_train_start_steps=config.generator_train_start_steps, + lambda_adv=config.lambda_adv, + lambda_aux=config.lambda_aux, + lambda_feat_match=config.lambda_feat_match, + output_dir=output_dir) + + evaluator = HiFiGANEvaluator( + models={ + "generator": generator, + "discriminator": discriminator, + }, + criterions={ + "mel": criterion_mel, + "feat_match": criterion_feat_match, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + }, + dataloader=dev_dataloader, + lambda_adv=config.lambda_adv, + lambda_aux=config.lambda_aux, + lambda_feat_match=config.lambda_feat_match, + output_dir=output_dir) + + trainer = Trainer( + updater, + stop_trigger=(config.train_max_steps, "iteration"), + out=output_dir) + + if dist.get_rank() == 0: + trainer.extend( + evaluator, trigger=(config.eval_interval_steps, 'iteration')) + trainer.extend(VisualDL(output_dir), trigger=(1, 'iteration')) + trainer.extend( + Snapshot(max_size=config.num_snapshots), + trigger=(config.save_interval_steps, 'iteration')) + + print("Trainer Done!") + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + + parser = argparse.ArgumentParser(description="Train a HiFiGAN model.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/__init__.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/train.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/train.py new file mode 100644 index 0000000..27ffded --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/multi_band_melgan/train.py @@ -0,0 +1,264 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle import nn +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.optimizer import Adam +from paddle.optimizer.lr import MultiStepDecay +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.datasets.vocoder_batch_fn import Clip +from paddlespeech.t2s.models.melgan import MBMelGANEvaluator +from paddlespeech.t2s.models.melgan import MBMelGANUpdater +from paddlespeech.t2s.models.melgan import MelGANGenerator +from paddlespeech.t2s.models.melgan import MelGANMultiScaleDiscriminator +from paddlespeech.t2s.modules.losses import DiscriminatorAdversarialLoss +from paddlespeech.t2s.modules.losses import GeneratorAdversarialLoss +from paddlespeech.t2s.modules.losses import MultiResolutionSTFTLoss +from paddlespeech.t2s.modules.pqmf import PQMF +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + dev_sampler = DistributedBatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + print("samplers done!") + + if "aux_context_window" in config.generator_params: + aux_context_window = config.generator_params.aux_context_window + else: + aux_context_window = 0 + train_batch_fn = Clip( + batch_max_steps=config.batch_max_steps, + hop_size=config.n_shift, + aux_context_window=aux_context_window) + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + batch_sampler=dev_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + generator = MelGANGenerator(**config["generator_params"]) + discriminator = MelGANMultiScaleDiscriminator( + **config["discriminator_params"]) + if world_size > 1: + generator = DataParallel(generator) + discriminator = DataParallel(discriminator) + print("models done!") + criterion_stft = MultiResolutionSTFTLoss(**config["stft_loss_params"]) + criterion_sub_stft = MultiResolutionSTFTLoss( + **config["subband_stft_loss_params"]) + criterion_gen_adv = GeneratorAdversarialLoss() + criterion_dis_adv = DiscriminatorAdversarialLoss() + # define special module for subband processing + criterion_pqmf = PQMF(subbands=config["generator_params"]["out_channels"]) + print("criterions done!") + + lr_schedule_g = MultiStepDecay(**config["generator_scheduler_params"]) + # Compared to multi_band_melgan.v1 config, Adam optimizer without gradient norm is used + generator_grad_norm = config["generator_grad_norm"] + gradient_clip_g = nn.ClipGradByGlobalNorm( + generator_grad_norm) if generator_grad_norm > 0 else None + print("gradient_clip_g:", gradient_clip_g) + + optimizer_g = Adam( + learning_rate=lr_schedule_g, + grad_clip=gradient_clip_g, + parameters=generator.parameters(), + **config["generator_optimizer_params"]) + lr_schedule_d = MultiStepDecay(**config["discriminator_scheduler_params"]) + discriminator_grad_norm = config["discriminator_grad_norm"] + gradient_clip_d = nn.ClipGradByGlobalNorm( + discriminator_grad_norm) if discriminator_grad_norm > 0 else None + print("gradient_clip_d:", gradient_clip_d) + optimizer_d = Adam( + learning_rate=lr_schedule_d, + grad_clip=gradient_clip_d, + parameters=discriminator.parameters(), + **config["discriminator_optimizer_params"]) + print("optimizers done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = MBMelGANUpdater( + models={ + "generator": generator, + "discriminator": discriminator, + }, + optimizers={ + "generator": optimizer_g, + "discriminator": optimizer_d, + }, + criterions={ + "stft": criterion_stft, + "sub_stft": criterion_sub_stft, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + "pqmf": criterion_pqmf + }, + schedulers={ + "generator": lr_schedule_g, + "discriminator": lr_schedule_d, + }, + dataloader=train_dataloader, + discriminator_train_start_steps=config.discriminator_train_start_steps, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + + evaluator = MBMelGANEvaluator( + models={ + "generator": generator, + "discriminator": discriminator, + }, + criterions={ + "stft": criterion_stft, + "sub_stft": criterion_sub_stft, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + "pqmf": criterion_pqmf + }, + dataloader=dev_dataloader, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + + trainer = Trainer( + updater, + stop_trigger=(config.train_max_steps, "iteration"), + out=output_dir) + + if dist.get_rank() == 0: + trainer.extend( + evaluator, trigger=(config.eval_interval_steps, 'iteration')) + trainer.extend(VisualDL(output_dir), trigger=(1, 'iteration')) + trainer.extend( + Snapshot(max_size=config.num_snapshots), + trigger=(config.save_interval_steps, 'iteration')) + + print("Trainer Done!") + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + + parser = argparse.ArgumentParser( + description="Train a Multi-Band MelGAN model.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/normalize.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/normalize.py new file mode 100644 index 0000000..ba95d3e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/normalize.py @@ -0,0 +1,133 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Normalize feature files and dump them.""" +import argparse +import logging +from operator import itemgetter +from pathlib import Path + +import jsonlines +import numpy as np +from sklearn.preprocessing import StandardScaler +from tqdm import tqdm + +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + """Run preprocessing process.""" + parser = argparse.ArgumentParser( + description="Normalize dumped raw features.") + parser.add_argument( + "--metadata", + type=str, + required=True, + help="directory including feature files to be normalized. " + "you need to specify either *-scp or rootdir.") + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump normalized feature files.") + parser.add_argument( + "--stats", type=str, required=True, help="statistics file.") + parser.add_argument( + "--skip-wav-copy", + default=False, + action="store_true", + help="whether to skip the copy of wav files.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + args = parser.parse_args() + + # set logger + if args.verbose > 1: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + elif args.verbose > 0: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + logging.warning('Skip DEBUG/INFO messages') + + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + # get dataset + with jsonlines.open(args.metadata, 'r') as reader: + metadata = list(reader) + dataset = DataTable( + metadata, + fields=["utt_id", "wave", "feats"], + converters={ + 'utt_id': None, + 'wave': None if args.skip_wav_copy else np.load, + 'feats': np.load, + }) + logging.info(f"The number of files = {len(dataset)}.") + + # restore scaler + scaler = StandardScaler() + scaler.mean_ = np.load(args.stats)[0] + scaler.scale_ = np.load(args.stats)[1] + + # from version 0.23.0, this information is needed + scaler.n_features_in_ = scaler.mean_.shape[0] + + # process each file + output_metadata = [] + + for item in tqdm(dataset): + utt_id = item['utt_id'] + wave = item['wave'] + mel = item['feats'] + # normalize + mel = scaler.transform(mel) + + # save + mel_path = dumpdir / f"{utt_id}_feats.npy" + np.save(mel_path, mel.astype(np.float32), allow_pickle=False) + if not args.skip_wav_copy: + wav_path = dumpdir / f"{utt_id}_wave.npy" + np.save(wav_path, wave.astype(np.float32), allow_pickle=False) + else: + wav_path = wave + output_metadata.append({ + 'utt_id': utt_id, + 'wave': str(wav_path), + 'feats': str(mel_path), + }) + output_metadata.sort(key=itemgetter('utt_id')) + output_metadata_path = Path(args.dumpdir) / "metadata.jsonl" + with jsonlines.open(output_metadata_path, 'w') as writer: + for item in output_metadata: + writer.write(item) + logging.info(f"metadata dumped into {output_metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/__init__.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/synthesize_from_wav.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/synthesize_from_wav.py new file mode 100644 index 0000000..def30e6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/synthesize_from_wav.py @@ -0,0 +1,118 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +from pathlib import Path + +import librosa +import numpy as np +import paddle +import soundfile as sf +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.models.parallel_wavegan import PWGGenerator +from paddlespeech.t2s.models.parallel_wavegan import PWGInference +from paddlespeech.t2s.modules.normalizer import ZScore + + +def evaluate(args, config): + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + vocoder = PWGGenerator(**config["generator_params"]) + state_dict = paddle.load(args.checkpoint) + vocoder.set_state_dict(state_dict["generator_params"]) + vocoder.remove_weight_norm() + vocoder.eval() + print("model done!") + + stat = np.load(args.stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + normalizer = ZScore(mu, std) + + pwg_inference = PWGInference(normalizer, vocoder) + + input_dir = Path(args.input_dir) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + + for utt_name in os.listdir(input_dir): + wav, _ = librosa.load(str(input_dir / utt_name), sr=config.fs) + # extract mel feats + mel = mel_extractor.get_log_mel_fbank(wav) + mel = paddle.to_tensor(mel) + with paddle.no_grad(): + gen_wav = pwg_inference(mel) + sf.write( + str(output_dir / ("gen_" + utt_name)), + gen_wav.numpy(), + samplerate=config.fs) + print(f"{utt_name} done!") + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with parallel wavegan.") + + parser.add_argument( + "--config", type=str, help="parallel wavegan config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument( + "--stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training parallel wavegan." + ) + parser.add_argument("--input-dir", type=str, help="input dir of wavs.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + + evaluate(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/train.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/train.py new file mode 100644 index 0000000..92de7a2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/parallelwave_gan/train.py @@ -0,0 +1,264 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle import nn +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.optimizer import Adam # No RAdaom +from paddle.optimizer.lr import StepDecay +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.datasets.vocoder_batch_fn import Clip +from paddlespeech.t2s.models.parallel_wavegan import PWGDiscriminator +from paddlespeech.t2s.models.parallel_wavegan import PWGEvaluator +from paddlespeech.t2s.models.parallel_wavegan import PWGGenerator +from paddlespeech.t2s.models.parallel_wavegan import PWGUpdater +from paddlespeech.t2s.modules.losses import MultiResolutionSTFTLoss +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.t2s.utils import str2bool + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + dev_sampler = DistributedBatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + print("samplers done!") + + train_batch_fn = Clip( + batch_max_steps=config.batch_max_steps, + hop_size=config.n_shift, + aux_context_window=config.generator_params.aux_context_window) + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + batch_sampler=dev_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + generator = PWGGenerator(**config["generator_params"]) + discriminator = PWGDiscriminator(**config["discriminator_params"]) + if world_size > 1: + generator = DataParallel(generator) + discriminator = DataParallel(discriminator) + print("models done!") + + criterion_stft = MultiResolutionSTFTLoss(**config["stft_loss_params"]) + criterion_mse = nn.MSELoss() + print("criterions done!") + + lr_schedule_g = StepDecay(**config["generator_scheduler_params"]) + gradient_clip_g = nn.ClipGradByGlobalNorm(config["generator_grad_norm"]) + optimizer_g = Adam( + learning_rate=lr_schedule_g, + grad_clip=gradient_clip_g, + parameters=generator.parameters(), + **config["generator_optimizer_params"]) + lr_schedule_d = StepDecay(**config["discriminator_scheduler_params"]) + gradient_clip_d = nn.ClipGradByGlobalNorm(config["discriminator_grad_norm"]) + optimizer_d = Adam( + learning_rate=lr_schedule_d, + grad_clip=gradient_clip_d, + parameters=discriminator.parameters(), + **config["discriminator_optimizer_params"]) + print("optimizers done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = PWGUpdater( + models={ + "generator": generator, + "discriminator": discriminator, + }, + optimizers={ + "generator": optimizer_g, + "discriminator": optimizer_d, + }, + criterions={ + "stft": criterion_stft, + "mse": criterion_mse, + }, + schedulers={ + "generator": lr_schedule_g, + "discriminator": lr_schedule_d, + }, + dataloader=train_dataloader, + discriminator_train_start_steps=config.discriminator_train_start_steps, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + + evaluator = PWGEvaluator( + models={ + "generator": generator, + "discriminator": discriminator, + }, + criterions={ + "stft": criterion_stft, + "mse": criterion_mse, + }, + dataloader=dev_dataloader, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + trainer = Trainer( + updater, + stop_trigger=(config.train_max_steps, "iteration"), + out=output_dir, + profiler_options=args.profiler_options) + + if dist.get_rank() == 0: + trainer.extend( + evaluator, trigger=(config.eval_interval_steps, 'iteration')) + trainer.extend(VisualDL(output_dir), trigger=(1, 'iteration')) + trainer.extend( + Snapshot(max_size=config.num_snapshots), + trigger=(config.save_interval_steps, 'iteration')) + + print("Trainer Done!") + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + + parser = argparse.ArgumentParser( + description="Train a ParallelWaveGAN model.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + benchmark_group = parser.add_argument_group( + 'benchmark', 'arguments related to benchmark.') + benchmark_group.add_argument( + "--batch-size", type=int, default=8, help="batch size.") + benchmark_group.add_argument( + "--max-iter", type=int, default=400000, help="train max steps.") + + benchmark_group.add_argument( + "--run-benchmark", + type=str2bool, + default=False, + help="runing benchmark or not, if True, use the --batch-size and --max-iter." + ) + benchmark_group.add_argument( + "--profiler_options", + type=str, + default=None, + help="The option of profiler, which should be in format \"key1=value1;key2=value2;key3=value3\"." + ) + + args = parser.parse_args() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + # 增加 --batch_size --max_iter 用于 benchmark 调用 + if args.run_benchmark: + config.batch_size = args.batch_size + config.train_max_steps = args.max_iter + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/preprocess.py new file mode 100644 index 0000000..4871bca --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/preprocess.py @@ -0,0 +1,292 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from concurrent.futures import ThreadPoolExecutor +from operator import itemgetter +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines +import librosa +import numpy as np +import tqdm +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.utils import str2bool + + +def process_sentence(config: Dict[str, Any], + fp: Path, + sentences: Dict, + output_dir: Path, + mel_extractor=None, + cut_sil: bool=True): + utt_id = fp.stem + # for vctk + if utt_id.endswith("_mic2"): + utt_id = utt_id[:-5] + record = None + if utt_id in sentences: + # reading, resampling may occur + y, _ = librosa.load(str(fp), sr=config.fs) + if len(y.shape) != 1 or np.abs(y).max() > 1.0: + return record + assert len(y.shape) == 1, f"{utt_id} is not a mono-channel audio." + assert np.abs(y).max( + ) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM." + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + d_cumsum = np.pad(np.array(durations).cumsum(0), (1, 0), 'constant') + # little imprecise than use *.TextGrid directly + times = librosa.frames_to_time( + d_cumsum, sr=config.fs, hop_length=config.n_shift) + if cut_sil: + start = 0 + end = d_cumsum[-1] + if phones[0] == "sil" and len(durations) > 1: + start = times[1] + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + end = times[-2] + durations = durations[:-1] + phones = phones[:-1] + sentences[utt_id][0] = phones + sentences[utt_id][1] = durations + start, end = librosa.time_to_samples([start, end], sr=config.fs) + y = y[start:end] + + # extract mel feats + logmel = mel_extractor.get_log_mel_fbank(y) + + # adjust time to make num_samples == num_frames * hop_length + num_frames = logmel.shape[0] + if y.size < num_frames * config.n_shift: + y = np.pad( + y, (0, num_frames * config.n_shift - y.size), mode="reflect") + else: + y = y[:num_frames * config.n_shift] + num_sample = y.shape[0] + + mel_path = output_dir / (utt_id + "_feats.npy") + wav_path = output_dir / (utt_id + "_wave.npy") + np.save(wav_path, y) # (num_samples, ) + np.save(mel_path, logmel) # (num_frames, n_mels) + record = { + "utt_id": utt_id, + "num_samples": num_sample, + "num_frames": num_frames, + "feats": str(mel_path), + "wave": str(wav_path), + } + return record + + +def process_sentences(config, + fps: List[Path], + sentences: Dict, + output_dir: Path, + mel_extractor=None, + nprocs: int=1, + cut_sil: bool=True): + if nprocs == 1: + results = [] + for fp in tqdm.tqdm(fps, total=len(fps)): + record = process_sentence(config, fp, sentences, output_dir, + mel_extractor, cut_sil) + if record: + results.append(record) + else: + with ThreadPoolExecutor(nprocs) as pool: + futures = [] + with tqdm.tqdm(total=len(fps)) as progress: + for fp in fps: + future = pool.submit(process_sentence, config, fp, + sentences, output_dir, mel_extractor, + cut_sil) + future.add_done_callback(lambda p: progress.update()) + futures.append(future) + + results = [] + for ft in futures: + record = ft.result() + if record: + results.append(record) + + results.sort(key=itemgetter("utt_id")) + with jsonlines.open(output_dir / "metadata.jsonl", 'w') as writer: + for item in results: + writer.write(item) + print("Done") + + +def main(): + # parse config and args + parser = argparse.ArgumentParser( + description="Preprocess audio and then extract features .") + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker, ljspeech, vctk} now") + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump feature files.") + parser.add_argument("--config", type=str, help="vocoder config file.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + parser.add_argument( + "--num-cpu", type=int, default=1, help="number of process.") + parser.add_argument( + "--dur-file", default=None, type=str, help="path to durations.txt.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + args = parser.parse_args() + + rootdir = Path(args.rootdir).expanduser() + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + dur_file = Path(args.dur_file).expanduser() + + assert rootdir.is_dir() + assert dur_file.is_file() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + if args.verbose > 1: + print(vars(args)) + print(config) + + sentences, speaker_set = get_phn_dur(dur_file) + merge_silence(sentences) + + # split data into 3 sections + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + + elif args.dataset == "ljspeech": + wav_files = sorted(list((rootdir / "wavs").rglob("*.wav"))) + # split data into 3 sections + num_train = 12900 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "vctk": + sub_num_dev = 5 + wav_dir = rootdir / "wav48_silence_trimmed" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*_mic2.flac"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + elif args.dataset == "aishell3": + sub_num_dev = 5 + wav_dir = rootdir / "train" / "wav" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*.wav"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + else: + print("dataset should in {baker, ljspeech, vctk, aishell3} now!") + + train_dump_dir = dumpdir / "train" / "raw" + train_dump_dir.mkdir(parents=True, exist_ok=True) + dev_dump_dir = dumpdir / "dev" / "raw" + dev_dump_dir.mkdir(parents=True, exist_ok=True) + test_dump_dir = dumpdir / "test" / "raw" + test_dump_dir.mkdir(parents=True, exist_ok=True) + + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + + # process for the 3 sections + if train_wav_files: + process_sentences( + config, + train_wav_files, + sentences, + train_dump_dir, + mel_extractor=mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil) + if dev_wav_files: + process_sentences( + config, + dev_wav_files, + sentences, + dev_dump_dir, + mel_extractor=mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil) + if test_wav_files: + process_sentences( + config, + test_wav_files, + sentences, + test_dump_dir, + mel_extractor=mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/__init__.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/train.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/train.py new file mode 100644 index 0000000..be3ba74 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/style_melgan/train.py @@ -0,0 +1,256 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle import nn +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.optimizer import Adam +from paddle.optimizer.lr import MultiStepDecay +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.datasets.vocoder_batch_fn import Clip +from paddlespeech.t2s.models.melgan import StyleMelGANDiscriminator +from paddlespeech.t2s.models.melgan import StyleMelGANEvaluator +from paddlespeech.t2s.models.melgan import StyleMelGANGenerator +from paddlespeech.t2s.models.melgan import StyleMelGANUpdater +from paddlespeech.t2s.modules.losses import DiscriminatorAdversarialLoss +from paddlespeech.t2s.modules.losses import GeneratorAdversarialLoss +from paddlespeech.t2s.modules.losses import MultiResolutionSTFTLoss +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + dev_sampler = DistributedBatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + print("samplers done!") + + if "aux_context_window" in config.generator_params: + aux_context_window = config.generator_params.aux_context_window + else: + aux_context_window = 0 + train_batch_fn = Clip( + batch_max_steps=config.batch_max_steps, + hop_size=config.n_shift, + aux_context_window=aux_context_window) + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + batch_sampler=dev_sampler, + collate_fn=train_batch_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + generator = StyleMelGANGenerator(**config["generator_params"]) + discriminator = StyleMelGANDiscriminator(**config["discriminator_params"]) + if world_size > 1: + generator = DataParallel(generator) + discriminator = DataParallel(discriminator) + print("models done!") + criterion_stft = MultiResolutionSTFTLoss(**config["stft_loss_params"]) + + criterion_gen_adv = GeneratorAdversarialLoss( + **config["generator_adv_loss_params"]) + criterion_dis_adv = DiscriminatorAdversarialLoss( + **config["discriminator_adv_loss_params"]) + print("criterions done!") + + lr_schedule_g = MultiStepDecay(**config["generator_scheduler_params"]) + # Compared to multi_band_melgan.v1 config, Adam optimizer without gradient norm is used + generator_grad_norm = config["generator_grad_norm"] + gradient_clip_g = nn.ClipGradByGlobalNorm( + generator_grad_norm) if generator_grad_norm > 0 else None + print("gradient_clip_g:", gradient_clip_g) + + optimizer_g = Adam( + learning_rate=lr_schedule_g, + grad_clip=gradient_clip_g, + parameters=generator.parameters(), + **config["generator_optimizer_params"]) + lr_schedule_d = MultiStepDecay(**config["discriminator_scheduler_params"]) + discriminator_grad_norm = config["discriminator_grad_norm"] + gradient_clip_d = nn.ClipGradByGlobalNorm( + discriminator_grad_norm) if discriminator_grad_norm > 0 else None + print("gradient_clip_d:", gradient_clip_d) + optimizer_d = Adam( + learning_rate=lr_schedule_d, + grad_clip=gradient_clip_d, + parameters=discriminator.parameters(), + **config["discriminator_optimizer_params"]) + print("optimizers done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = StyleMelGANUpdater( + models={ + "generator": generator, + "discriminator": discriminator, + }, + optimizers={ + "generator": optimizer_g, + "discriminator": optimizer_d, + }, + criterions={ + "stft": criterion_stft, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + }, + schedulers={ + "generator": lr_schedule_g, + "discriminator": lr_schedule_d, + }, + dataloader=train_dataloader, + discriminator_train_start_steps=config.discriminator_train_start_steps, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + + evaluator = StyleMelGANEvaluator( + models={ + "generator": generator, + "discriminator": discriminator, + }, + criterions={ + "stft": criterion_stft, + "gen_adv": criterion_gen_adv, + "dis_adv": criterion_dis_adv, + }, + dataloader=dev_dataloader, + lambda_adv=config.lambda_adv, + output_dir=output_dir) + + trainer = Trainer( + updater, + stop_trigger=(config.train_max_steps, "iteration"), + out=output_dir) + + if dist.get_rank() == 0: + trainer.extend( + evaluator, trigger=(config.eval_interval_steps, 'iteration')) + trainer.extend(VisualDL(output_dir), trigger=(1, 'iteration')) + trainer.extend( + Snapshot(max_size=config.num_snapshots), + trigger=(config.save_interval_steps, 'iteration')) + + print("Trainer Done!") + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + + parser = argparse.ArgumentParser(description="Train a Style MelGAN model.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize.py new file mode 100644 index 0000000..9d9a8c4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize.py @@ -0,0 +1,121 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from paddle import distributed as dist +from timer import timer +from yacs.config import CfgNode + +import paddlespeech +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + parser = argparse.ArgumentParser(description="Synthesize with GANVocoder.") + parser.add_argument( + "--generator-type", + type=str, + default="pwgan", + help="type of GANVocoder, should in {pwgan, mb_melgan, style_melgan, hifigan, } now" + ) + parser.add_argument("--config", type=str, help="GANVocoder config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument("--test-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + class_map = { + "hifigan": "HiFiGANGenerator", + "mb_melgan": "MelGANGenerator", + "pwgan": "PWGGenerator", + "style_melgan": "StyleMelGANGenerator", + } + + generator_type = args.generator_type + + assert generator_type in class_map + + print("generator_type:", generator_type) + + generator_class = getattr(paddlespeech.t2s.models, + class_map[generator_type]) + generator = generator_class(**config["generator_params"]) + state_dict = paddle.load(args.checkpoint) + generator.set_state_dict(state_dict["generator_params"]) + generator.remove_weight_norm() + generator.eval() + + with jsonlines.open(args.test_metadata, 'r') as reader: + metadata = list(reader) + test_dataset = DataTable( + metadata, + fields=['utt_id', 'feats'], + converters={ + 'utt_id': None, + 'feats': np.load, + }) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + N = 0 + T = 0 + for example in test_dataset: + utt_id = example['utt_id'] + mel = example['feats'] + mel = paddle.to_tensor(mel) # (T, C) + with timer() as t: + with paddle.no_grad(): + wav = generator.inference(c=mel) + wav = wav.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write(str(output_dir / (utt_id + ".wav")), wav, samplerate=config.fs) + print(f"generation speed: {N / T}Hz, RTF: {config.fs / (N / T) }") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize_fxr.py b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize_fxr.py new file mode 100644 index 0000000..9d9a8c4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/gan_vocoder/synthesize_fxr.py @@ -0,0 +1,121 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from paddle import distributed as dist +from timer import timer +from yacs.config import CfgNode + +import paddlespeech +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + parser = argparse.ArgumentParser(description="Synthesize with GANVocoder.") + parser.add_argument( + "--generator-type", + type=str, + default="pwgan", + help="type of GANVocoder, should in {pwgan, mb_melgan, style_melgan, hifigan, } now" + ) + parser.add_argument("--config", type=str, help="GANVocoder config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument("--test-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + class_map = { + "hifigan": "HiFiGANGenerator", + "mb_melgan": "MelGANGenerator", + "pwgan": "PWGGenerator", + "style_melgan": "StyleMelGANGenerator", + } + + generator_type = args.generator_type + + assert generator_type in class_map + + print("generator_type:", generator_type) + + generator_class = getattr(paddlespeech.t2s.models, + class_map[generator_type]) + generator = generator_class(**config["generator_params"]) + state_dict = paddle.load(args.checkpoint) + generator.set_state_dict(state_dict["generator_params"]) + generator.remove_weight_norm() + generator.eval() + + with jsonlines.open(args.test_metadata, 'r') as reader: + metadata = list(reader) + test_dataset = DataTable( + metadata, + fields=['utt_id', 'feats'], + converters={ + 'utt_id': None, + 'feats': np.load, + }) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + N = 0 + T = 0 + for example in test_dataset: + utt_id = example['utt_id'] + mel = example['feats'] + mel = paddle.to_tensor(mel) # (T, C) + with timer() as t: + with paddle.no_grad(): + wav = generator.inference(c=mel) + wav = wav.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write(str(output_dir / (utt_id + ".wav")), wav, samplerate=config.fs) + print(f"generation speed: {N / T}Hz, RTF: {config.fs / (N / T) }") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/inference.py b/ernie-sat/paddlespeech/t2s/exps/inference.py new file mode 100644 index 0000000..62602a0 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/inference.py @@ -0,0 +1,246 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +import numpy +import soundfile as sf +from paddle import inference +from timer import timer + +from paddlespeech.t2s.exps.syn_utils import get_frontend +from paddlespeech.t2s.exps.syn_utils import get_sentences +from paddlespeech.t2s.utils import str2bool + + +def get_predictor(args, filed='am'): + full_name = '' + if filed == 'am': + full_name = args.am + elif filed == 'voc': + full_name = args.voc + model_name = full_name[:full_name.rindex('_')] + config = inference.Config( + str(Path(args.inference_dir) / (full_name + ".pdmodel")), + str(Path(args.inference_dir) / (full_name + ".pdiparams"))) + if args.device == "gpu": + config.enable_use_gpu(100, 0) + elif args.device == "cpu": + config.disable_gpu() + # This line must be commented for fastspeech2, if not, it will OOM + if model_name != 'fastspeech2': + config.enable_memory_optim() + predictor = inference.create_predictor(config) + return predictor + + +def get_am_output(args, am_predictor, frontend, merge_sentences, input): + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + am_input_names = am_predictor.get_input_names() + get_tone_ids = False + get_spk_id = False + if am_name == 'speedyspeech': + get_tone_ids = True + if am_dataset in {"aishell3", "vctk"} and args.speaker_dict: + get_spk_id = True + spk_id = numpy.array([args.spk_id]) + if args.lang == 'zh': + input_ids = frontend.get_input_ids( + input, merge_sentences=merge_sentences, get_tone_ids=get_tone_ids) + phone_ids = input_ids["phone_ids"] + elif args.lang == 'en': + input_ids = frontend.get_input_ids( + input, merge_sentences=merge_sentences) + phone_ids = input_ids["phone_ids"] + else: + print("lang should in {'zh', 'en'}!") + + if get_tone_ids: + tone_ids = input_ids["tone_ids"] + tones = tone_ids[0].numpy() + tones_handle = am_predictor.get_input_handle(am_input_names[1]) + tones_handle.reshape(tones.shape) + tones_handle.copy_from_cpu(tones) + if get_spk_id: + spk_id_handle = am_predictor.get_input_handle(am_input_names[1]) + spk_id_handle.reshape(spk_id.shape) + spk_id_handle.copy_from_cpu(spk_id) + phones = phone_ids[0].numpy() + phones_handle = am_predictor.get_input_handle(am_input_names[0]) + phones_handle.reshape(phones.shape) + phones_handle.copy_from_cpu(phones) + + am_predictor.run() + am_output_names = am_predictor.get_output_names() + am_output_handle = am_predictor.get_output_handle(am_output_names[0]) + am_output_data = am_output_handle.copy_to_cpu() + return am_output_data + + +def get_voc_output(args, voc_predictor, input): + voc_input_names = voc_predictor.get_input_names() + mel_handle = voc_predictor.get_input_handle(voc_input_names[0]) + mel_handle.reshape(input.shape) + mel_handle.copy_from_cpu(input) + + voc_predictor.run() + voc_output_names = voc_predictor.get_output_names() + voc_output_handle = voc_predictor.get_output_handle(voc_output_names[0]) + wav = voc_output_handle.copy_to_cpu() + return wav + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Paddle Infernce with acoustic model & vocoder.") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'speedyspeech_csmsc', 'fastspeech2_csmsc', 'fastspeech2_aishell3', + 'fastspeech2_vctk', 'tacotron2_csmsc' + ], + help='Choose acoustic model type of tts task.') + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument( + "--speaker_dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + '--spk_id', + type=int, + default=0, + help='spk id for multi speaker acoustic model') + # voc + parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=[ + 'pwgan_csmsc', 'mb_melgan_csmsc', 'hifigan_csmsc', 'pwgan_aishell3', + 'pwgan_vctk', 'wavernn_csmsc' + ], + help='Choose vocoder type of tts task.') + # other + parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en') + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line") + parser.add_argument( + "--inference_dir", type=str, help="dir to save inference models") + parser.add_argument("--output_dir", type=str, help="output dir") + # inference + parser.add_argument( + "--use_trt", + type=str2bool, + default=False, + help="Whether to use inference engin TensorRT.", ) + parser.add_argument( + "--int8", + type=str2bool, + default=False, + help="Whether to use int8 inference.", ) + parser.add_argument( + "--fp16", + type=str2bool, + default=False, + help="Whether to use float16 inference.", ) + parser.add_argument( + "--device", + default="gpu", + choices=["gpu", "cpu"], + help="Device selected for inference.", ) + + args, _ = parser.parse_known_args() + return args + + +# only inference for models trained with csmsc now +def main(): + args = parse_args() + # frontend + frontend = get_frontend(args) + + # am_predictor + am_predictor = get_predictor(args, filed='am') + # model: {model_name}_{dataset} + am_dataset = args.am[args.am.rindex('_') + 1:] + + # voc_predictor + voc_predictor = get_predictor(args, filed='voc') + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + sentences = get_sentences(args) + + merge_sentences = True + fs = 24000 if am_dataset != 'ljspeech' else 22050 + # warmup + for utt_id, sentence in sentences[:3]: + with timer() as t: + am_output_data = get_am_output( + args, + am_predictor=am_predictor, + frontend=frontend, + merge_sentences=merge_sentences, + input=sentence) + wav = get_voc_output( + args, voc_predictor=voc_predictor, input=am_output_data) + speed = wav.size / t.elapse + rtf = fs / speed + print( + f"{utt_id}, mel: {am_output_data.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + + print("warm up done!") + + N = 0 + T = 0 + for utt_id, sentence in sentences: + with timer() as t: + am_output_data = get_am_output( + args, + am_predictor=am_predictor, + frontend=frontend, + merge_sentences=merge_sentences, + input=sentence) + wav = get_voc_output( + args, voc_predictor=voc_predictor, input=am_output_data) + + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = fs / speed + + sf.write(output_dir / (utt_id + ".wav"), wav, samplerate=24000) + print( + f"{utt_id}, mel: {am_output_data.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + + print(f"{utt_id} done!") + print(f"generation speed: {N / T}Hz, RTF: {fs / (N / T) }") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/ort_predict.py b/ernie-sat/paddlespeech/t2s/exps/ort_predict.py new file mode 100644 index 0000000..e8d4d61 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/ort_predict.py @@ -0,0 +1,156 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +import jsonlines +import numpy as np +import onnxruntime as ort +import soundfile as sf +from timer import timer + +from paddlespeech.t2s.exps.syn_utils import get_test_dataset +from paddlespeech.t2s.utils import str2bool + + +def get_sess(args, filed='am'): + full_name = '' + if filed == 'am': + full_name = args.am + elif filed == 'voc': + full_name = args.voc + model_dir = str(Path(args.inference_dir) / (full_name + ".onnx")) + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + + if args.device == "gpu": + # fastspeech2/mb_melgan can't use trt now! + if args.use_trt: + providers = ['TensorrtExecutionProvider'] + else: + providers = ['CUDAExecutionProvider'] + elif args.device == "cpu": + providers = ['CPUExecutionProvider'] + sess_options.intra_op_num_threads = args.cpu_threads + sess = ort.InferenceSession( + model_dir, providers=providers, sess_options=sess_options) + return sess + + +def ort_predict(args): + # construct dataset for evaluation + with jsonlines.open(args.test_metadata, 'r') as reader: + test_metadata = list(reader) + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + test_dataset = get_test_dataset(args, test_metadata, am_name, am_dataset) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + fs = 24000 if am_dataset != 'ljspeech' else 22050 + + # am + am_sess = get_sess(args, filed='am') + + # vocoder + voc_sess = get_sess(args, filed='voc') + + # am warmup + for T in [27, 38, 54]: + data = np.random.randint(1, 266, size=(T, )) + am_sess.run(None, {"text": data}) + + # voc warmup + for T in [227, 308, 544]: + data = np.random.rand(T, 80).astype("float32") + voc_sess.run(None, {"logmel": data}) + print("warm up done!") + + N = 0 + T = 0 + for example in test_dataset: + utt_id = example['utt_id'] + phone_ids = example["text"] + with timer() as t: + mel = am_sess.run(output_names=None, input_feed={'text': phone_ids}) + mel = mel[0] + wav = voc_sess.run(output_names=None, input_feed={'logmel': mel}) + + N += len(wav[0]) + T += t.elapse + speed = len(wav[0]) / t.elapse + rtf = fs / speed + sf.write( + str(output_dir / (utt_id + ".wav")), + np.array(wav)[0], + samplerate=fs) + print( + f"{utt_id}, mel: {mel.shape}, wave: {len(wav[0])}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + print(f"generation speed: {N / T}Hz, RTF: {fs / (N / T) }") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Infernce with onnxruntime.") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'fastspeech2_csmsc', + ], + help='Choose acoustic model type of tts task.') + + # voc + parser.add_argument( + '--voc', + type=str, + default='hifigan_csmsc', + choices=['hifigan_csmsc', 'mb_melgan_csmsc'], + help='Choose vocoder type of tts task.') + # other + parser.add_argument( + "--inference_dir", type=str, help="dir to save inference models") + parser.add_argument("--test_metadata", type=str, help="test metadata.") + parser.add_argument("--output_dir", type=str, help="output dir") + + # inference + parser.add_argument( + "--use_trt", + type=str2bool, + default=False, + help="Whether to use inference engin TensorRT.", ) + + parser.add_argument( + "--device", + default="gpu", + choices=["gpu", "cpu"], + help="Device selected for inference.", ) + parser.add_argument('--cpu_threads', type=int, default=1) + + args, _ = parser.parse_known_args() + return args + + +def main(): + args = parse_args() + + ort_predict(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/ort_predict_e2e.py b/ernie-sat/paddlespeech/t2s/exps/ort_predict_e2e.py new file mode 100644 index 0000000..8aa04cb --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/ort_predict_e2e.py @@ -0,0 +1,183 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +import numpy as np +import onnxruntime as ort +import soundfile as sf +from timer import timer + +from paddlespeech.t2s.exps.syn_utils import get_frontend +from paddlespeech.t2s.exps.syn_utils import get_sentences +from paddlespeech.t2s.utils import str2bool + + +def get_sess(args, filed='am'): + full_name = '' + if filed == 'am': + full_name = args.am + elif filed == 'voc': + full_name = args.voc + model_dir = str(Path(args.inference_dir) / (full_name + ".onnx")) + sess_options = ort.SessionOptions() + sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + + if args.device == "gpu": + # fastspeech2/mb_melgan can't use trt now! + if args.use_trt: + providers = ['TensorrtExecutionProvider'] + else: + providers = ['CUDAExecutionProvider'] + elif args.device == "cpu": + providers = ['CPUExecutionProvider'] + sess_options.intra_op_num_threads = args.cpu_threads + sess = ort.InferenceSession( + model_dir, providers=providers, sess_options=sess_options) + return sess + + +def ort_predict(args): + + # frontend + frontend = get_frontend(args) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + sentences = get_sentences(args) + + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + fs = 24000 if am_dataset != 'ljspeech' else 22050 + + # am + am_sess = get_sess(args, filed='am') + + # vocoder + voc_sess = get_sess(args, filed='voc') + + # am warmup + for T in [27, 38, 54]: + data = np.random.randint(1, 266, size=(T, )) + am_sess.run(None, {"text": data}) + + # voc warmup + for T in [227, 308, 544]: + data = np.random.rand(T, 80).astype("float32") + voc_sess.run(None, {"logmel": data}) + print("warm up done!") + + # frontend warmup + # Loading model cost 0.5+ seconds + if args.lang == 'zh': + frontend.get_input_ids("你好,欢迎使用飞桨框架进行深度学习研究!", merge_sentences=True) + else: + print("lang should in be 'zh' here!") + + N = 0 + T = 0 + merge_sentences = True + for utt_id, sentence in sentences: + with timer() as t: + if args.lang == 'zh': + input_ids = frontend.get_input_ids( + sentence, merge_sentences=merge_sentences) + + phone_ids = input_ids["phone_ids"] + else: + print("lang should in be 'zh' here!") + # merge_sentences=True here, so we only use the first item of phone_ids + phone_ids = phone_ids[0].numpy() + mel = am_sess.run(output_names=None, input_feed={'text': phone_ids}) + mel = mel[0] + wav = voc_sess.run(output_names=None, input_feed={'logmel': mel}) + + N += len(wav[0]) + T += t.elapse + speed = len(wav[0]) / t.elapse + rtf = fs / speed + sf.write( + str(output_dir / (utt_id + ".wav")), + np.array(wav)[0], + samplerate=fs) + print( + f"{utt_id}, mel: {mel.shape}, wave: {len(wav[0])}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + print(f"generation speed: {N / T}Hz, RTF: {fs / (N / T) }") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Infernce with onnxruntime.") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'fastspeech2_csmsc', + ], + help='Choose acoustic model type of tts task.') + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + + # voc + parser.add_argument( + '--voc', + type=str, + default='hifigan_csmsc', + choices=['hifigan_csmsc', 'mb_melgan_csmsc'], + help='Choose vocoder type of tts task.') + # other + parser.add_argument( + "--inference_dir", type=str, help="dir to save inference models") + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line") + parser.add_argument("--output_dir", type=str, help="output dir") + parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en') + + # inference + parser.add_argument( + "--use_trt", + type=str2bool, + default=False, + help="Whether to use inference engin TensorRT.", ) + + parser.add_argument( + "--device", + default="gpu", + choices=["gpu", "cpu"], + help="Device selected for inference.", ) + parser.add_argument('--cpu_threads', type=int, default=1) + + args, _ = parser.parse_known_args() + return args + + +def main(): + args = parse_args() + + ort_predict(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/sentences.txt b/ernie-sat/paddlespeech/t2s/exps/sentences.txt new file mode 100644 index 0000000..3aa5376 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/sentences.txt @@ -0,0 +1,16 @@ +001 凯莫瑞安联合体的经济崩溃,迫在眉睫。 +002 对于所有想要离开那片废土,去寻找更美好生活的人来说。 +003 克哈,是你们所有人安全的港湾。 +004 为了保护尤摩扬人民不受异虫的残害,我所做的,比他们自己的领导委员会都多。 +005 无论他们如何诽谤我,我将继续为所有泰伦人的最大利益,而努力奋斗。 +006 身为你们的元首,我带领泰伦人实现了人类统治领地和经济的扩张。 +007 我们将继续成长,用行动回击那些只会说风凉话,不愿意和我们相向而行的害群之马。 +008 帝国武装力量,无数的优秀儿女,正时刻守卫着我们的家园大门,但是他们孤木难支。 +009 凡是今天应征入伍者,所获的所有刑罚罪责,减半。 +010 激进分子和异见者希望你们一听见枪声,就背弃多年的和平与繁荣。 +011 他们没有勇气和能力,带领人类穿越一个充满危险的星系。 +012 法治是我们的命脉,然而它却受到前所未有的挑战。 +013 我将恢复我们帝国的荣光,绝不会向任何外星势力低头。 +014 我已经驯服了异虫,荡平了星灵。如今它们的创造者,想要夺走我们拥有的一切。 +015 永远记住,谁才是最能保护你们的人。 +016 不要听信别人的谗言,我不是什么克隆人。 \ No newline at end of file diff --git a/ernie-sat/paddlespeech/t2s/exps/sentences_en.txt b/ernie-sat/paddlespeech/t2s/exps/sentences_en.txt new file mode 100644 index 0000000..36b73a5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/sentences_en.txt @@ -0,0 +1,9 @@ +001 Life was like a box of chocolates, you never know what you're gonna get. +002 With great power there must come great responsibility. +003 To be or not to be, that’s a question. +004 A man can be destroyed but not defeated +005 Do not, for one repulse, give up the purpose that you resolved to effort. +006 Death is just a part of life, something we're all destined to do. +007 I think it's hard winning a war with words. +008 Don’t argue with the people of strong determination, because they may change the fact! +009 Love you three thousand times. \ No newline at end of file diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/__init__.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/gen_gta_mel.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/gen_gta_mel.py new file mode 100644 index 0000000..31b7d2e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/gen_gta_mel.py @@ -0,0 +1,244 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# generate mels using durations.txt +# for mb melgan finetune +# 长度和原本的 mel 不一致怎么办? +import argparse +import os +from pathlib import Path + +import numpy as np +import paddle +import yaml +from tqdm import tqdm +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.t2s.models.speedyspeech import SpeedySpeech +from paddlespeech.t2s.models.speedyspeech import SpeedySpeechInference +from paddlespeech.t2s.modules.normalizer import ZScore +from paddlespeech.t2s.utils import str2bool + + +def evaluate(args, speedyspeech_config): + rootdir = Path(args.rootdir).expanduser() + assert rootdir.is_dir() + + # construct dataset for evaluation + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + phone_dict = {} + for phn, id in phn_id: + phone_dict[phn] = int(id) + + with open(args.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + frontend = Frontend( + phone_vocab_path=args.phones_dict, tone_vocab_path=args.tones_dict) + + if args.speaker_dict: + with open(args.speaker_dict, 'rt') as f: + spk_id_list = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id_list) + else: + spk_num = None + + model = SpeedySpeech( + vocab_size=vocab_size, + tone_size=tone_size, + **speedyspeech_config["model"], + spk_num=spk_num) + + model.set_state_dict( + paddle.load(args.speedyspeech_checkpoint)["main_params"]) + model.eval() + + stat = np.load(args.speedyspeech_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + speedyspeech_normalizer = ZScore(mu, std) + + speedyspeech_inference = SpeedySpeechInference(speedyspeech_normalizer, + model) + speedyspeech_inference.eval() + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + sentences, speaker_set = get_phn_dur(args.dur_file) + merge_silence(sentences) + + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + # split data into 3 sections + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "aishell3": + sub_num_dev = 5 + wav_dir = rootdir / "train" / "wav" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*.wav"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + train_wav_files = [ + os.path.basename(str(str_path)) for str_path in train_wav_files + ] + dev_wav_files = [ + os.path.basename(str(str_path)) for str_path in dev_wav_files + ] + test_wav_files = [ + os.path.basename(str(str_path)) for str_path in test_wav_files + ] + + for i, utt_id in enumerate(tqdm(sentences)): + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + # 裁剪掉开头和结尾的 sil + if args.cut_sil: + if phones[0] == "sil" and len(durations) > 1: + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + durations = durations[:-1] + phones = phones[:-1] + + phones, tones = frontend._get_phone_tone(phones, get_tone_ids=True) + if tones: + tone_ids = frontend._t2id(tones) + tone_ids = paddle.to_tensor(tone_ids) + if phones: + phone_ids = frontend._p2id(phones) + phone_ids = paddle.to_tensor(phone_ids) + + if args.speaker_dict: + speaker_id = int( + [item[1] for item in spk_id_list if speaker == item[0]][0]) + speaker_id = paddle.to_tensor(speaker_id) + else: + speaker_id = None + + durations = paddle.to_tensor(np.array(durations)) + durations = paddle.unsqueeze(durations, axis=0) + + # 生成的和真实的可能有 1, 2 帧的差距,但是 batch_fn 会修复 + # split data into 3 sections + + wav_path = utt_id + ".wav" + + if wav_path in train_wav_files: + sub_output_dir = output_dir / ("train/raw") + elif wav_path in dev_wav_files: + sub_output_dir = output_dir / ("dev/raw") + elif wav_path in test_wav_files: + sub_output_dir = output_dir / ("test/raw") + + sub_output_dir.mkdir(parents=True, exist_ok=True) + + with paddle.no_grad(): + mel = speedyspeech_inference( + phone_ids, tone_ids, durations=durations, spk_id=speaker_id) + np.save(sub_output_dir / (utt_id + "_feats.npy"), mel) + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with speedyspeech & parallel wavegan.") + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker, ljspeech, vctk} now") + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + parser.add_argument( + "--speedyspeech-config", type=str, help="speedyspeech config file.") + parser.add_argument( + "--speedyspeech-checkpoint", + type=str, + help="speedyspeech checkpoint to load.") + parser.add_argument( + "--speedyspeech-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training speedyspeech." + ) + + parser.add_argument( + "--phones-dict", + type=str, + default="phone_id_map.txt", + help="phone vocabulary file.") + parser.add_argument( + "--tones-dict", + type=str, + default="tone_id_map.txt", + help="tone vocabulary file.") + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + + parser.add_argument( + "--dur-file", default=None, type=str, help="path to durations.txt.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.speedyspeech_config) as f: + speedyspeech_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(speedyspeech_config) + + evaluate(args, speedyspeech_config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/inference.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/inference.py new file mode 100644 index 0000000..d4958bc --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/inference.py @@ -0,0 +1,115 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# remain for chains +import argparse +from pathlib import Path + +import soundfile as sf +from paddle import inference + +from paddlespeech.t2s.frontend.zh_frontend import Frontend + + +def main(): + parser = argparse.ArgumentParser( + description="Paddle Infernce with speedyspeech & parallel wavegan.") + parser.add_argument( + "--inference-dir", type=str, help="dir to save inference models") + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line") + parser.add_argument("--output-dir", type=str, help="output dir") + parser.add_argument( + "--phones-dict", + type=str, + default="phones.txt", + help="phone vocabulary file.") + parser.add_argument( + "--tones-dict", + type=str, + default="tones.txt", + help="tone vocabulary file.") + + args, _ = parser.parse_known_args() + + frontend = Frontend( + phone_vocab_path=args.phones_dict, tone_vocab_path=args.tones_dict) + print("frontend done!") + + speedyspeech_config = inference.Config( + str(Path(args.inference_dir) / "speedyspeech.pdmodel"), + str(Path(args.inference_dir) / "speedyspeech.pdiparams")) + speedyspeech_config.enable_use_gpu(100, 0) + speedyspeech_config.enable_memory_optim() + speedyspeech_predictor = inference.create_predictor(speedyspeech_config) + + pwg_config = inference.Config( + str(Path(args.inference_dir) / "pwg.pdmodel"), + str(Path(args.inference_dir) / "pwg.pdiparams")) + pwg_config.enable_use_gpu(100, 0) + pwg_config.enable_memory_optim() + pwg_predictor = inference.create_predictor(pwg_config) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + sentences = [] + + with open(args.text, 'rt') as f: + for line in f: + items = line.strip().split() + utt_id = items[0] + sentence = "".join(items[1:]) + sentences.append((utt_id, sentence)) + + for utt_id, sentence in sentences: + input_ids = frontend.get_input_ids( + sentence, merge_sentences=True, get_tone_ids=True) + phone_ids = input_ids["phone_ids"] + tone_ids = input_ids["tone_ids"] + phones = phone_ids[0].numpy() + tones = tone_ids[0].numpy() + + input_names = speedyspeech_predictor.get_input_names() + phones_handle = speedyspeech_predictor.get_input_handle(input_names[0]) + tones_handle = speedyspeech_predictor.get_input_handle(input_names[1]) + + phones_handle.reshape(phones.shape) + phones_handle.copy_from_cpu(phones) + tones_handle.reshape(tones.shape) + tones_handle.copy_from_cpu(tones) + + speedyspeech_predictor.run() + output_names = speedyspeech_predictor.get_output_names() + output_handle = speedyspeech_predictor.get_output_handle( + output_names[0]) + output_data = output_handle.copy_to_cpu() + + input_names = pwg_predictor.get_input_names() + mel_handle = pwg_predictor.get_input_handle(input_names[0]) + mel_handle.reshape(output_data.shape) + mel_handle.copy_from_cpu(output_data) + + pwg_predictor.run() + output_names = pwg_predictor.get_output_names() + output_handle = pwg_predictor.get_output_handle(output_names[0]) + wav = output_data = output_handle.copy_to_cpu() + + sf.write(output_dir / (utt_id + ".wav"), wav, samplerate=24000) + + print(f"{utt_id} done!") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/normalize.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/normalize.py new file mode 100644 index 0000000..249a4d6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/normalize.py @@ -0,0 +1,166 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Normalize feature files and dump them.""" +import argparse +import logging +from operator import itemgetter +from pathlib import Path + +import jsonlines +import numpy as np +from sklearn.preprocessing import StandardScaler +from tqdm import tqdm + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.utils import str2bool + + +def main(): + """Run preprocessing process.""" + parser = argparse.ArgumentParser( + description="Normalize dumped raw features (See detail in parallel_wavegan/bin/normalize.py)." + ) + parser.add_argument( + "--metadata", + type=str, + required=True, + help="directory including feature files to be normalized. " + "you need to specify either *-scp or rootdir.") + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump normalized feature files.") + parser.add_argument( + "--stats", type=str, required=True, help="statistics file.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones-dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + + parser.add_argument( + "--use-relative-path", + type=str2bool, + default=False, + help="whether use relative path in metadata") + args = parser.parse_args() + + # set logger + if args.verbose > 1: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + elif args.verbose > 0: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + logging.warning('Skip DEBUG/INFO messages') + + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + # get dataset + with jsonlines.open(args.metadata, 'r') as reader: + metadata = list(reader) + if args.use_relative_path: + # if use_relative_path in preprocess, covert it to absolute path here + metadata_dir = Path(args.metadata).parent + for item in metadata: + item["feats"] = str(metadata_dir / item["feats"]) + + dataset = DataTable( + metadata, converters={ + 'feats': np.load, + }) + logging.info(f"The number of files = {len(dataset)}.") + + # restore scaler + scaler = StandardScaler() + scaler.mean_ = np.load(args.stats)[0] + scaler.scale_ = np.load(args.stats)[1] + # from version 0.23.0, this information is needed + scaler.n_features_in_ = scaler.mean_.shape[0] + + vocab_phones = {} + with open(args.phones_dict, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + vocab_phones[phn] = int(id) + + vocab_tones = {} + with open(args.tones_dict, 'rt') as f: + tone_id = [line.strip().split() for line in f.readlines()] + for tone, id in tone_id: + vocab_tones[tone] = int(id) + + vocab_speaker = {} + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + for spk, id in spk_id: + vocab_speaker[spk] = int(id) + + # process each file + output_metadata = [] + + for item in tqdm(dataset): + utt_id = item['utt_id'] + mel = item['feats'] + # normalize + mel = scaler.transform(mel) + + # save + mel_path = dumpdir / f"{utt_id}_feats.npy" + np.save(mel_path, mel.astype(np.float32), allow_pickle=False) + phone_ids = [vocab_phones[p] for p in item['phones']] + tone_ids = [vocab_tones[p] for p in item['tones']] + spk_id = vocab_speaker[item["speaker"]] + if args.use_relative_path: + # convert absolute path to relative path: + mel_path = mel_path.relative_to(dumpdir) + output_metadata.append({ + 'utt_id': utt_id, + "spk_id": spk_id, + 'phones': phone_ids, + 'tones': tone_ids, + 'num_phones': item['num_phones'], + 'num_frames': item['num_frames'], + 'durations': item['durations'], + 'feats': str(mel_path), + }) + output_metadata.sort(key=itemgetter('utt_id')) + output_metadata_path = Path(args.dumpdir) / "metadata.jsonl" + with jsonlines.open(output_metadata_path, 'w') as writer: + for item in output_metadata: + writer.write(item) + logging.info(f"metadata dumped into {output_metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/preprocess.py new file mode 100644 index 0000000..e833d13 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/preprocess.py @@ -0,0 +1,298 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import re +from concurrent.futures import ThreadPoolExecutor +from operator import itemgetter +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines +import librosa +import numpy as np +import tqdm +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.datasets.preprocess_utils import compare_duration_and_mel_length +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import get_phones_tones +from paddlespeech.t2s.datasets.preprocess_utils import get_spk_id_map +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.utils import str2bool + + +def process_sentence(config: Dict[str, Any], + fp: Path, + sentences: Dict, + output_dir: Path, + mel_extractor=None, + cut_sil: bool=True): + utt_id = fp.stem + record = None + if utt_id in sentences: + # reading, resampling may occur + wav, _ = librosa.load(str(fp), sr=config.fs) + if len(wav.shape) != 1 or np.abs(wav).max() > 1.0: + return record + assert len(wav.shape) == 1, f"{utt_id} is not a mono-channel audio." + assert np.abs(wav).max( + ) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM." + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + d_cumsum = np.pad(np.array(durations).cumsum(0), (1, 0), 'constant') + # little imprecise than use *.TextGrid directly + times = librosa.frames_to_time( + d_cumsum, sr=config.fs, hop_length=config.n_shift) + if cut_sil: + start = 0 + end = d_cumsum[-1] + if phones[0] == "sil" and len(durations) > 1: + start = times[1] + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + end = times[-2] + durations = durations[:-1] + phones = phones[:-1] + sentences[utt_id][0] = phones + sentences[utt_id][1] = durations + start, end = librosa.time_to_samples([start, end], sr=config.fs) + wav = wav[start:end] + + # extract mel feats + logmel = mel_extractor.get_log_mel_fbank(wav) + # change duration according to mel_length + compare_duration_and_mel_length(sentences, utt_id, logmel) + # utt_id may be popped in compare_duration_and_mel_length + if utt_id not in sentences: + return None + labels = sentences[utt_id][0] + # extract phone and duration + phones = [] + tones = [] + for label in labels: + # split tone from finals + match = re.match(r'^(\w+)([012345])$', label) + if match: + phones.append(match.group(1)) + tones.append(match.group(2)) + else: + phones.append(label) + tones.append('0') + durations = sentences[utt_id][1] + num_frames = logmel.shape[0] + assert sum(durations) == num_frames + assert len(phones) == len(tones) == len(durations) + + mel_path = output_dir / (utt_id + "_feats.npy") + np.save(mel_path, logmel) # (num_frames, n_mels) + record = { + "utt_id": utt_id, + "phones": phones, + "tones": tones, + "speaker": speaker, + "num_phones": len(phones), + "num_frames": num_frames, + "durations": durations, + "feats": str(mel_path), # Path object + } + return record + + +def process_sentences(config, + fps: List[Path], + sentences: Dict, + output_dir: Path, + mel_extractor=None, + nprocs: int=1, + cut_sil: bool=True, + use_relative_path: bool=False): + if nprocs == 1: + results = [] + for fp in tqdm.tqdm(fps, total=len(fps)): + record = process_sentence(config, fp, sentences, output_dir, + mel_extractor, cut_sil) + if record: + results.append(record) + else: + with ThreadPoolExecutor(nprocs) as pool: + futures = [] + with tqdm.tqdm(total=len(fps)) as progress: + for fp in fps: + future = pool.submit(process_sentence, config, fp, + sentences, output_dir, mel_extractor, + cut_sil) + future.add_done_callback(lambda p: progress.update()) + futures.append(future) + + results = [] + for ft in futures: + record = ft.result() + if record: + results.append(record) + + results.sort(key=itemgetter("utt_id")) + output_dir = Path(output_dir) + metadata_path = output_dir / "metadata.jsonl" + # NOTE: use relative path to the meta jsonlines file for Full Chain Project + with jsonlines.open(metadata_path, 'w') as writer: + for item in results: + if use_relative_path: + item["feats"] = str(Path(item["feats"]).relative_to(output_dir)) + writer.write(item) + print("Done") + + +def main(): + # parse config and args + parser = argparse.ArgumentParser( + description="Preprocess audio and then extract features.") + + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker} now") + + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump feature files.") + + parser.add_argument( + "--dur-file", + default=None, + type=str, + help="path to baker durations.txt.") + + parser.add_argument("--config", type=str, help="fastspeech2 config file.") + + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + parser.add_argument( + "--num-cpu", type=int, default=1, help="number of process.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + + parser.add_argument( + "--use-relative-path", + type=str2bool, + default=False, + help="whether use relative path in metadata") + + args = parser.parse_args() + + rootdir = Path(args.rootdir).expanduser() + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + dur_file = Path(args.dur_file).expanduser() + + assert rootdir.is_dir() + assert dur_file.is_file() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + if args.verbose > 1: + print(vars(args)) + print(config) + + sentences, speaker_set = get_phn_dur(dur_file) + + merge_silence(sentences) + phone_id_map_path = dumpdir / "phone_id_map.txt" + tone_id_map_path = dumpdir / "tone_id_map.txt" + get_phones_tones(sentences, phone_id_map_path, tone_id_map_path, + args.dataset) + speaker_id_map_path = dumpdir / "speaker_id_map.txt" + get_spk_id_map(speaker_set, speaker_id_map_path) + + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + # split data into 3 sections + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + + train_dump_dir = dumpdir / "train" / "raw" + train_dump_dir.mkdir(parents=True, exist_ok=True) + dev_dump_dir = dumpdir / "dev" / "raw" + dev_dump_dir.mkdir(parents=True, exist_ok=True) + test_dump_dir = dumpdir / "test" / "raw" + test_dump_dir.mkdir(parents=True, exist_ok=True) + + # Extractor + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + + # process for the 3 sections + if train_wav_files: + process_sentences( + config, + train_wav_files, + sentences, + train_dump_dir, + mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + use_relative_path=args.use_relative_path) + if dev_wav_files: + process_sentences( + config, + dev_wav_files, + sentences, + dev_dump_dir, + mel_extractor, + cut_sil=args.cut_sil, + use_relative_path=args.use_relative_path) + if test_wav_files: + process_sentences( + config, + test_wav_files, + sentences, + test_dump_dir, + mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + use_relative_path=args.use_relative_path) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/synthesize_e2e.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/synthesize_e2e.py new file mode 100644 index 0000000..cb742c5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/synthesize_e2e.py @@ -0,0 +1,203 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# remain for chains +import argparse +import logging +import os +from pathlib import Path + +import numpy as np +import paddle +import soundfile as sf +import yaml +from paddle import jit +from paddle.static import InputSpec +from yacs.config import CfgNode + +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.t2s.models.parallel_wavegan import PWGGenerator +from paddlespeech.t2s.models.parallel_wavegan import PWGInference +from paddlespeech.t2s.models.speedyspeech import SpeedySpeech +from paddlespeech.t2s.models.speedyspeech import SpeedySpeechInference +from paddlespeech.t2s.modules.normalizer import ZScore + + +def evaluate(args, speedyspeech_config, pwg_config): + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for evaluation + sentences = [] + with open(args.text, 'rt') as f: + for line in f: + items = line.strip().split() + utt_id = items[0] + sentence = "".join(items[1:]) + sentences.append((utt_id, sentence)) + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + with open(args.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + model = SpeedySpeech( + vocab_size=vocab_size, + tone_size=tone_size, + **speedyspeech_config["model"]) + model.set_state_dict( + paddle.load(args.speedyspeech_checkpoint)["main_params"]) + model.eval() + + vocoder = PWGGenerator(**pwg_config["generator_params"]) + vocoder.set_state_dict(paddle.load(args.pwg_checkpoint)["generator_params"]) + vocoder.remove_weight_norm() + vocoder.eval() + print("model done!") + + stat = np.load(args.speedyspeech_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + speedyspeech_normalizer = ZScore(mu, std) + + stat = np.load(args.pwg_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + pwg_normalizer = ZScore(mu, std) + + speedyspeech_inference = SpeedySpeechInference(speedyspeech_normalizer, + model) + speedyspeech_inference.eval() + speedyspeech_inference = jit.to_static( + speedyspeech_inference, + input_spec=[ + InputSpec([-1], dtype=paddle.int64), InputSpec( + [-1], dtype=paddle.int64) + ]) + paddle.jit.save(speedyspeech_inference, + os.path.join(args.inference_dir, "speedyspeech")) + speedyspeech_inference = paddle.jit.load( + os.path.join(args.inference_dir, "speedyspeech")) + + pwg_inference = PWGInference(pwg_normalizer, vocoder) + pwg_inference.eval() + pwg_inference = jit.to_static( + pwg_inference, input_spec=[ + InputSpec([-1, 80], dtype=paddle.float32), + ]) + paddle.jit.save(pwg_inference, os.path.join(args.inference_dir, "pwg")) + pwg_inference = paddle.jit.load(os.path.join(args.inference_dir, "pwg")) + + frontend = Frontend( + phone_vocab_path=args.phones_dict, tone_vocab_path=args.tones_dict) + print("frontend done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + for utt_id, sentence in sentences: + input_ids = frontend.get_input_ids( + sentence, merge_sentences=True, get_tone_ids=True) + phone_ids = input_ids["phone_ids"] + tone_ids = input_ids["tone_ids"] + + flags = 0 + for i in range(len(phone_ids)): + part_phone_ids = phone_ids[i] + part_tone_ids = tone_ids[i] + with paddle.no_grad(): + mel = speedyspeech_inference(part_phone_ids, part_tone_ids) + temp_wav = pwg_inference(mel) + if flags == 0: + wav = temp_wav + flags = 1 + else: + wav = paddle.concat([wav, temp_wav]) + sf.write( + output_dir / (utt_id + ".wav"), + wav.numpy(), + samplerate=speedyspeech_config.fs) + print(f"{utt_id} done!") + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with speedyspeech & parallel wavegan.") + parser.add_argument( + "--speedyspeech-config", type=str, help="config file for speedyspeech.") + parser.add_argument( + "--speedyspeech-checkpoint", + type=str, + help="speedyspeech checkpoint to load.") + parser.add_argument( + "--speedyspeech-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training speedyspeech." + ) + parser.add_argument( + "--pwg-config", type=str, help="config file for parallelwavegan.") + parser.add_argument( + "--pwg-checkpoint", + type=str, + help="parallel wavegan checkpoint to load.") + parser.add_argument( + "--pwg-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training speedyspeech." + ) + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones-dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument("--output-dir", type=str, help="output dir") + parser.add_argument( + "--inference-dir", type=str, help="dir to save inference models") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args, _ = parser.parse_known_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.speedyspeech_config) as f: + speedyspeech_config = CfgNode(yaml.safe_load(f)) + with open(args.pwg_config) as f: + pwg_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(speedyspeech_config) + print(pwg_config) + + evaluate(args, speedyspeech_config, pwg_config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/speedyspeech/train.py b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/train.py new file mode 100644 index 0000000..bda5370 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/speedyspeech/train.py @@ -0,0 +1,239 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.am_batch_fn import speedyspeech_multi_spk_batch_fn +from paddlespeech.t2s.datasets.am_batch_fn import speedyspeech_single_spk_batch_fn +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.speedyspeech import SpeedySpeech +from paddlespeech.t2s.models.speedyspeech import SpeedySpeechEvaluator +from paddlespeech.t2s.models.speedyspeech import SpeedySpeechUpdater +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.optimizer import build_optimizers +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.t2s.utils import str2bool + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + fields = [ + "phones", "tones", "num_phones", "num_frames", "feats", "durations" + ] + + spk_num = None + if args.speaker_dict is not None: + print("multiple speaker speedyspeech!") + collate_fn = speedyspeech_multi_spk_batch_fn + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + fields += ["spk_id"] + else: + print("single speaker speedyspeech!") + collate_fn = speedyspeech_single_spk_batch_fn + print("spk_num:", spk_num) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + if args.use_relative_path: + # if use_relative_path in preprocess, covert it to absolute path here + metadata_dir = Path(args.train_metadata).parent + for item in train_metadata: + item["feats"] = str(metadata_dir / item["feats"]) + + train_dataset = DataTable( + data=train_metadata, + fields=fields, + converters={ + "feats": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + if args.use_relative_path: + # if use_relative_path in preprocess, covert it to absolute path here + metadata_dir = Path(args.dev_metadata).parent + for item in dev_metadata: + item["feats"] = str(metadata_dir / item["feats"]) + + dev_dataset = DataTable( + data=dev_metadata, + fields=fields, + converters={ + "feats": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + print("samplers done!") + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=collate_fn, + num_workers=config.num_workers) + dev_dataloader = DataLoader( + dev_dataset, + shuffle=False, + drop_last=False, + batch_size=config.batch_size, + collate_fn=collate_fn, + num_workers=config.num_workers) + print("dataloaders done!") + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + with open(args.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + model = SpeedySpeech( + vocab_size=vocab_size, + tone_size=tone_size, + spk_num=spk_num, + **config["model"]) + if world_size > 1: + model = DataParallel(model) + print("model done!") + optimizer = build_optimizers(model, **config["optimizer"]) + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = SpeedySpeechUpdater( + model=model, + optimizer=optimizer, + dataloader=train_dataloader, + output_dir=output_dir) + + trainer = Trainer(updater, (config.max_epoch, 'epoch'), output_dir) + + evaluator = SpeedySpeechEvaluator( + model, dev_dataloader, output_dir=output_dir) + + if dist.get_rank() == 0: + trainer.extend(evaluator, trigger=(1, "epoch")) + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + trainer.extend( + Snapshot(max_size=config.num_snapshots), trigger=(1, 'epoch')) + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Train a Speedyspeech model with a single speaker dataset.") + parser.add_argument("--config", type=str, help="config file.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + parser.add_argument( + "--use-relative-path", + type=str2bool, + default=False, + help="whether use relative path in metadata") + + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + + parser.add_argument( + "--tones-dict", type=str, default=None, help="tone vocabulary file.") + + parser.add_argument( + "--speaker-dict", + type=str, + default=None, + help="speaker id map file for multiple speaker model.") + + # 这里可以多传入 max_epoch 等 + args, rest = parser.parse_known_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + if rest: + extra = [] + # to support key=value format + for item in rest: + # remove "--" + item = item[2:] + extra.extend(item.split("=", maxsplit=1)) + config.merge_from_list(extra) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/syn_utils.py b/ernie-sat/paddlespeech/t2s/exps/syn_utils.py new file mode 100644 index 0000000..c52cb37 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/syn_utils.py @@ -0,0 +1,243 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import numpy as np +import paddle +from paddle import jit +from paddle.static import InputSpec + +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.frontend import English +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.t2s.modules.normalizer import ZScore + +model_alias = { + # acoustic model + "speedyspeech": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeech", + "speedyspeech_inference": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeechInference", + "fastspeech2": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2", + "fastspeech2_inference": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2Inference", + "tacotron2": + "paddlespeech.t2s.models.tacotron2:Tacotron2", + "tacotron2_inference": + "paddlespeech.t2s.models.tacotron2:Tacotron2Inference", + # voc + "pwgan": + "paddlespeech.t2s.models.parallel_wavegan:PWGGenerator", + "pwgan_inference": + "paddlespeech.t2s.models.parallel_wavegan:PWGInference", + "mb_melgan": + "paddlespeech.t2s.models.melgan:MelGANGenerator", + "mb_melgan_inference": + "paddlespeech.t2s.models.melgan:MelGANInference", + "style_melgan": + "paddlespeech.t2s.models.melgan:StyleMelGANGenerator", + "style_melgan_inference": + "paddlespeech.t2s.models.melgan:StyleMelGANInference", + "hifigan": + "paddlespeech.t2s.models.hifigan:HiFiGANGenerator", + "hifigan_inference": + "paddlespeech.t2s.models.hifigan:HiFiGANInference", + "wavernn": + "paddlespeech.t2s.models.wavernn:WaveRNN", + "wavernn_inference": + "paddlespeech.t2s.models.wavernn:WaveRNNInference", +} + + +# input +def get_sentences(args): + # construct dataset for evaluation + sentences = [] + with open(args.text, 'rt') as f: + for line in f: + items = line.strip().split() + utt_id = items[0] + if 'lang' in args and args.lang == 'zh': + sentence = "".join(items[1:]) + elif 'lang' in args and args.lang == 'en': + sentence = " ".join(items[1:]) + sentences.append((utt_id, sentence)) + return sentences + + +def get_test_dataset(args, test_metadata, am_name, am_dataset): + if am_name == 'fastspeech2': + fields = ["utt_id", "text"] + if am_dataset in {"aishell3", "vctk"} and args.speaker_dict: + print("multiple speaker fastspeech2!") + fields += ["spk_id"] + elif 'voice_cloning' in args and args.voice_cloning: + print("voice cloning!") + fields += ["spk_emb"] + else: + print("single speaker fastspeech2!") + elif am_name == 'speedyspeech': + fields = ["utt_id", "phones", "tones"] + elif am_name == 'tacotron2': + fields = ["utt_id", "text"] + if 'voice_cloning' in args and args.voice_cloning: + print("voice cloning!") + fields += ["spk_emb"] + + test_dataset = DataTable(data=test_metadata, fields=fields) + return test_dataset + + +# frontend +def get_frontend(args): + if 'lang' in args and args.lang == 'zh': + frontend = Frontend( + phone_vocab_path=args.phones_dict, tone_vocab_path=args.tones_dict) + elif 'lang' in args and args.lang == 'en': + frontend = English(phone_vocab_path=args.phones_dict) + else: + print("wrong lang!") + print("frontend done!") + return frontend + + +# dygraph +def get_am_inference(args, am_config): + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + tone_size = None + if 'tones_dict' in args and args.tones_dict: + with open(args.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + spk_num = None + if 'speaker_dict' in args and args.speaker_dict: + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + print("spk_num:", spk_num) + + odim = am_config.n_mels + # model: {model_name}_{dataset} + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + + am_class = dynamic_import(am_name, model_alias) + am_inference_class = dynamic_import(am_name + '_inference', model_alias) + + if am_name == 'fastspeech2': + am = am_class( + idim=vocab_size, odim=odim, spk_num=spk_num, **am_config["model"]) + elif am_name == 'speedyspeech': + am = am_class( + vocab_size=vocab_size, + tone_size=tone_size, + spk_num=spk_num, + **am_config["model"]) + elif am_name == 'tacotron2': + am = am_class(idim=vocab_size, odim=odim, **am_config["model"]) + + am.set_state_dict(paddle.load(args.am_ckpt)["main_params"]) + am.eval() + am_mu, am_std = np.load(args.am_stat) + am_mu = paddle.to_tensor(am_mu) + am_std = paddle.to_tensor(am_std) + am_normalizer = ZScore(am_mu, am_std) + am_inference = am_inference_class(am_normalizer, am) + am_inference.eval() + print("acoustic model done!") + return am_inference, am_name, am_dataset + + +def get_voc_inference(args, voc_config): + # model: {model_name}_{dataset} + voc_name = args.voc[:args.voc.rindex('_')] + voc_class = dynamic_import(voc_name, model_alias) + voc_inference_class = dynamic_import(voc_name + '_inference', model_alias) + if voc_name != 'wavernn': + voc = voc_class(**voc_config["generator_params"]) + voc.set_state_dict(paddle.load(args.voc_ckpt)["generator_params"]) + voc.remove_weight_norm() + voc.eval() + else: + voc = voc_class(**voc_config["model"]) + voc.set_state_dict(paddle.load(args.voc_ckpt)["main_params"]) + voc.eval() + + voc_mu, voc_std = np.load(args.voc_stat) + voc_mu = paddle.to_tensor(voc_mu) + voc_std = paddle.to_tensor(voc_std) + voc_normalizer = ZScore(voc_mu, voc_std) + voc_inference = voc_inference_class(voc_normalizer, voc) + voc_inference.eval() + print("voc done!") + return voc_inference + + +# to static +def am_to_static(args, am_inference, am_name, am_dataset): + if am_name == 'fastspeech2': + if am_dataset in {"aishell3", "vctk"} and args.speaker_dict: + am_inference = jit.to_static( + am_inference, + input_spec=[ + InputSpec([-1], dtype=paddle.int64), + InputSpec([1], dtype=paddle.int64), + ], ) + else: + am_inference = jit.to_static( + am_inference, input_spec=[InputSpec([-1], dtype=paddle.int64)]) + + elif am_name == 'speedyspeech': + if am_dataset in {"aishell3", "vctk"} and args.speaker_dict: + am_inference = jit.to_static( + am_inference, + input_spec=[ + InputSpec([-1], dtype=paddle.int64), # text + InputSpec([-1], dtype=paddle.int64), # tone + InputSpec([1], dtype=paddle.int64), # spk_id + None # duration + ]) + else: + am_inference = jit.to_static( + am_inference, + input_spec=[ + InputSpec([-1], dtype=paddle.int64), + InputSpec([-1], dtype=paddle.int64) + ]) + + elif am_name == 'tacotron2': + am_inference = jit.to_static( + am_inference, input_spec=[InputSpec([-1], dtype=paddle.int64)]) + + paddle.jit.save(am_inference, os.path.join(args.inference_dir, args.am)) + am_inference = paddle.jit.load(os.path.join(args.inference_dir, args.am)) + return am_inference + + +def voc_to_static(args, voc_inference): + voc_inference = jit.to_static( + voc_inference, input_spec=[ + InputSpec([-1, 80], dtype=paddle.float32), + ]) + paddle.jit.save(voc_inference, os.path.join(args.inference_dir, args.voc)) + voc_inference = paddle.jit.load(os.path.join(args.inference_dir, args.voc)) + return voc_inference diff --git a/ernie-sat/paddlespeech/t2s/exps/synthesize.py b/ernie-sat/paddlespeech/t2s/exps/synthesize.py new file mode 100644 index 0000000..abb1eb4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/synthesize.py @@ -0,0 +1,200 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from timer import timer +from yacs.config import CfgNode + +from paddlespeech.t2s.exps.syn_utils import get_am_inference +from paddlespeech.t2s.exps.syn_utils import get_test_dataset +from paddlespeech.t2s.exps.syn_utils import get_voc_inference +from paddlespeech.t2s.utils import str2bool + + +def evaluate(args): + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for evaluation + with jsonlines.open(args.test_metadata, 'r') as reader: + test_metadata = list(reader) + + # Init body. + with open(args.am_config) as f: + am_config = CfgNode(yaml.safe_load(f)) + with open(args.voc_config) as f: + voc_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(am_config) + print(voc_config) + + # acoustic model + am_inference, am_name, am_dataset = get_am_inference(args, am_config) + test_dataset = get_test_dataset(args, test_metadata, am_name, am_dataset) + + # vocoder + voc_inference = get_voc_inference(args, voc_config) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + N = 0 + T = 0 + + for datum in test_dataset: + utt_id = datum["utt_id"] + with timer() as t: + with paddle.no_grad(): + # acoustic model + if am_name == 'fastspeech2': + phone_ids = paddle.to_tensor(datum["text"]) + spk_emb = None + spk_id = None + # multi speaker + if args.voice_cloning and "spk_emb" in datum: + spk_emb = paddle.to_tensor(np.load(datum["spk_emb"])) + elif "spk_id" in datum: + spk_id = paddle.to_tensor(datum["spk_id"]) + mel = am_inference( + phone_ids, spk_id=spk_id, spk_emb=spk_emb) + elif am_name == 'speedyspeech': + phone_ids = paddle.to_tensor(datum["phones"]) + tone_ids = paddle.to_tensor(datum["tones"]) + mel = am_inference(phone_ids, tone_ids) + elif am_name == 'tacotron2': + phone_ids = paddle.to_tensor(datum["text"]) + spk_emb = None + # multi speaker + if args.voice_cloning and "spk_emb" in datum: + spk_emb = paddle.to_tensor(np.load(datum["spk_emb"])) + mel = am_inference(phone_ids, spk_emb=spk_emb) + # vocoder + wav = voc_inference(mel) + + wav = wav.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = am_config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.size}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write( + str(output_dir / (utt_id + ".wav")), wav, samplerate=am_config.fs) + print(f"{utt_id} done!") + print(f"generation speed: {N / T}Hz, RTF: {am_config.fs / (N / T) }") + + +def parse_args(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with acoustic model & vocoder") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'speedyspeech_csmsc', 'fastspeech2_csmsc', 'fastspeech2_ljspeech', + 'fastspeech2_aishell3', 'fastspeech2_vctk', 'tacotron2_csmsc', + 'tacotron2_ljspeech', 'tacotron2_aishell3' + ], + help='Choose acoustic model type of tts task.') + parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument( + "--speaker_dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + "--voice-cloning", + type=str2bool, + default=False, + help="whether training voice cloning model.") + # vocoder + parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=[ + 'pwgan_csmsc', 'pwgan_ljspeech', 'pwgan_aishell3', 'pwgan_vctk', + 'mb_melgan_csmsc', 'wavernn_csmsc', 'hifigan_csmsc', + 'hifigan_ljspeech', 'hifigan_aishell3', 'hifigan_vctk', + 'style_melgan_csmsc' + ], + help='Choose vocoder type of tts task.') + parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + parser.add_argument( + '--voc_ckpt', type=str, default=None, help='Checkpoint file of voc.') + parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + # other + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument("--test_metadata", type=str, help="test metadata.") + parser.add_argument("--output_dir", type=str, help="output dir.") + + args = parser.parse_args() + return args + + +def main(): + + args = parse_args() + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + evaluate(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/synthesize_e2e.py b/ernie-sat/paddlespeech/t2s/exps/synthesize_e2e.py new file mode 100644 index 0000000..10b33c6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/synthesize_e2e.py @@ -0,0 +1,247 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +import paddle +import soundfile as sf +import yaml +from timer import timer +from yacs.config import CfgNode + +from paddlespeech.t2s.exps.syn_utils import am_to_static +from paddlespeech.t2s.exps.syn_utils import get_am_inference +from paddlespeech.t2s.exps.syn_utils import get_frontend +from paddlespeech.t2s.exps.syn_utils import get_sentences +from paddlespeech.t2s.exps.syn_utils import get_voc_inference +from paddlespeech.t2s.exps.syn_utils import voc_to_static + + +def evaluate(args): + + # Init body. + with open(args.am_config) as f: + am_config = CfgNode(yaml.safe_load(f)) + with open(args.voc_config) as f: + voc_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(am_config) + print(voc_config) + + sentences = get_sentences(args) + + # frontend + frontend = get_frontend(args) + + # acoustic model + am_inference, am_name, am_dataset = get_am_inference(args, am_config) + + # vocoder + voc_inference = get_voc_inference(args, voc_config) + + # whether dygraph to static + if args.inference_dir: + # acoustic model + am_inference = am_to_static(args, am_inference, am_name, am_dataset) + + # vocoder + voc_inference = voc_to_static(args, voc_inference) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + merge_sentences = False + # Avoid not stopping at the end of a sub sentence when tacotron2_ljspeech dygraph to static graph + # but still not stopping in the end (NOTE by yuantian01 Feb 9 2022) + if am_name == 'tacotron2': + merge_sentences = True + N = 0 + T = 0 + for utt_id, sentence in sentences: + with timer() as t: + get_tone_ids = False + if am_name == 'speedyspeech': + get_tone_ids = True + if args.lang == 'zh': + input_ids = frontend.get_input_ids( + sentence, + merge_sentences=merge_sentences, + get_tone_ids=get_tone_ids) + phone_ids = input_ids["phone_ids"] + if get_tone_ids: + tone_ids = input_ids["tone_ids"] + elif args.lang == 'en': + input_ids = frontend.get_input_ids( + sentence, merge_sentences=merge_sentences) + phone_ids = input_ids["phone_ids"] + else: + print("lang should in {'zh', 'en'}!") + with paddle.no_grad(): + flags = 0 + for i in range(len(phone_ids)): + part_phone_ids = phone_ids[i] + # acoustic model + if am_name == 'fastspeech2': + # multi speaker + if am_dataset in {"aishell3", "vctk"}: + spk_id = paddle.to_tensor(args.spk_id) + mel = am_inference(part_phone_ids, spk_id) + else: + mel = am_inference(part_phone_ids) + elif am_name == 'speedyspeech': + part_tone_ids = tone_ids[i] + if am_dataset in {"aishell3", "vctk"}: + spk_id = paddle.to_tensor(args.spk_id) + mel = am_inference(part_phone_ids, part_tone_ids, + spk_id) + else: + mel = am_inference(part_phone_ids, part_tone_ids) + elif am_name == 'tacotron2': + mel = am_inference(part_phone_ids) + # vocoder + wav = voc_inference(mel) + if flags == 0: + wav_all = wav + flags = 1 + else: + wav_all = paddle.concat([wav_all, wav]) + wav = wav_all.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = am_config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write( + str(output_dir / (utt_id + ".wav")), wav, samplerate=am_config.fs) + print(f"{utt_id} done!") + print(f"generation speed: {N / T}Hz, RTF: {am_config.fs / (N / T) }") + + +def parse_args(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with acoustic model & vocoder") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'speedyspeech_csmsc', 'speedyspeech_aishell3', 'fastspeech2_csmsc', + 'fastspeech2_ljspeech', 'fastspeech2_aishell3', 'fastspeech2_vctk', + 'tacotron2_csmsc', 'tacotron2_ljspeech' + ], + help='Choose acoustic model type of tts task.') + parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument( + "--speaker_dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + '--spk_id', + type=int, + default=0, + help='spk id for multi speaker acoustic model') + # vocoder + parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=[ + 'pwgan_csmsc', + 'pwgan_ljspeech', + 'pwgan_aishell3', + 'pwgan_vctk', + 'mb_melgan_csmsc', + 'style_melgan_csmsc', + 'hifigan_csmsc', + 'hifigan_ljspeech', + 'hifigan_aishell3', + 'hifigan_vctk', + 'wavernn_csmsc', + ], + help='Choose vocoder type of tts task.') + parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + parser.add_argument( + '--voc_ckpt', type=str, default=None, help='Checkpoint file of voc.') + parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + # other + parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en') + + parser.add_argument( + "--inference_dir", + type=str, + default=None, + help="dir to save inference models") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line.") + parser.add_argument("--output_dir", type=str, help="output dir.") + + args = parser.parse_args() + return args + + +def main(): + args = parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + evaluate(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/synthesize_streaming.py b/ernie-sat/paddlespeech/t2s/exps/synthesize_streaming.py new file mode 100644 index 0000000..7b9906c --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/synthesize_streaming.py @@ -0,0 +1,273 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import math +from pathlib import Path + +import numpy as np +import paddle +import soundfile as sf +import yaml +from timer import timer +from yacs.config import CfgNode + +from paddlespeech.s2t.utils.dynamic_import import dynamic_import +from paddlespeech.t2s.exps.syn_utils import get_frontend +from paddlespeech.t2s.exps.syn_utils import get_sentences +from paddlespeech.t2s.exps.syn_utils import get_voc_inference +from paddlespeech.t2s.exps.syn_utils import model_alias +from paddlespeech.t2s.utils import str2bool + + +def denorm(data, mean, std): + return data * std + mean + + +def get_chunks(data, chunk_size, pad_size): + data_len = data.shape[1] + chunks = [] + n = math.ceil(data_len / chunk_size) + for i in range(n): + start = max(0, i * chunk_size - pad_size) + end = min((i + 1) * chunk_size + pad_size, data_len) + chunks.append(data[:, start:end, :]) + return chunks + + +def evaluate(args): + + # Init body. + with open(args.am_config) as f: + am_config = CfgNode(yaml.safe_load(f)) + with open(args.voc_config) as f: + voc_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(am_config) + print(voc_config) + + sentences = get_sentences(args) + + # frontend + frontend = get_frontend(args) + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + # acoustic model, only support fastspeech2 here now! + # am_inference, am_name, am_dataset = get_am_inference(args, am_config) + # model: {model_name}_{dataset} + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + odim = am_config.n_mels + + am_class = dynamic_import(am_name, model_alias) + am = am_class(idim=vocab_size, odim=odim, **am_config["model"]) + am.set_state_dict(paddle.load(args.am_ckpt)["main_params"]) + am.eval() + am_mu, am_std = np.load(args.am_stat) + am_mu = paddle.to_tensor(am_mu) + am_std = paddle.to_tensor(am_std) + + # vocoder + voc_inference = get_voc_inference(args, voc_config) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + merge_sentences = True + get_tone_ids = False + + N = 0 + T = 0 + chunk_size = args.chunk_size + pad_size = args.pad_size + + for utt_id, sentence in sentences: + with timer() as t: + if args.lang == 'zh': + input_ids = frontend.get_input_ids( + sentence, + merge_sentences=merge_sentences, + get_tone_ids=get_tone_ids) + + phone_ids = input_ids["phone_ids"] + else: + print("lang should in be 'zh' here!") + # merge_sentences=True here, so we only use the first item of phone_ids + phone_ids = phone_ids[0] + with paddle.no_grad(): + # acoustic model + orig_hs, h_masks = am.encoder_infer(phone_ids) + + if args.am_streaming: + hss = get_chunks(orig_hs, chunk_size, pad_size) + chunk_num = len(hss) + mel_list = [] + for i, hs in enumerate(hss): + before_outs, _ = am.decoder(hs) + after_outs = before_outs + am.postnet( + before_outs.transpose((0, 2, 1))).transpose( + (0, 2, 1)) + normalized_mel = after_outs[0] + sub_mel = denorm(normalized_mel, am_mu, am_std) + # clip output part of pad + if i == 0: + sub_mel = sub_mel[:-pad_size] + elif i == chunk_num - 1: + # 最后一块的右侧一定没有 pad 够 + sub_mel = sub_mel[pad_size:] + else: + # 倒数几块的右侧也可能没有 pad 够 + sub_mel = sub_mel[pad_size:(chunk_size + pad_size) - + sub_mel.shape[0]] + mel_list.append(sub_mel) + mel = paddle.concat(mel_list, axis=0) + + else: + before_outs, _ = am.decoder(orig_hs) + after_outs = before_outs + am.postnet( + before_outs.transpose((0, 2, 1))).transpose((0, 2, 1)) + normalized_mel = after_outs[0] + mel = denorm(normalized_mel, am_mu, am_std) + + # vocoder + wav = voc_inference(mel) + + wav = wav.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = am_config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write( + str(output_dir / (utt_id + ".wav")), wav, samplerate=am_config.fs) + print(f"{utt_id} done!") + print(f"generation speed: {N / T}Hz, RTF: {am_config.fs / (N / T) }") + + +def parse_args(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with acoustic model & vocoder") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=['fastspeech2_csmsc'], + help='Choose acoustic model type of tts task.') + parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + + # vocoder + parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=[ + 'pwgan_csmsc', + 'pwgan_ljspeech', + 'pwgan_aishell3', + 'pwgan_vctk', + 'mb_melgan_csmsc', + 'style_melgan_csmsc', + 'hifigan_csmsc', + 'hifigan_ljspeech', + 'hifigan_aishell3', + 'hifigan_vctk', + 'wavernn_csmsc', + ], + help='Choose vocoder type of tts task.') + parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + parser.add_argument( + '--voc_ckpt', type=str, default=None, help='Checkpoint file of voc.') + parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + # other + parser.add_argument( + '--lang', + type=str, + default='zh', + help='Choose model language. zh or en') + + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line.") + + parser.add_argument( + "--am_streaming", + type=str2bool, + default=False, + help="whether use streaming acoustic model") + parser.add_argument( + "--chunk_size", type=int, default=42, help="chunk size of am streaming") + parser.add_argument( + "--pad_size", type=int, default=12, help="pad size of am streaming") + + parser.add_argument("--output_dir", type=str, help="output dir.") + + args = parser.parse_args() + return args + + +def main(): + args = parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + evaluate(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/tacotron2/__init__.py b/ernie-sat/paddlespeech/t2s/exps/tacotron2/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/tacotron2/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/tacotron2/normalize.py b/ernie-sat/paddlespeech/t2s/exps/tacotron2/normalize.py new file mode 100644 index 0000000..87e975b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/tacotron2/normalize.py @@ -0,0 +1,146 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Normalize feature files and dump them.""" +import argparse +import logging +from operator import itemgetter +from pathlib import Path + +import jsonlines +import numpy as np +from sklearn.preprocessing import StandardScaler +from tqdm import tqdm + +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + """Run preprocessing process.""" + parser = argparse.ArgumentParser( + description="Normalize dumped raw features (See detail in parallel_wavegan/bin/normalize.py)." + ) + parser.add_argument( + "--metadata", + type=str, + required=True, + help="directory including feature files to be normalized. " + "you need to specify either *-scp or rootdir.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump normalized feature files.") + parser.add_argument( + "--speech-stats", + type=str, + required=True, + help="speech statistics file.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + args = parser.parse_args() + + # set logger + if args.verbose > 1: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + elif args.verbose > 0: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + logging.warning('Skip DEBUG/INFO messages') + + # check directory existence + dumpdir = Path(args.dumpdir).resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + # get dataset + with jsonlines.open(args.metadata, 'r') as reader: + metadata = list(reader) + dataset = DataTable( + metadata, converters={ + "speech": np.load, + }) + logging.info(f"The number of files = {len(dataset)}.") + + # restore scaler + speech_scaler = StandardScaler() + speech_scaler.mean_ = np.load(args.speech_stats)[0] + speech_scaler.scale_ = np.load(args.speech_stats)[1] + speech_scaler.n_features_in_ = speech_scaler.mean_.shape[0] + + vocab_phones = {} + with open(args.phones_dict, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + vocab_phones[phn] = int(id) + + vocab_speaker = {} + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + for spk, id in spk_id: + vocab_speaker[spk] = int(id) + + # process each file + output_metadata = [] + + for item in tqdm(dataset): + utt_id = item['utt_id'] + speech = item['speech'] + # normalize + speech = speech_scaler.transform(speech) + speech_dir = dumpdir / "data_speech" + speech_dir.mkdir(parents=True, exist_ok=True) + speech_path = speech_dir / f"{utt_id}_speech.npy" + np.save(speech_path, speech.astype(np.float32), allow_pickle=False) + + phone_ids = [vocab_phones[p] for p in item['phones']] + spk_id = vocab_speaker[item["speaker"]] + record = { + "utt_id": item['utt_id'], + "spk_id": spk_id, + "text": phone_ids, + "text_lengths": item['text_lengths'], + "speech_lengths": item['speech_lengths'], + "speech": str(speech_path), + } + # add spk_emb for voice cloning + if "spk_emb" in item: + record["spk_emb"] = str(item["spk_emb"]) + output_metadata.append(record) + output_metadata.sort(key=itemgetter('utt_id')) + output_metadata_path = Path(args.dumpdir) / "metadata.jsonl" + with jsonlines.open(output_metadata_path, 'w') as writer: + for item in output_metadata: + writer.write(item) + logging.info(f"metadata dumped into {output_metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/tacotron2/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/tacotron2/preprocess.py new file mode 100644 index 0000000..14a0d7e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/tacotron2/preprocess.py @@ -0,0 +1,329 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from concurrent.futures import ThreadPoolExecutor +from operator import itemgetter +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines +import librosa +import numpy as np +import tqdm +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.datasets.preprocess_utils import compare_duration_and_mel_length +from paddlespeech.t2s.datasets.preprocess_utils import get_input_token +from paddlespeech.t2s.datasets.preprocess_utils import get_phn_dur +from paddlespeech.t2s.datasets.preprocess_utils import get_spk_id_map +from paddlespeech.t2s.datasets.preprocess_utils import merge_silence +from paddlespeech.t2s.utils import str2bool + + +def process_sentence(config: Dict[str, Any], + fp: Path, + sentences: Dict, + output_dir: Path, + mel_extractor=None, + cut_sil: bool=True, + spk_emb_dir: Path=None): + utt_id = fp.stem + # for vctk + if utt_id.endswith("_mic2"): + utt_id = utt_id[:-5] + record = None + if utt_id in sentences: + # reading, resampling may occur + wav, _ = librosa.load(str(fp), sr=config.fs) + if len(wav.shape) != 1 or np.abs(wav).max() > 1.0: + return record + assert len(wav.shape) == 1, f"{utt_id} is not a mono-channel audio." + assert np.abs(wav).max( + ) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM." + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + speaker = sentences[utt_id][2] + d_cumsum = np.pad(np.array(durations).cumsum(0), (1, 0), 'constant') + # little imprecise than use *.TextGrid directly + times = librosa.frames_to_time( + d_cumsum, sr=config.fs, hop_length=config.n_shift) + if cut_sil: + start = 0 + end = d_cumsum[-1] + if phones[0] == "sil" and len(durations) > 1: + start = times[1] + durations = durations[1:] + phones = phones[1:] + if phones[-1] == 'sil' and len(durations) > 1: + end = times[-2] + durations = durations[:-1] + phones = phones[:-1] + sentences[utt_id][0] = phones + sentences[utt_id][1] = durations + start, end = librosa.time_to_samples([start, end], sr=config.fs) + wav = wav[start:end] + # extract mel feats + logmel = mel_extractor.get_log_mel_fbank(wav) + # change duration according to mel_length + compare_duration_and_mel_length(sentences, utt_id, logmel) + # utt_id may be popped in compare_duration_and_mel_length + if utt_id not in sentences: + return None + phones = sentences[utt_id][0] + durations = sentences[utt_id][1] + num_frames = logmel.shape[0] + assert sum(durations) == num_frames + mel_dir = output_dir / "data_speech" + mel_dir.mkdir(parents=True, exist_ok=True) + mel_path = mel_dir / (utt_id + "_speech.npy") + np.save(mel_path, logmel) + record = { + "utt_id": utt_id, + "phones": phones, + "text_lengths": len(phones), + "speech_lengths": num_frames, + "speech": str(mel_path), + "speaker": speaker + } + if spk_emb_dir: + if speaker in os.listdir(spk_emb_dir): + embed_name = utt_id + ".npy" + embed_path = spk_emb_dir / speaker / embed_name + if embed_path.is_file(): + record["spk_emb"] = str(embed_path) + else: + return None + return record + + +def process_sentences(config, + fps: List[Path], + sentences: Dict, + output_dir: Path, + mel_extractor=None, + nprocs: int=1, + cut_sil: bool=True, + spk_emb_dir: Path=None): + if nprocs == 1: + results = [] + for fp in fps: + record = process_sentence(config, fp, sentences, output_dir, + mel_extractor, cut_sil, spk_emb_dir) + if record: + results.append(record) + else: + with ThreadPoolExecutor(nprocs) as pool: + futures = [] + with tqdm.tqdm(total=len(fps)) as progress: + for fp in fps: + future = pool.submit(process_sentence, config, fp, + sentences, output_dir, mel_extractor, + cut_sil, spk_emb_dir) + future.add_done_callback(lambda p: progress.update()) + futures.append(future) + + results = [] + for ft in futures: + record = ft.result() + if record: + results.append(record) + + results.sort(key=itemgetter("utt_id")) + with jsonlines.open(output_dir / "metadata.jsonl", 'w') as writer: + for item in results: + writer.write(item) + print("Done") + + +def main(): + # parse config and args + parser = argparse.ArgumentParser( + description="Preprocess audio and then extract features.") + + parser.add_argument( + "--dataset", + default="baker", + type=str, + help="name of dataset, should in {baker, aishell3, ljspeech, vctk} now") + + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump feature files.") + parser.add_argument( + "--dur-file", default=None, type=str, help="path to durations.txt.") + + parser.add_argument("--config", type=str, help="fastspeech2 config file.") + + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + parser.add_argument( + "--num-cpu", type=int, default=1, help="number of process.") + + parser.add_argument( + "--cut-sil", + type=str2bool, + default=True, + help="whether cut sil in the edge of audio") + + parser.add_argument( + "--spk_emb_dir", + default=None, + type=str, + help="directory to speaker embedding files.") + args = parser.parse_args() + + rootdir = Path(args.rootdir).expanduser() + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + dur_file = Path(args.dur_file).expanduser() + + if args.spk_emb_dir: + spk_emb_dir = Path(args.spk_emb_dir).expanduser().resolve() + else: + spk_emb_dir = None + + assert rootdir.is_dir() + assert dur_file.is_file() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + if args.verbose > 1: + print(vars(args)) + print(config) + + sentences, speaker_set = get_phn_dur(dur_file) + + merge_silence(sentences) + phone_id_map_path = dumpdir / "phone_id_map.txt" + speaker_id_map_path = dumpdir / "speaker_id_map.txt" + get_input_token(sentences, phone_id_map_path, args.dataset) + get_spk_id_map(speaker_set, speaker_id_map_path) + + if args.dataset == "baker": + wav_files = sorted(list((rootdir / "Wave").rglob("*.wav"))) + # split data into 3 sections + num_train = 9800 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "aishell3": + sub_num_dev = 5 + wav_dir = rootdir / "train" / "wav" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*.wav"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + elif args.dataset == "ljspeech": + wav_files = sorted(list((rootdir / "wavs").rglob("*.wav"))) + # split data into 3 sections + num_train = 12900 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + elif args.dataset == "vctk": + sub_num_dev = 5 + wav_dir = rootdir / "wav48_silence_trimmed" + train_wav_files = [] + dev_wav_files = [] + test_wav_files = [] + for speaker in os.listdir(wav_dir): + wav_files = sorted(list((wav_dir / speaker).rglob("*_mic2.flac"))) + if len(wav_files) > 100: + train_wav_files += wav_files[:-sub_num_dev * 2] + dev_wav_files += wav_files[-sub_num_dev * 2:-sub_num_dev] + test_wav_files += wav_files[-sub_num_dev:] + else: + train_wav_files += wav_files + + else: + print("dataset should in {baker, aishell3, ljspeech, vctk} now!") + + train_dump_dir = dumpdir / "train" / "raw" + train_dump_dir.mkdir(parents=True, exist_ok=True) + dev_dump_dir = dumpdir / "dev" / "raw" + dev_dump_dir.mkdir(parents=True, exist_ok=True) + test_dump_dir = dumpdir / "test" / "raw" + test_dump_dir.mkdir(parents=True, exist_ok=True) + + # Extractor + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + + # process for the 3 sections + if train_wav_files: + process_sentences( + config, + train_wav_files, + sentences, + train_dump_dir, + mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + if dev_wav_files: + process_sentences( + config, + dev_wav_files, + sentences, + dev_dump_dir, + mel_extractor, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + if test_wav_files: + process_sentences( + config, + test_wav_files, + sentences, + test_dump_dir, + mel_extractor, + nprocs=args.num_cpu, + cut_sil=args.cut_sil, + spk_emb_dir=spk_emb_dir) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/tacotron2/train.py b/ernie-sat/paddlespeech/t2s/exps/tacotron2/train.py new file mode 100644 index 0000000..69ff80e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/tacotron2/train.py @@ -0,0 +1,202 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.am_batch_fn import tacotron2_multi_spk_batch_fn +from paddlespeech.t2s.datasets.am_batch_fn import tacotron2_single_spk_batch_fn +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.tacotron2 import Tacotron2 +from paddlespeech.t2s.models.tacotron2 import Tacotron2Evaluator +from paddlespeech.t2s.models.tacotron2 import Tacotron2Updater +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.optimizer import build_optimizers +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.t2s.utils import str2bool + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + world_size = paddle.distributed.get_world_size() + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + fields = [ + "text", + "text_lengths", + "speech", + "speech_lengths", + ] + + converters = { + "speech": np.load, + } + if args.voice_cloning: + print("Training voice cloning!") + collate_fn = tacotron2_multi_spk_batch_fn + fields += ["spk_emb"] + converters["spk_emb"] = np.load + else: + print("single speaker tacotron2!") + collate_fn = tacotron2_single_spk_batch_fn + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=fields, + converters=converters, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=fields, + converters=converters, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + + print("samplers done!") + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=collate_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + shuffle=False, + drop_last=False, + batch_size=config.batch_size, + collate_fn=collate_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + odim = config.n_mels + model = Tacotron2(idim=vocab_size, odim=odim, **config["model"]) + if world_size > 1: + model = DataParallel(model) + print("model done!") + + optimizer = build_optimizers(model, **config["optimizer"]) + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = Tacotron2Updater( + model=model, + optimizer=optimizer, + dataloader=train_dataloader, + output_dir=output_dir, + **config["updater"]) + + trainer = Trainer(updater, (config.max_epoch, 'epoch'), output_dir) + + evaluator = Tacotron2Evaluator( + model, dev_dataloader, output_dir=output_dir, **config["updater"]) + + if dist.get_rank() == 0: + trainer.extend(evaluator, trigger=(1, "epoch")) + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + trainer.extend( + Snapshot(max_size=config.num_snapshots), trigger=(1, 'epoch')) + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Train a Tacotron2 model.") + parser.add_argument("--config", type=str, help="tacotron2 config file.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + + parser.add_argument( + "--voice-cloning", + type=str2bool, + default=False, + help="whether training voice cloning model.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/__init__.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/normalize.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/normalize.py new file mode 100644 index 0000000..87e975b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/normalize.py @@ -0,0 +1,146 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Normalize feature files and dump them.""" +import argparse +import logging +from operator import itemgetter +from pathlib import Path + +import jsonlines +import numpy as np +from sklearn.preprocessing import StandardScaler +from tqdm import tqdm + +from paddlespeech.t2s.datasets.data_table import DataTable + + +def main(): + """Run preprocessing process.""" + parser = argparse.ArgumentParser( + description="Normalize dumped raw features (See detail in parallel_wavegan/bin/normalize.py)." + ) + parser.add_argument( + "--metadata", + type=str, + required=True, + help="directory including feature files to be normalized. " + "you need to specify either *-scp or rootdir.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump normalized feature files.") + parser.add_argument( + "--speech-stats", + type=str, + required=True, + help="speech statistics file.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--speaker-dict", type=str, default=None, help="speaker id map file.") + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + args = parser.parse_args() + + # set logger + if args.verbose > 1: + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + elif args.verbose > 0: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + else: + logging.basicConfig( + level=logging.WARN, + format="%(asctime)s (%(module)s:%(lineno)d) %(levelname)s: %(message)s" + ) + logging.warning('Skip DEBUG/INFO messages') + + # check directory existence + dumpdir = Path(args.dumpdir).resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + # get dataset + with jsonlines.open(args.metadata, 'r') as reader: + metadata = list(reader) + dataset = DataTable( + metadata, converters={ + "speech": np.load, + }) + logging.info(f"The number of files = {len(dataset)}.") + + # restore scaler + speech_scaler = StandardScaler() + speech_scaler.mean_ = np.load(args.speech_stats)[0] + speech_scaler.scale_ = np.load(args.speech_stats)[1] + speech_scaler.n_features_in_ = speech_scaler.mean_.shape[0] + + vocab_phones = {} + with open(args.phones_dict, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + vocab_phones[phn] = int(id) + + vocab_speaker = {} + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + for spk, id in spk_id: + vocab_speaker[spk] = int(id) + + # process each file + output_metadata = [] + + for item in tqdm(dataset): + utt_id = item['utt_id'] + speech = item['speech'] + # normalize + speech = speech_scaler.transform(speech) + speech_dir = dumpdir / "data_speech" + speech_dir.mkdir(parents=True, exist_ok=True) + speech_path = speech_dir / f"{utt_id}_speech.npy" + np.save(speech_path, speech.astype(np.float32), allow_pickle=False) + + phone_ids = [vocab_phones[p] for p in item['phones']] + spk_id = vocab_speaker[item["speaker"]] + record = { + "utt_id": item['utt_id'], + "spk_id": spk_id, + "text": phone_ids, + "text_lengths": item['text_lengths'], + "speech_lengths": item['speech_lengths'], + "speech": str(speech_path), + } + # add spk_emb for voice cloning + if "spk_emb" in item: + record["spk_emb"] = str(item["spk_emb"]) + output_metadata.append(record) + output_metadata.sort(key=itemgetter('utt_id')) + output_metadata_path = Path(args.dumpdir) / "metadata.jsonl" + with jsonlines.open(output_metadata_path, 'w') as writer: + for item in output_metadata: + writer.write(item) + logging.info(f"metadata dumped into {output_metadata_path}") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/preprocess.py new file mode 100644 index 0000000..9aa87e9 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/preprocess.py @@ -0,0 +1,275 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from concurrent.futures import ThreadPoolExecutor +from operator import itemgetter +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines +import librosa +import numpy as np +import tqdm +import yaml +from yacs.config import CfgNode as Configuration + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.frontend import English + + +def get_lj_sentences(file_name, frontend): + '''read MFA duration.txt + + Args: + file_name (str or Path) + Returns: + Dict: sentence: {'utt': ([char], [int])} + ''' + f = open(file_name, 'r') + sentence = {} + speaker_set = set() + for line in f: + line_list = line.strip().split('|') + utt = line_list[0] + speaker = utt.split("-")[0][:2] + speaker_set.add(speaker) + raw_text = line_list[-1] + phonemes = frontend.phoneticize(raw_text) + phonemes = phonemes[1:-1] + phonemes = [phn for phn in phonemes if not phn.isspace()] + sentence[utt] = (phonemes, speaker) + f.close() + return sentence, speaker_set + + +def get_input_token(sentence, output_path): + '''get phone set from training data and save it + + Args: + sentence (Dict): sentence: {'utt': ([char], str)} + output_path (str or path): path to save phone_id_map + ''' + phn_token = set() + for utt in sentence: + for phn in sentence[utt][0]: + if phn != "": + phn_token.add(phn) + phn_token = list(phn_token) + phn_token.sort() + phn_token = ["", ""] + phn_token + phn_token += [""] + + with open(output_path, 'w') as f: + for i, phn in enumerate(phn_token): + f.write(phn + ' ' + str(i) + '\n') + + +def get_spk_id_map(speaker_set, output_path): + speakers = sorted(list(speaker_set)) + with open(output_path, 'w') as f: + for i, spk in enumerate(speakers): + f.write(spk + ' ' + str(i) + '\n') + + +def process_sentence(config: Dict[str, Any], + fp: Path, + sentences: Dict, + output_dir: Path, + mel_extractor=None): + utt_id = fp.stem + record = None + if utt_id in sentences: + # reading, resampling may occur + wav, _ = librosa.load(str(fp), sr=config.fs) + if len(wav.shape) != 1 or np.abs(wav).max() > 1.0: + return record + assert len(wav.shape) == 1, f"{utt_id} is not a mono-channel audio." + assert np.abs(wav).max( + ) <= 1.0, f"{utt_id} is seems to be different that 16 bit PCM." + phones = sentences[utt_id][0] + speaker = sentences[utt_id][1] + logmel = mel_extractor.get_log_mel_fbank(wav, base='e') + # change duration according to mel_length + num_frames = logmel.shape[0] + mel_dir = output_dir / "data_speech" + mel_dir.mkdir(parents=True, exist_ok=True) + mel_path = mel_dir / (utt_id + "_speech.npy") + np.save(mel_path, logmel) + record = { + "utt_id": utt_id, + "phones": phones, + "text_lengths": len(phones), + "speech_lengths": num_frames, + "speech": str(mel_path), + "speaker": speaker + } + return record + + +def process_sentences(config, + fps: List[Path], + sentences: Dict, + output_dir: Path, + mel_extractor=None, + nprocs: int=1): + if nprocs == 1: + results = [] + for fp in tqdm.tqdm(fps, total=len(fps)): + record = process_sentence(config, fp, sentences, output_dir, + mel_extractor) + if record: + results.append(record) + else: + with ThreadPoolExecutor(nprocs) as pool: + futures = [] + with tqdm.tqdm(total=len(fps)) as progress: + for fp in fps: + future = pool.submit(process_sentence, config, fp, + sentences, output_dir, mel_extractor) + future.add_done_callback(lambda p: progress.update()) + futures.append(future) + + results = [] + for ft in futures: + record = ft.result() + if record: + results.append(record) + + results.sort(key=itemgetter("utt_id")) + with jsonlines.open(output_dir / "metadata.jsonl", 'w') as writer: + for item in results: + writer.write(item) + print("Done") + + +def main(): + # parse config and args + parser = argparse.ArgumentParser( + description="Preprocess audio and then extract features.") + + parser.add_argument( + "--dataset", + default="ljspeech", + type=str, + help="name of dataset, should in {ljspeech} now") + + parser.add_argument( + "--rootdir", default=None, type=str, help="directory to dataset.") + + parser.add_argument( + "--dumpdir", + type=str, + required=True, + help="directory to dump feature files.") + + parser.add_argument( + "--config-path", + default="conf/default.yaml", + type=str, + help="yaml format configuration file.") + + parser.add_argument( + "--verbose", + type=int, + default=1, + help="logging level. higher is more logging. (default=1)") + parser.add_argument( + "--num-cpu", type=int, default=1, help="number of process.") + + args = parser.parse_args() + + config_path = Path(args.config_path).resolve() + root_dir = Path(args.rootdir).expanduser() + dumpdir = Path(args.dumpdir).expanduser() + # use absolute path + dumpdir = dumpdir.resolve() + dumpdir.mkdir(parents=True, exist_ok=True) + + assert root_dir.is_dir() + + with open(config_path, 'rt') as f: + _C = yaml.safe_load(f) + _C = Configuration(_C) + config = _C.clone() + + if args.verbose > 1: + print(vars(args)) + print(config) + + phone_id_map_path = dumpdir / "phone_id_map.txt" + speaker_id_map_path = dumpdir / "speaker_id_map.txt" + + if args.dataset == "ljspeech": + wav_files = sorted(list((root_dir / "wavs").rglob("*.wav"))) + frontend = English() + sentences, speaker_set = get_lj_sentences(root_dir / "metadata.csv", + frontend) + get_input_token(sentences, phone_id_map_path) + get_spk_id_map(speaker_set, speaker_id_map_path) + # split data into 3 sections + num_train = 12900 + num_dev = 100 + train_wav_files = wav_files[:num_train] + dev_wav_files = wav_files[num_train:num_train + num_dev] + test_wav_files = wav_files[num_train + num_dev:] + + train_dump_dir = dumpdir / "train" / "raw" + train_dump_dir.mkdir(parents=True, exist_ok=True) + dev_dump_dir = dumpdir / "dev" / "raw" + dev_dump_dir.mkdir(parents=True, exist_ok=True) + test_dump_dir = dumpdir / "test" / "raw" + test_dump_dir.mkdir(parents=True, exist_ok=True) + + # Extractor + mel_extractor = LogMelFBank( + sr=config.fs, + n_fft=config.n_fft, + hop_length=config.n_shift, + win_length=config.win_length, + window=config.window, + n_mels=config.n_mels, + fmin=config.fmin, + fmax=config.fmax) + + # process for the 3 sections + if train_wav_files: + process_sentences( + config, + train_wav_files, + sentences, + train_dump_dir, + mel_extractor, + nprocs=args.num_cpu) + if dev_wav_files: + process_sentences( + config, + dev_wav_files, + sentences, + dev_dump_dir, + mel_extractor, + nprocs=args.num_cpu) + if test_wav_files: + process_sentences( + config, + test_wav_files, + sentences, + test_dump_dir, + mel_extractor, + nprocs=args.num_cpu) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize.py new file mode 100644 index 0000000..7b6b187 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize.py @@ -0,0 +1,146 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.transformer_tts import TransformerTTS +from paddlespeech.t2s.models.transformer_tts import TransformerTTSInference +from paddlespeech.t2s.models.waveflow import ConditionalWaveFlow +from paddlespeech.t2s.modules.normalizer import ZScore +from paddlespeech.t2s.utils import layer_tools + + +def evaluate(args, acoustic_model_config, vocoder_config): + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for evaluation + with jsonlines.open(args.test_metadata, 'r') as reader: + test_metadata = list(reader) + test_dataset = DataTable(data=test_metadata, fields=["utt_id", "text"]) + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + odim = acoustic_model_config.n_mels + model = TransformerTTS( + idim=vocab_size, odim=odim, **acoustic_model_config["model"]) + + model.set_state_dict( + paddle.load(args.transformer_tts_checkpoint)["main_params"]) + model.eval() + # remove ".pdparams" in waveflow_checkpoint + vocoder_checkpoint_path = args.waveflow_checkpoint[:-9] if args.waveflow_checkpoint.endswith( + ".pdparams") else args.waveflow_checkpoint + vocoder = ConditionalWaveFlow.from_pretrained(vocoder_config, + vocoder_checkpoint_path) + layer_tools.recursively_remove_weight_norm(vocoder) + vocoder.eval() + print("model done!") + + stat = np.load(args.transformer_tts_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + transformer_tts_normalizer = ZScore(mu, std) + + transformer_tts_inference = TransformerTTSInference( + transformer_tts_normalizer, model) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + for datum in test_dataset: + utt_id = datum["utt_id"] + text = paddle.to_tensor(datum["text"]) + + with paddle.no_grad(): + mel = transformer_tts_inference(text) + # mel shape is (T, feats) and waveflow's input shape is (batch, feats, T) + mel = mel.unsqueeze(0).transpose([0, 2, 1]) + # wavflow's output shape is (B, T) + wav = vocoder.infer(mel)[0] + + sf.write( + str(output_dir / (utt_id + ".wav")), + wav.numpy(), + samplerate=acoustic_model_config.fs) + print(f"{utt_id} done!") + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with transformer tts & waveflow.") + parser.add_argument( + "--transformer-tts-config", + type=str, + help="transformer tts config file.") + parser.add_argument( + "--transformer-tts-checkpoint", + type=str, + help="transformer tts checkpoint to load.") + parser.add_argument( + "--transformer-tts-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training transformer tts." + ) + parser.add_argument( + "--waveflow-config", type=str, help="waveflow config file.") + # not normalize when training waveflow + parser.add_argument( + "--waveflow-checkpoint", type=str, help="waveflow checkpoint to load.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + + parser.add_argument("--test-metadata", type=str, help="test metadata.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.transformer_tts_config) as f: + transformer_tts_config = CfgNode(yaml.safe_load(f)) + with open(args.waveflow_config) as f: + waveflow_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(transformer_tts_config) + print(waveflow_config) + + evaluate(args, transformer_tts_config, waveflow_config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize_e2e.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize_e2e.py new file mode 100644 index 0000000..0cd7d22 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/synthesize_e2e.py @@ -0,0 +1,165 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +from pathlib import Path + +import numpy as np +import paddle +import soundfile as sf +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.frontend import English +from paddlespeech.t2s.models.transformer_tts import TransformerTTS +from paddlespeech.t2s.models.transformer_tts import TransformerTTSInference +from paddlespeech.t2s.models.waveflow import ConditionalWaveFlow +from paddlespeech.t2s.modules.normalizer import ZScore +from paddlespeech.t2s.utils import layer_tools + + +def evaluate(args, acoustic_model_config, vocoder_config): + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for evaluation + sentences = [] + with open(args.text, 'rt') as f: + for line in f: + line_list = line.strip().split() + utt_id = line_list[0] + sentence = " ".join(line_list[1:]) + sentences.append((utt_id, sentence)) + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + + vocab_size = len(phn_id) + phone_id_map = {} + for phn, id in phn_id: + phone_id_map[phn] = int(id) + print("vocab_size:", vocab_size) + odim = acoustic_model_config.n_mels + model = TransformerTTS( + idim=vocab_size, odim=odim, **acoustic_model_config["model"]) + + model.set_state_dict( + paddle.load(args.transformer_tts_checkpoint)["main_params"]) + model.eval() + + # remove ".pdparams" in waveflow_checkpoint + vocoder_checkpoint_path = args.waveflow_checkpoint[:-9] if args.waveflow_checkpoint.endswith( + ".pdparams") else args.waveflow_checkpoint + vocoder = ConditionalWaveFlow.from_pretrained(vocoder_config, + vocoder_checkpoint_path) + layer_tools.recursively_remove_weight_norm(vocoder) + vocoder.eval() + print("model done!") + + frontend = English() + print("frontend done!") + + stat = np.load(args.transformer_tts_stat) + mu, std = stat + mu = paddle.to_tensor(mu) + std = paddle.to_tensor(std) + transformer_tts_normalizer = ZScore(mu, std) + + transformer_tts_inference = TransformerTTSInference( + transformer_tts_normalizer, model) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + for utt_id, sentence in sentences: + phones = frontend.phoneticize(sentence) + # remove start_symbol and end_symbol + phones = phones[1:-1] + phones = [phn for phn in phones if not phn.isspace()] + phones = [phn if phn in phone_id_map else "," for phn in phones] + phone_ids = [phone_id_map[phn] for phn in phones] + with paddle.no_grad(): + mel = transformer_tts_inference(paddle.to_tensor(phone_ids)) + # mel shape is (T, feats) and waveflow's input shape is (batch, feats, T) + mel = mel.unsqueeze(0).transpose([0, 2, 1]) + # wavflow's output shape is (B, T) + wav = vocoder.infer(mel)[0] + + sf.write( + str(output_dir / (utt_id + ".wav")), + wav.numpy(), + samplerate=acoustic_model_config.fs) + print(f"{utt_id} done!") + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with transformer tts & waveflow.") + parser.add_argument( + "--transformer-tts-config", + type=str, + help="transformer tts config file.") + parser.add_argument( + "--transformer-tts-checkpoint", + type=str, + help="transformer tts checkpoint to load.") + parser.add_argument( + "--transformer-tts-stat", + type=str, + help="mean and standard deviation used to normalize spectrogram when training transformer tts." + ) + parser.add_argument( + "--waveflow-config", type=str, help="waveflow config file.") + # not normalize when training waveflow + parser.add_argument( + "--waveflow-checkpoint", type=str, help="waveflow checkpoint to load.") + parser.add_argument( + "--phones-dict", + type=str, + default="phone_id_map.txt", + help="phone vocabulary file.") + parser.add_argument( + "--text", + type=str, + help="text to synthesize, a 'utt_id sentence' pair per line.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + with open(args.transformer_tts_config) as f: + transformer_tts_config = CfgNode(yaml.safe_load(f)) + with open(args.waveflow_config) as f: + waveflow_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(transformer_tts_config) + print(waveflow_config) + + evaluate(args, transformer_tts_config, waveflow_config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/transformer_tts/train.py b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/train.py new file mode 100644 index 0000000..45ecb26 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/transformer_tts/train.py @@ -0,0 +1,193 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.am_batch_fn import transformer_single_spk_batch_fn +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.transformer_tts import TransformerTTS +from paddlespeech.t2s.models.transformer_tts import TransformerTTSEvaluator +from paddlespeech.t2s.models.transformer_tts import TransformerTTSUpdater +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.optimizer import build_optimizers +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + if paddle.is_compiled_with_cuda() and args.ngpu > 0: + paddle.set_device("gpu") + elif paddle.is_compiled_with_npu() and args.ngpu > 0: + paddle.set_device("npu") + else: + paddle.set_device("cpu") + world_size = paddle.distributed.get_world_size() + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=[ + "text", + "text_lengths", + "speech", + "speech_lengths", + ], + converters={ + "speech": np.load, + }, ) + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=[ + "text", + "text_lengths", + "speech", + "speech_lengths", + ], + converters={ + "speech": np.load, + }, ) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + + print("samplers done!") + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=transformer_single_spk_batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + shuffle=False, + drop_last=False, + batch_size=config.batch_size, + collate_fn=transformer_single_spk_batch_fn, + num_workers=config.num_workers) + print("dataloaders done!") + + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + print("vocab_size:", vocab_size) + + odim = config.n_mels + model = TransformerTTS(idim=vocab_size, odim=odim, **config["model"]) + if world_size > 1: + model = DataParallel(model) + print("model done!") + + optimizer = build_optimizers(model, **config["optimizer"]) + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = TransformerTTSUpdater( + model=model, + optimizer=optimizer, + dataloader=train_dataloader, + output_dir=output_dir, + **config["updater"]) + + trainer = Trainer(updater, (config.max_epoch, 'epoch'), output_dir) + + evaluator = TransformerTTSEvaluator( + model, dev_dataloader, output_dir=output_dir, **config["updater"]) + + if dist.get_rank() == 0: + trainer.extend(evaluator, trigger=(1, "epoch")) + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + trainer.extend( + Snapshot(max_size=config.num_snapshots), trigger=(1, 'epoch')) + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Train a TransformerTTS " + "model with LJSpeech TTS dataset.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument( + "--phones-dict", type=str, default=None, help="phone vocabulary file.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/voice_cloning.py b/ernie-sat/paddlespeech/t2s/exps/voice_cloning.py new file mode 100644 index 0000000..1afd21d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/voice_cloning.py @@ -0,0 +1,202 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import numpy as np +import paddle +import soundfile as sf +import yaml +from yacs.config import CfgNode + +from paddlespeech.t2s.exps.syn_utils import get_am_inference +from paddlespeech.t2s.exps.syn_utils import get_voc_inference +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.vector.exps.ge2e.audio_processor import SpeakerVerificationPreprocessor +from paddlespeech.vector.models.lstm_speaker_encoder import LSTMSpeakerEncoder + + +def voice_cloning(args): + # Init body. + with open(args.am_config) as f: + am_config = CfgNode(yaml.safe_load(f)) + with open(args.voc_config) as f: + voc_config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(am_config) + print(voc_config) + + # speaker encoder + p = SpeakerVerificationPreprocessor( + sampling_rate=16000, + audio_norm_target_dBFS=-30, + vad_window_length=30, + vad_moving_average_width=8, + vad_max_silence_length=6, + mel_window_length=25, + mel_window_step=10, + n_mels=40, + partial_n_frames=160, + min_pad_coverage=0.75, + partial_overlap_ratio=0.5) + print("Audio Processor Done!") + + speaker_encoder = LSTMSpeakerEncoder( + n_mels=40, num_layers=3, hidden_size=256, output_size=256) + speaker_encoder.set_state_dict(paddle.load(args.ge2e_params_path)) + speaker_encoder.eval() + print("GE2E Done!") + + frontend = Frontend(phone_vocab_path=args.phones_dict) + print("frontend done!") + + # acoustic model + am_inference, *_ = get_am_inference(args, am_config) + + # vocoder + voc_inference = get_voc_inference(args, voc_config) + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + input_dir = Path(args.input_dir) + + sentence = args.text + + input_ids = frontend.get_input_ids(sentence, merge_sentences=True) + phone_ids = input_ids["phone_ids"][0] + + for name in os.listdir(input_dir): + utt_id = name.split(".")[0] + ref_audio_path = input_dir / name + mel_sequences = p.extract_mel_partials(p.preprocess_wav(ref_audio_path)) + # print("mel_sequences: ", mel_sequences.shape) + with paddle.no_grad(): + spk_emb = speaker_encoder.embed_utterance( + paddle.to_tensor(mel_sequences)) + # print("spk_emb shape: ", spk_emb.shape) + + with paddle.no_grad(): + wav = voc_inference(am_inference(phone_ids, spk_emb=spk_emb)) + + sf.write( + str(output_dir / (utt_id + ".wav")), + wav.numpy(), + samplerate=am_config.fs) + print(f"{utt_id} done!") + # Randomly generate numbers of 0 ~ 0.2, 256 is the dim of spk_emb + random_spk_emb = np.random.rand(256) * 0.2 + random_spk_emb = paddle.to_tensor(random_spk_emb) + utt_id = "random_spk_emb" + with paddle.no_grad(): + wav = voc_inference(am_inference(phone_ids, spk_emb=spk_emb)) + sf.write( + str(output_dir / (utt_id + ".wav")), + wav.numpy(), + samplerate=am_config.fs) + print(f"{utt_id} done!") + + +def parse_args(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="") + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=['fastspeech2_aishell3', 'tacotron2_aishell3'], + help='Choose acoustic model type of tts task.') + parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + parser.add_argument( + "--phones-dict", + type=str, + default="phone_id_map.txt", + help="phone vocabulary file.") + # vocoder + parser.add_argument( + '--voc', + type=str, + default='pwgan_csmsc', + choices=['pwgan_aishell3'], + help='Choose vocoder type of tts task.') + + parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + parser.add_argument( + '--voc_ckpt', type=str, default=None, help='Checkpoint file of voc.') + parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + parser.add_argument( + "--text", + type=str, + default="每当你觉得,想要批评什么人的时候,你切要记着,这个世界上的人,并非都具备你禀有的条件。", + help="text to synthesize, a line") + + parser.add_argument( + "--ge2e_params_path", type=str, help="ge2e params path.") + + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + + parser.add_argument( + "--input-dir", + type=str, + help="input dir of *.wav, the sample rate will be resample to 16k.") + parser.add_argument("--output-dir", type=str, help="output dir.") + + args = parser.parse_args() + return args + + +def main(): + args = parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + voice_cloning(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/__init__.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/config.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/config.py new file mode 100644 index 0000000..869caa6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/config.py @@ -0,0 +1,55 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from yacs.config import CfgNode as CN + +_C = CN() +_C.data = CN( + dict( + batch_size=8, # batch size + valid_size=16, # the first N examples are reserved for validation + sample_rate=22050, # Hz, sample rate + n_fft=1024, # fft frame size + win_length=1024, # window size + hop_length=256, # hop size between ajacent frame + fmin=0, + fmax=8000, # Hz, max frequency when converting to mel + n_mels=80, # mel bands + clip_frames=65, # mel clip frames + )) + +_C.model = CN( + dict( + upsample_factors=[16, 16], + n_flows=8, # number of flows in WaveFlow + n_layers=8, # number of conv block in each flow + n_group=16, # folding factor of audio and spectrogram + channels=128, # resiaudal channel in each flow + kernel_size=[3, 3], # kernel size in each conv block + sigma=1.0, # stddev of the random noise + )) + +_C.training = CN( + dict( + lr=2e-4, # learning rates + valid_interval=1000, # validation + save_interval=10000, # checkpoint + max_iteration=3000000, # max iteration to train + )) + + +def get_cfg_defaults(): + """Get a yacs CfgNode object with default values for my_project.""" + # Return a clone so that the defaults will not be altered + # This is for the "local variable" use pattern + return _C.clone() diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/ljspeech.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/ljspeech.py new file mode 100644 index 0000000..a6efa9e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/ljspeech.py @@ -0,0 +1,89 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from pathlib import Path + +import numpy as np +import pandas +from paddle.io import Dataset + +from paddlespeech.t2s.datasets.batch import batch_spec +from paddlespeech.t2s.datasets.batch import batch_wav + + +class LJSpeech(Dataset): + """A simple dataset adaptor for the processed ljspeech dataset.""" + + def __init__(self, root): + self.root = Path(root).expanduser() + meta_data = pandas.read_csv( + str(self.root / "metadata.csv"), + sep="\t", + header=None, + names=["fname", "frames", "samples"]) + + records = [] + for row in meta_data.itertuples(): + mel_path = str(self.root / "mel" / (row.fname + ".npy")) + wav_path = str(self.root / "wav" / (row.fname + ".npy")) + records.append((mel_path, wav_path)) + self.records = records + + def __getitem__(self, i): + mel_name, wav_name = self.records[i] + mel = np.load(mel_name) + wav = np.load(wav_name) + return mel, wav + + def __len__(self): + return len(self.records) + + +class LJSpeechCollector(object): + """A simple callable to batch LJSpeech examples.""" + + def __init__(self, padding_value=0.): + self.padding_value = padding_value + + def __call__(self, examples): + mels = [example[0] for example in examples] + wavs = [example[1] for example in examples] + mels, _ = batch_spec(mels, pad_value=self.padding_value) + wavs, _ = batch_wav(wavs, pad_value=self.padding_value) + return mels, wavs + + +class LJSpeechClipCollector(object): + def __init__(self, clip_frames=65, hop_length=256): + self.clip_frames = clip_frames + self.hop_length = hop_length + + def __call__(self, examples): + mels = [] + wavs = [] + for example in examples: + mel_clip, wav_clip = self.clip(example) + mels.append(mel_clip) + wavs.append(wav_clip) + mels = np.stack(mels) + wavs = np.stack(wavs) + return mels, wavs + + def clip(self, example): + mel, wav = example + frames = mel.shape[-1] + start = np.random.randint(0, frames - self.clip_frames) + mel_clip = mel[:, start:start + self.clip_frames] + wav_clip = wav[start * self.hop_length:(start + self.clip_frames) * + self.hop_length] + return mel_clip, wav_clip diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/preprocess.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/preprocess.py new file mode 100644 index 0000000..ef3a291 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/preprocess.py @@ -0,0 +1,160 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import librosa +import numpy as np +import pandas as pd +import tqdm + +from paddlespeech.t2s.audio import LogMagnitude +from paddlespeech.t2s.datasets import LJSpeechMetaData +from paddlespeech.t2s.exps.waveflow.config import get_cfg_defaults + + +class Transform(object): + def __init__(self, sample_rate, n_fft, win_length, hop_length, n_mels, fmin, + fmax): + self.sample_rate = sample_rate + self.n_fft = n_fft + self.win_length = win_length + self.hop_length = hop_length + self.n_mels = n_mels + self.fmin = fmin + self.fmax = fmax + + self.spec_normalizer = LogMagnitude(min=1e-5) + + def __call__(self, example): + wav_path, _, _ = example + + sr = self.sample_rate + n_fft = self.n_fft + win_length = self.win_length + hop_length = self.hop_length + n_mels = self.n_mels + fmin = self.fmin + fmax = self.fmax + + wav, loaded_sr = librosa.load(wav_path, sr=None) + assert loaded_sr == sr, "sample rate does not match, resampling applied" + + # Pad audio to the right size. + frames = int(np.ceil(float(wav.size) / hop_length)) + fft_padding = (n_fft - hop_length) // 2 # sound + desired_length = frames * hop_length + fft_padding * 2 + pad_amount = (desired_length - wav.size) // 2 + + if wav.size % 2 == 0: + wav = np.pad(wav, (pad_amount, pad_amount), mode='reflect') + else: + wav = np.pad(wav, (pad_amount, pad_amount + 1), mode='reflect') + + # Normalize audio. + wav = wav / np.abs(wav).max() * 0.999 + + # Compute mel-spectrogram. + # Turn center to False to prevent internal padding. + spectrogram = librosa.core.stft( + wav, + hop_length=hop_length, + win_length=win_length, + n_fft=n_fft, + center=False) + spectrogram_magnitude = np.abs(spectrogram) + + # Compute mel-spectrograms. + mel_filter_bank = librosa.filters.mel( + sr=sr, n_fft=n_fft, n_mels=n_mels, fmin=fmin, fmax=fmax) + mel_spectrogram = np.dot(mel_filter_bank, spectrogram_magnitude) + + # log scale mel_spectrogram. + mel_spectrogram = self.spec_normalizer.transform(mel_spectrogram) + + # Extract the center of audio that corresponds to mel spectrograms. + audio = wav[fft_padding:-fft_padding] + assert mel_spectrogram.shape[1] * hop_length == audio.size + + # there is no clipping here + return audio, mel_spectrogram + + +def create_dataset(config, input_dir, output_dir): + input_dir = Path(input_dir).expanduser() + dataset = LJSpeechMetaData(input_dir) + + output_dir = Path(output_dir).expanduser() + output_dir.mkdir(exist_ok=True) + + transform = Transform(config.sample_rate, config.n_fft, config.win_length, + config.hop_length, config.n_mels, config.fmin, + config.fmax) + file_names = [] + + for example in tqdm.tqdm(dataset): + fname, _, _ = example + base_name = os.path.splitext(os.path.basename(fname))[0] + wav_dir = output_dir / "wav" + mel_dir = output_dir / "mel" + wav_dir.mkdir(exist_ok=True) + mel_dir.mkdir(exist_ok=True) + + audio, mel = transform(example) + np.save(str(wav_dir / base_name), audio) + np.save(str(mel_dir / base_name), mel) + + file_names.append((base_name, mel.shape[-1], audio.shape[-1])) + + meta_data = pd.DataFrame.from_records(file_names) + meta_data.to_csv( + str(output_dir / "metadata.csv"), sep="\t", index=None, header=None) + print("saved meta data in to {}".format( + os.path.join(output_dir, "metadata.csv"))) + + print("Done!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="create dataset") + parser.add_argument( + "--config", + type=str, + metavar="FILE", + help="extra config to overwrite the default config") + parser.add_argument( + "--input", type=str, help="path of the ljspeech dataset") + parser.add_argument( + "--output", type=str, help="path to save output dataset") + parser.add_argument( + "--opts", + nargs=argparse.REMAINDER, + help="options to overwrite --config file and the default config, passing in KEY VALUE pairs" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print msg") + + config = get_cfg_defaults() + args = parser.parse_args() + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + if args.verbose: + print(config.data) + print(args) + + create_dataset(config.data, args.input, args.output) diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/synthesize.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/synthesize.py new file mode 100644 index 0000000..53715b0 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/synthesize.py @@ -0,0 +1,87 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import numpy as np +import paddle +import soundfile as sf + +from paddlespeech.t2s.exps.waveflow.config import get_cfg_defaults +from paddlespeech.t2s.models.waveflow import ConditionalWaveFlow +from paddlespeech.t2s.utils import layer_tools + + +def main(config, args): + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + model = ConditionalWaveFlow.from_pretrained(config, args.checkpoint_path) + layer_tools.recursively_remove_weight_norm(model) + model.eval() + + mel_dir = Path(args.input).expanduser() + output_dir = Path(args.output).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + for file_path in mel_dir.glob("*.npy"): + mel = np.load(str(file_path)) + with paddle.amp.auto_cast(): + audio = model.predict(mel) + audio_path = output_dir / (os.path.splitext(file_path.name)[0] + ".wav") + sf.write(audio_path, audio, config.data.sample_rate) + print("[synthesize] {} -> {}".format(file_path, audio_path)) + + +if __name__ == "__main__": + config = get_cfg_defaults() + + parser = argparse.ArgumentParser( + description="generate mel spectrogram with TransformerTTS.") + parser.add_argument( + "--config", + type=str, + metavar="FILE", + help="extra config to overwrite the default config") + parser.add_argument( + "--checkpoint_path", type=str, help="path of the checkpoint to load.") + parser.add_argument( + "--input", + type=str, + help="path of directory containing mel spectrogram (in .npy format)") + parser.add_argument("--output", type=str, help="path to save outputs") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + parser.add_argument( + "--opts", + nargs=argparse.REMAINDER, + help="options to overwrite --config file and the default config, passing in KEY VALUE pairs" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print msg") + + args = parser.parse_args() + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + print(args) + + main(config, args) diff --git a/ernie-sat/paddlespeech/t2s/exps/waveflow/train.py b/ernie-sat/paddlespeech/t2s/exps/waveflow/train.py new file mode 100644 index 0000000..cf03f5e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/waveflow/train.py @@ -0,0 +1,160 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import time + +import numpy as np +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler + +from paddlespeech.t2s.datasets import dataset +from paddlespeech.t2s.exps.waveflow.config import get_cfg_defaults +from paddlespeech.t2s.exps.waveflow.ljspeech import LJSpeech +from paddlespeech.t2s.exps.waveflow.ljspeech import LJSpeechClipCollector +from paddlespeech.t2s.exps.waveflow.ljspeech import LJSpeechCollector +from paddlespeech.t2s.models.waveflow import ConditionalWaveFlow +from paddlespeech.t2s.models.waveflow import WaveFlowLoss +from paddlespeech.t2s.training.cli import default_argument_parser +from paddlespeech.t2s.training.experiment import ExperimentBase +from paddlespeech.t2s.utils import mp_tools + + +class Experiment(ExperimentBase): + def setup_model(self): + config = self.config + model = ConditionalWaveFlow( + upsample_factors=config.model.upsample_factors, + n_flows=config.model.n_flows, + n_layers=config.model.n_layers, + n_group=config.model.n_group, + channels=config.model.channels, + n_mels=config.data.n_mels, + kernel_size=config.model.kernel_size) + + if self.parallel: + model = paddle.DataParallel(model) + optimizer = paddle.optimizer.Adam( + config.training.lr, parameters=model.parameters()) + criterion = WaveFlowLoss(sigma=config.model.sigma) + + self.model = model + self.optimizer = optimizer + self.criterion = criterion + + def setup_dataloader(self): + config = self.config + args = self.args + + ljspeech_dataset = LJSpeech(args.data) + valid_set, train_set = dataset.split(ljspeech_dataset, + config.data.valid_size) + + batch_fn = LJSpeechClipCollector(config.data.clip_frames, + config.data.hop_length) + + if not self.parallel: + train_loader = DataLoader( + train_set, + batch_size=config.data.batch_size, + shuffle=True, + drop_last=True, + collate_fn=batch_fn) + else: + sampler = DistributedBatchSampler( + train_set, + batch_size=config.data.batch_size, + num_replicas=dist.get_world_size(), + rank=dist.get_rank(), + shuffle=True, + drop_last=True) + train_loader = DataLoader( + train_set, batch_sampler=sampler, collate_fn=batch_fn) + + valid_batch_fn = LJSpeechCollector() + valid_loader = DataLoader( + valid_set, batch_size=1, collate_fn=valid_batch_fn) + + self.train_loader = train_loader + self.valid_loader = valid_loader + + def compute_outputs(self, mel, wav): + # model_core = model._layers if isinstance(model, paddle.DataParallel) else model + z, log_det_jocobian = self.model(wav, mel) + return z, log_det_jocobian + + def train_batch(self): + start = time.time() + batch = self.read_batch() + data_loader_time = time.time() - start + + self.model.train() + self.optimizer.clear_grad() + mel, wav = batch + z, log_det_jocobian = self.compute_outputs(mel, wav) + loss = self.criterion(z, log_det_jocobian) + loss.backward() + self.optimizer.step() + iteration_time = time.time() - start + + loss_value = float(loss) + msg = "Rank: {}, ".format(dist.get_rank()) + msg += "step: {}, ".format(self.iteration) + msg += "time: {:>.3f}s/{:>.3f}s, ".format(data_loader_time, + iteration_time) + msg += "loss: {:>.6f}".format(loss_value) + self.logger.info(msg) + if dist.get_rank() == 0: + self.visualizer.add_scalar("train/loss", loss_value, self.iteration) + + @mp_tools.rank_zero_only + @paddle.no_grad() + def valid(self): + valid_iterator = iter(self.valid_loader) + valid_losses = [] + mel, wav = next(valid_iterator) + z, log_det_jocobian = self.compute_outputs(mel, wav) + loss = self.criterion(z, log_det_jocobian) + valid_losses.append(float(loss)) + valid_loss = np.mean(valid_losses) + self.visualizer.add_scalar("valid/loss", valid_loss, self.iteration) + + +def main_sp(config, args): + exp = Experiment(config, args) + exp.setup() + exp.resume_or_load() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + config = get_cfg_defaults() + parser = default_argument_parser() + args = parser.parse_args() + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + print(args) + + main(config, args) diff --git a/ernie-sat/paddlespeech/t2s/exps/wavernn/__init__.py b/ernie-sat/paddlespeech/t2s/exps/wavernn/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/wavernn/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/exps/wavernn/synthesize.py b/ernie-sat/paddlespeech/t2s/exps/wavernn/synthesize.py new file mode 100644 index 0000000..d23e9cb --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/wavernn/synthesize.py @@ -0,0 +1,108 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from paddle import distributed as dist +from timer import timer +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.models.wavernn import WaveRNN + + +def main(): + parser = argparse.ArgumentParser(description="Synthesize with WaveRNN.") + + parser.add_argument("--config", type=str, help="Vocoder config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument("--test-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + model = WaveRNN( + hop_length=config.n_shift, sample_rate=config.fs, **config["model"]) + state_dict = paddle.load(args.checkpoint) + model.set_state_dict(state_dict["main_params"]) + + model.eval() + + with jsonlines.open(args.test_metadata, 'r') as reader: + metadata = list(reader) + test_dataset = DataTable( + metadata, + fields=['utt_id', 'feats'], + converters={ + 'utt_id': None, + 'feats': np.load, + }) + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + N = 0 + T = 0 + for example in test_dataset: + utt_id = example['utt_id'] + mel = example['feats'] + mel = paddle.to_tensor(mel) # (T, C) + with timer() as t: + with paddle.no_grad(): + wav = model.generate( + c=mel, + batched=config.inference.gen_batched, + target=config.inference.target, + overlap=config.inference.overlap, + mu_law=config.mu_law, + gen_display=False) + wav = wav.numpy() + N += wav.size + T += t.elapse + speed = wav.size / t.elapse + rtf = config.fs / speed + print( + f"{utt_id}, mel: {mel.shape}, wave: {wav.shape}, time: {t.elapse}s, Hz: {speed}, RTF: {rtf}." + ) + sf.write(str(output_dir / (utt_id + ".wav")), wav, samplerate=config.fs) + print(f"generation speed: {N / T}Hz, RTF: {config.fs / (N / T) }") + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/exps/wavernn/train.py b/ernie-sat/paddlespeech/t2s/exps/wavernn/train.py new file mode 100644 index 0000000..8661d31 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/exps/wavernn/train.py @@ -0,0 +1,212 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import shutil +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.optimizer import Adam +from yacs.config import CfgNode + +from paddlespeech.t2s.datasets.data_table import DataTable +from paddlespeech.t2s.datasets.vocoder_batch_fn import WaveRNNClip +from paddlespeech.t2s.models.wavernn import WaveRNN +from paddlespeech.t2s.models.wavernn import WaveRNNEvaluator +from paddlespeech.t2s.models.wavernn import WaveRNNUpdater +from paddlespeech.t2s.modules.losses import discretized_mix_logistic_loss +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + world_size = paddle.distributed.get_world_size() + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + + # construct dataset for training and validation + with jsonlines.open(args.train_metadata, 'r') as reader: + train_metadata = list(reader) + train_dataset = DataTable( + data=train_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + with jsonlines.open(args.dev_metadata, 'r') as reader: + dev_metadata = list(reader) + dev_dataset = DataTable( + data=dev_metadata, + fields=["wave", "feats"], + converters={ + "wave": np.load, + "feats": np.load, + }, ) + + batch_fn = WaveRNNClip( + mode=config.model.mode, + aux_context_window=config.model.aux_context_window, + hop_size=config.n_shift, + batch_max_steps=config.batch_max_steps, + bits=config.model.bits) + + # collate function and dataloader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=True) + dev_sampler = DistributedBatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + print("samplers done!") + + train_dataloader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + collate_fn=batch_fn, + num_workers=config.num_workers) + + dev_dataloader = DataLoader( + dev_dataset, + collate_fn=batch_fn, + batch_sampler=dev_sampler, + num_workers=config.num_workers) + + valid_generate_loader = DataLoader(dev_dataset, batch_size=1) + + print("dataloaders done!") + + model = WaveRNN( + hop_length=config.n_shift, sample_rate=config.fs, **config["model"]) + if world_size > 1: + model = DataParallel(model) + print("model done!") + + if config.model.mode == 'RAW': + criterion = paddle.nn.CrossEntropyLoss(axis=1) + elif config.model.mode == 'MOL': + criterion = discretized_mix_logistic_loss + else: + criterion = None + RuntimeError('Unknown model mode value - ', config.model.mode) + print("criterions done!") + clip = paddle.nn.ClipGradByGlobalNorm(config.grad_clip) + optimizer = Adam( + parameters=model.parameters(), + learning_rate=config.learning_rate, + grad_clip=clip) + + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = WaveRNNUpdater( + model=model, + optimizer=optimizer, + criterion=criterion, + dataloader=train_dataloader, + output_dir=output_dir, + mode=config.model.mode) + + evaluator = WaveRNNEvaluator( + model=model, + dataloader=dev_dataloader, + criterion=criterion, + output_dir=output_dir, + valid_generate_loader=valid_generate_loader, + config=config) + + trainer = Trainer( + updater, + stop_trigger=(config.train_max_steps, "iteration"), + out=output_dir) + + if dist.get_rank() == 0: + trainer.extend( + evaluator, trigger=(config.eval_interval_steps, 'iteration')) + trainer.extend(VisualDL(output_dir), trigger=(1, 'iteration')) + trainer.extend( + Snapshot(max_size=config.num_snapshots), + trigger=(config.save_interval_steps, 'iteration')) + + print("Trainer Done!") + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + + parser = argparse.ArgumentParser(description="Train a WaveRNN model.") + parser.add_argument( + "--config", type=str, help="config file to overwrite default config.") + parser.add_argument("--train-metadata", type=str, help="training data.") + parser.add_argument("--dev-metadata", type=str, help="dev data.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + args = parser.parse_args() + + with open(args.config, 'rt') as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/t2s/frontend/__init__.py b/ernie-sat/paddlespeech/t2s/frontend/__init__.py new file mode 100644 index 0000000..6401543 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .generate_lexicon import * +from .normalizer import * +from .phonectic import * +from .punctuation import * +from .tone_sandhi import * +from .vocab import * +from .zh_normalization import * diff --git a/ernie-sat/paddlespeech/t2s/frontend/arpabet.py b/ernie-sat/paddlespeech/t2s/frontend/arpabet.py new file mode 100644 index 0000000..7a81b64 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/arpabet.py @@ -0,0 +1,268 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.t2s.frontend.phonectic import Phonetics +""" +A phonology system with ARPABET symbols and limited punctuations. The G2P +conversion is done by g2p_en. + +Note that g2p_en does not handle words with hypen well. So make sure the input +sentence is first normalized. +""" +from paddlespeech.t2s.frontend.vocab import Vocab +from g2p_en import G2p + + +class ARPABET(Phonetics): + """A phonology for English that uses ARPABET as the phoneme vocabulary. + See http://www.speech.cs.cmu.edu/cgi-bin/cmudict for more details. + Phoneme Example Translation + ------- ------- ----------- + AA odd AA D + AE at AE T + AH hut HH AH T + AO ought AO T + AW cow K AW + AY hide HH AY D + B be B IY + CH cheese CH IY Z + D dee D IY + DH thee DH IY + EH Ed EH D + ER hurt HH ER T + EY ate EY T + F fee F IY + G green G R IY N + HH he HH IY + IH it IH T + IY eat IY T + JH gee JH IY + K key K IY + L lee L IY + M me M IY + N knee N IY + NG ping P IH NG + OW oat OW T + OY toy T OY + P pee P IY + R read R IY D + S sea S IY + SH she SH IY + T tea T IY + TH theta TH EY T AH + UH hood HH UH D + UW two T UW + V vee V IY + W we W IY + Y yield Y IY L D + Z zee Z IY + ZH seizure S IY ZH ER + """ + phonemes = [ + 'AA', 'AE', 'AH', 'AO', 'AW', 'AY', 'B', 'CH', 'D', 'DH', 'EH', 'ER', + 'EY', 'F', 'G', 'HH', 'IH', 'IY', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW', + 'OY', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UW', 'UH', 'V', 'W', 'Y', 'Z', + 'ZH' + ] + punctuations = [',', '.', '?', '!'] + symbols = phonemes + punctuations + _stress_to_no_stress_ = { + 'AA0': 'AA', + 'AA1': 'AA', + 'AA2': 'AA', + 'AE0': 'AE', + 'AE1': 'AE', + 'AE2': 'AE', + 'AH0': 'AH', + 'AH1': 'AH', + 'AH2': 'AH', + 'AO0': 'AO', + 'AO1': 'AO', + 'AO2': 'AO', + 'AW0': 'AW', + 'AW1': 'AW', + 'AW2': 'AW', + 'AY0': 'AY', + 'AY1': 'AY', + 'AY2': 'AY', + 'EH0': 'EH', + 'EH1': 'EH', + 'EH2': 'EH', + 'ER0': 'ER', + 'ER1': 'ER', + 'ER2': 'ER', + 'EY0': 'EY', + 'EY1': 'EY', + 'EY2': 'EY', + 'IH0': 'IH', + 'IH1': 'IH', + 'IH2': 'IH', + 'IY0': 'IY', + 'IY1': 'IY', + 'IY2': 'IY', + 'OW0': 'OW', + 'OW1': 'OW', + 'OW2': 'OW', + 'OY0': 'OY', + 'OY1': 'OY', + 'OY2': 'OY', + 'UH0': 'UH', + 'UH1': 'UH', + 'UH2': 'UH', + 'UW0': 'UW', + 'UW1': 'UW', + 'UW2': 'UW' + } + + def __init__(self): + self.backend = G2p() + self.vocab = Vocab(self.phonemes + self.punctuations) + + def _remove_vowels(self, phone): + return self._stress_to_no_stress_.get(phone, phone) + + def phoneticize(self, sentence, add_start_end=False): + """ Normalize the input text sequence and convert it into pronunciation sequence. + Args: + sentence (str): The input text sequence. + + Returns: + List[str]: The list of pronunciation sequence. + """ + phonemes = [ + self._remove_vowels(item) for item in self.backend(sentence) + ] + if add_start_end: + start = self.vocab.start_symbol + end = self.vocab.end_symbol + phonemes = [start] + phonemes + [end] + phonemes = [item for item in phonemes if item in self.vocab.stoi] + return phonemes + + def numericalize(self, phonemes): + """ Convert pronunciation sequence into pronunciation id sequence. + + Args: + phonemes (List[str]): The list of pronunciation sequence. + + Returns: + List[int]: The list of pronunciation id sequence. + """ + ids = [self.vocab.lookup(item) for item in phonemes] + return ids + + def reverse(self, ids): + """ Reverse the list of pronunciation id sequence to a list of pronunciation sequence. + + Args: + ids( List[int]): The list of pronunciation id sequence. + + Returns: + List[str]: + The list of pronunciation sequence. + """ + return [self.vocab.reverse(i) for i in ids] + + def __call__(self, sentence, add_start_end=False): + """ Convert the input text sequence into pronunciation id sequence. + + Args: + sentence (str): The input text sequence. + + Returns: + List[str]: The list of pronunciation id sequence. + """ + return self.numericalize( + self.phoneticize(sentence, add_start_end=add_start_end)) + + @property + def vocab_size(self): + """ Vocab size. + """ + # 47 = 39 phones + 4 punctuations + 4 special tokens + return len(self.vocab) + + +class ARPABETWithStress(Phonetics): + phonemes = [ + 'AA0', 'AA1', 'AA2', 'AE0', 'AE1', 'AE2', 'AH0', 'AH1', 'AH2', 'AO0', + 'AO1', 'AO2', 'AW0', 'AW1', 'AW2', 'AY0', 'AY1', 'AY2', 'B', 'CH', 'D', + 'DH', 'EH0', 'EH1', 'EH2', 'ER0', 'ER1', 'ER2', 'EY0', 'EY1', 'EY2', + 'F', 'G', 'HH', 'IH0', 'IH1', 'IH2', 'IY0', 'IY1', 'IY2', 'JH', 'K', + 'L', 'M', 'N', 'NG', 'OW0', 'OW1', 'OW2', 'OY0', 'OY1', 'OY2', 'P', 'R', + 'S', 'SH', 'T', 'TH', 'UH0', 'UH1', 'UH2', 'UW0', 'UW1', 'UW2', 'V', + 'W', 'Y', 'Z', 'ZH' + ] + punctuations = [',', '.', '?', '!'] + symbols = phonemes + punctuations + + def __init__(self): + self.backend = G2p() + self.vocab = Vocab(self.phonemes + self.punctuations) + + def phoneticize(self, sentence, add_start_end=False): + """ Normalize the input text sequence and convert it into pronunciation sequence. + + Args: + sentence (str): The input text sequence. + + Returns: + List[str]: The list of pronunciation sequence. + """ + phonemes = self.backend(sentence) + if add_start_end: + start = self.vocab.start_symbol + end = self.vocab.end_symbol + phonemes = [start] + phonemes + [end] + phonemes = [item for item in phonemes if item in self.vocab.stoi] + return phonemes + + def numericalize(self, phonemes): + """ Convert pronunciation sequence into pronunciation id sequence. + + Args: + phonemes (List[str]): The list of pronunciation sequence. + + Returns: + List[int]: The list of pronunciation id sequence. + """ + ids = [self.vocab.lookup(item) for item in phonemes] + return ids + + def reverse(self, ids): + """ Reverse the list of pronunciation id sequence to a list of pronunciation sequence. + Args: + ids (List[int]): The list of pronunciation id sequence. + + Returns: + List[str]: The list of pronunciation sequence. + """ + return [self.vocab.reverse(i) for i in ids] + + def __call__(self, sentence, add_start_end=False): + """ Convert the input text sequence into pronunciation id sequence. + Args: + sentence (str): The input text sequence. + + Returns: + List[str]: The list of pronunciation id sequence. + """ + return self.numericalize( + self.phoneticize(sentence, add_start_end=add_start_end)) + + @property + def vocab_size(self): + """ Vocab size. + """ + # 77 = 69 phones + 4 punctuations + 4 special tokens + return len(self.vocab) diff --git a/ernie-sat/paddlespeech/t2s/frontend/generate_lexicon.py b/ernie-sat/paddlespeech/t2s/frontend/generate_lexicon.py new file mode 100644 index 0000000..6b467d0 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/generate_lexicon.py @@ -0,0 +1,158 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Design principles: https://zhuanlan.zhihu.com/p/349600439 +"""Generate lexicon and symbols for Mandarin Chinese phonology. +The lexicon is used for Montreal Force Aligner. +Note that syllables are used as word in this lexicon. Since syllables rather +than words are used in transcriptions produced by `reorganize_baker.py`. +We make this choice to better leverage other software for chinese text to +pinyin tools like pypinyin. This is the convention for G2P in Chinese. +""" +import re +from collections import OrderedDict + +INITIALS = [ + 'b', 'p', 'm', 'f', 'd', 't', 'n', 'l', 'g', 'k', 'h', 'zh', 'ch', 'sh', + 'r', 'z', 'c', 's', 'j', 'q', 'x' +] + +FINALS = [ + 'a', 'ai', 'ao', 'an', 'ang', 'e', 'er', 'ei', 'en', 'eng', 'o', 'ou', + 'ong', 'ii', 'iii', 'i', 'ia', 'iao', 'ian', 'iang', 'ie', 'io', 'iou', + 'iong', 'in', 'ing', 'u', 'ua', 'uai', 'uan', 'uang', 'uei', 'uo', 'uen', + 'ueng', 'v', 've', 'van', 'vn' +] + +SPECIALS = ['sil', 'sp'] + + +def rule(C, V, R, T): + """Generate a syllable given the initial, the final, erhua indicator, and tone. + Orthographical rules for pinyin are applied. (special case for y, w, ui, un, iu) + + Note that in this system, 'ü' is alway written as 'v' when appeared in phoneme, but converted to + 'u' in syllables when certain conditions are satisfied. + + 'i' is distinguished when appeared in phonemes, and separated into 3 categories, 'i', 'ii' and 'iii'. + Erhua is is possibly applied to every finals, except for finals that already ends with 'r'. + When a syllable is impossible or does not have any characters with this pronunciation, return None + to filter it out. + """ + + # 不可拼的音节, ii 只能和 z, c, s 拼 + if V in ["ii"] and (C not in ['z', 'c', 's']): + return None + # iii 只能和 zh, ch, sh, r 拼 + if V in ['iii'] and (C not in ['zh', 'ch', 'sh', 'r']): + return None + + # 齐齿呼或者撮口呼不能和 f, g, k, h, zh, ch, sh, r, z, c, s + if (V not in ['ii', 'iii']) and V[0] in ['i', 'v'] and ( + C in ['f', 'g', 'k', 'h', 'zh', 'ch', 'sh', 'r', 'z', 'c', 's']): + return None + + # 撮口呼只能和 j, q, x l, n 拼 + if V.startswith("v"): + # v, ve 只能和 j ,q , x, n, l 拼 + if V in ['v', 've']: + if C not in ['j', 'q', 'x', 'n', 'l', '']: + return None + # 其他只能和 j, q, x 拼 + else: + if C not in ['j', 'q', 'x', '']: + return None + + # j, q, x 只能和齐齿呼或者撮口呼拼 + if (C in ['j', 'q', 'x']) and not ( + (V not in ['ii', 'iii']) and V[0] in ['i', 'v']): + return None + + # b, p ,m, f 不能和合口呼拼,除了 u 之外 + # bm p, m, f 不能和撮口呼拼 + if (C in ['b', 'p', 'm', 'f']) and ((V[0] in ['u', 'v'] and V != "u") or + V == 'ong'): + return None + + # ua, uai, uang 不能和 d, t, n, l, r, z, c, s 拼 + if V in ['ua', 'uai', + 'uang'] and C in ['d', 't', 'n', 'l', 'r', 'z', 'c', 's']: + return None + + # sh 和 ong 不能拼 + if V == 'ong' and C in ['sh']: + return None + + # o 和 gkh, zh ch sh r z c s 不能拼 + if V == "o" and C in [ + 'd', 't', 'n', 'g', 'k', 'h', 'zh', 'ch', 'sh', 'r', 'z', 'c', 's' + ]: + return None + + # ueng 只是 weng 这个 ad-hoc 其他情况下都是 ong + if V == 'ueng' and C != '': + return + + # 非儿化的 er 只能单独存在 + if V == 'er' and C != '': + return None + + if C == '': + if V in ["i", "in", "ing"]: + C = 'y' + elif V == 'u': + C = 'w' + elif V.startswith('i') and V not in ["ii", "iii"]: + C = 'y' + V = V[1:] + elif V.startswith('u'): + C = 'w' + V = V[1:] + elif V.startswith('v'): + C = 'yu' + V = V[1:] + else: + if C in ['j', 'q', 'x']: + if V.startswith('v'): + V = re.sub('v', 'u', V) + if V == 'iou': + V = 'iu' + elif V == 'uei': + V = 'ui' + elif V == 'uen': + V = 'un' + result = C + V + + # Filter er 不能再儿化 + if result.endswith('r') and R == 'r': + return None + + # ii and iii, change back to i + result = re.sub(r'i+', 'i', result) + + result = result + R + T + return result + + +def generate_lexicon(with_tone=False, with_erhua=False): + """Generate lexicon for Mandarin Chinese.""" + syllables = OrderedDict() + + for C in [''] + INITIALS: + for V in FINALS: + for R in [''] if not with_erhua else ['', 'r']: + for T in [''] if not with_tone else ['1', '2', '3', '4', '5']: + result = rule(C, V, R, T) + if result: + syllables[result] = f'{C} {V}{R}{T}' + return syllables diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/__init__.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/__init__.py new file mode 100644 index 0000000..a03329f --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.t2s.frontend.normalizer.normalizer import * +from paddlespeech.t2s.frontend.normalizer.numbers import * diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/abbrrviation.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/abbrrviation.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/abbrrviation.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/acronyms.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/acronyms.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/acronyms.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/normalizer.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/normalizer.py new file mode 100644 index 0000000..421ebd1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/normalizer.py @@ -0,0 +1,34 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +import unicodedata +from builtins import str as unicode + +from paddlespeech.t2s.frontend.normalizer.numbers import normalize_numbers + + +def normalize(sentence): + """ Normalize English text. + """ + # preprocessing + sentence = unicode(sentence) + sentence = normalize_numbers(sentence) + sentence = ''.join( + char for char in unicodedata.normalize('NFD', sentence) + if unicodedata.category(char) != 'Mn') # Strip accents + sentence = sentence.lower() + sentence = re.sub(r"[^ a-z'.,?!\-]", "", sentence) + sentence = sentence.replace("i.e.", "that is") + sentence = sentence.replace("e.g.", "for example") + return sentence diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/numbers.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/numbers.py new file mode 100644 index 0000000..564fb9b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/numbers.py @@ -0,0 +1,86 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# number expansion is not that easy +import re + +import inflect + +_inflect = inflect.engine() +_comma_number_re = re.compile(r'([0-9][0-9\,]+[0-9])') +_decimal_number_re = re.compile(r'([0-9]+\.[0-9]+)') +_pounds_re = re.compile(r'£([0-9\,]*[0-9]+)') +_dollars_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(m): + return m.group(1).replace(',', '') + + +def _expand_decimal_point(m): + return m.group(1).replace('.', ' point ') + + +def _expand_dollars(m): + match = m.group(1) + parts = match.split('.') + if len(parts) > 2: + return match + ' dollars' # Unexpected format + dollars = int(parts[0]) if parts[0] else 0 + cents = int(parts[1]) if len(parts) > 1 and parts[1] else 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) + elif dollars: + 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) + else: + return 'zero dollars' + + +def _expand_ordinal(m): + return _inflect.number_to_words(m.group(0)) + + +def _expand_number(m): + num = int(m.group(0)) + if num > 1000 and num < 3000: + if num == 2000: + return 'two thousand' + elif num > 2000 and num < 2010: + return 'two thousand ' + _inflect.number_to_words(num % 100) + elif num % 100 == 0: + return _inflect.number_to_words(num // 100) + ' hundred' + else: + return _inflect.number_to_words( + num, andword='', zero='oh', group=2).replace(', ', ' ') + else: + return _inflect.number_to_words(num, andword='') + + +def normalize_numbers(text): + """ Normalize numbers in English text. + """ + text = re.sub(_comma_number_re, _remove_commas, text) + text = re.sub(_pounds_re, r'\1 pounds', text) + text = re.sub(_dollars_re, _expand_dollars, text) + text = re.sub(_decimal_number_re, _expand_decimal_point, text) + text = re.sub(_ordinal_re, _expand_ordinal, text) + text = re.sub(_number_re, _expand_number, text) + return text diff --git a/ernie-sat/paddlespeech/t2s/frontend/normalizer/width.py b/ernie-sat/paddlespeech/t2s/frontend/normalizer/width.py new file mode 100644 index 0000000..d655e92 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/normalizer/width.py @@ -0,0 +1,40 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def full2half_width(ustr): + half = [] + for u in ustr: + num = ord(u) + if num == 0x3000: # 全角空格变半角 + num = 32 + elif 0xFF01 <= num <= 0xFF5E: + num -= 0xfee0 + u = chr(num) + half.append(u) + return ''.join(half) + + +def half2full_width(ustr): + full = [] + for u in ustr: + num = ord(u) + if num == 32: # 半角空格变全角 + num = 0x3000 + elif 0x21 <= num <= 0x7E: + num += 0xfee0 + u = chr(num) # to unicode + full.append(u) + + return ''.join(full) diff --git a/ernie-sat/paddlespeech/t2s/frontend/phonectic.py b/ernie-sat/paddlespeech/t2s/frontend/phonectic.py new file mode 100644 index 0000000..8e9f117 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/phonectic.py @@ -0,0 +1,294 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from abc import ABC +from abc import abstractmethod +from typing import List + +import numpy as np +import paddle +from g2p_en import G2p +from g2pM import G2pM + +from paddlespeech.t2s.frontend.normalizer.normalizer import normalize +from paddlespeech.t2s.frontend.punctuation import get_punctuations +from paddlespeech.t2s.frontend.vocab import Vocab +from paddlespeech.t2s.frontend.zh_normalization.text_normlization import TextNormalizer + +# discard opencc untill we find an easy solution to install it on windows +# from opencc import OpenCC + +__all__ = ["Phonetics", "English", "EnglishCharacter", "Chinese"] + + +class Phonetics(ABC): + @abstractmethod + def __call__(self, sentence): + pass + + @abstractmethod + def phoneticize(self, sentence): + pass + + @abstractmethod + def numericalize(self, phonemes): + pass + + +class English(Phonetics): + """ Normalize the input text sequence and convert into pronunciation id sequence. + """ + + def __init__(self, phone_vocab_path=None): + self.backend = G2p() + self.phonemes = list(self.backend.phonemes) + self.punctuations = get_punctuations("en") + self.vocab = Vocab(self.phonemes + self.punctuations) + self.vocab_phones = {} + self.punc = ":,;。?!“”‘’':,;.?!" + self.text_normalizer = TextNormalizer() + if phone_vocab_path: + with open(phone_vocab_path, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + self.vocab_phones[phn] = int(id) + + def phoneticize(self, sentence): + """ Normalize the input text sequence and convert it into pronunciation sequence. + Args: + sentence (str): The input text sequence. + Returns: + List[str]: The list of pronunciation sequence. + """ + start = self.vocab.start_symbol + end = self.vocab.end_symbol + phonemes = ([] if start is None else [start]) \ + + self.backend(sentence) \ + + ([] if end is None else [end]) + phonemes = [item for item in phonemes if item in self.vocab.stoi] + return phonemes + + def _p2id(self, phonemes: List[str]) -> np.array: + phone_ids = [self.vocab_phones[item] for item in phonemes] + return np.array(phone_ids, np.int64) + + def get_input_ids(self, sentence: str, + merge_sentences: bool=False) -> paddle.Tensor: + result = {} + sentences = self.text_normalizer._split(sentence, lang="en") + phones_list = [] + temp_phone_ids = [] + for sentence in sentences: + phones = self.phoneticize(sentence) + # remove start_symbol and end_symbol + phones = phones[1:-1] + phones = [phn for phn in phones if not phn.isspace()] + # replace unk phone with sp + phones = [ + phn + if (phn in self.vocab_phones and phn not in self.punc) else "sp" + for phn in phones + ] + phones_list.append(phones) + + if merge_sentences: + merge_list = sum(phones_list, []) + # rm the last 'sp' to avoid the noise at the end + # cause in the training data, no 'sp' in the end + if merge_list[-1] == 'sp': + merge_list = merge_list[:-1] + phones_list = [] + phones_list.append(merge_list) + + for part_phones_list in phones_list: + phone_ids = self._p2id(part_phones_list) + phone_ids = paddle.to_tensor(phone_ids) + temp_phone_ids.append(phone_ids) + result["phone_ids"] = temp_phone_ids + return result + + def numericalize(self, phonemes): + """ Convert pronunciation sequence into pronunciation id sequence. + Args: + phonemes (List[str]): The list of pronunciation sequence. + Returns: + List[int]: The list of pronunciation id sequence. + """ + ids = [ + self.vocab.lookup(item) for item in phonemes + if item in self.vocab.stoi + ] + return ids + + def reverse(self, ids): + """ Reverse the list of pronunciation id sequence to a list of pronunciation sequence. + Args: + ids (List[int]): The list of pronunciation id sequence. + Returns: + List[str]: The list of pronunciation sequence. + """ + return [self.vocab.reverse(i) for i in ids] + + def __call__(self, sentence): + """ Convert the input text sequence into pronunciation id sequence. + Args: + sentence(str): The input text sequence. + Returns: + List[str]: The list of pronunciation id sequence. + """ + return self.numericalize(self.phoneticize(sentence)) + + @property + def vocab_size(self): + """ Vocab size. + """ + return len(self.vocab) + + +class EnglishCharacter(Phonetics): + """ Normalize the input text sequence and convert it into character id sequence. + """ + + def __init__(self): + self.backend = G2p() + self.graphemes = list(self.backend.graphemes) + self.punctuations = get_punctuations("en") + self.vocab = Vocab(self.graphemes + self.punctuations) + + def phoneticize(self, sentence): + """ Normalize the input text sequence. + Args: + sentence(str): The input text sequence. + Returns: + str: A text sequence after normalize. + """ + words = normalize(sentence) + return words + + def numericalize(self, sentence): + """ Convert a text sequence into ids. + Args: + sentence (str): The input text sequence. + Returns: + List[int]: + List of a character id sequence. + """ + ids = [ + self.vocab.lookup(item) for item in sentence + if item in self.vocab.stoi + ] + return ids + + def reverse(self, ids): + """ Convert a character id sequence into text. + Args: + ids (List[int]): List of a character id sequence. + Returns: + str: The input text sequence. + """ + return [self.vocab.reverse(i) for i in ids] + + def __call__(self, sentence): + """ Normalize the input text sequence and convert it into character id sequence. + Args: + sentence (str): The input text sequence. + Returns: + List[int]: List of a character id sequence. + """ + return self.numericalize(self.phoneticize(sentence)) + + @property + def vocab_size(self): + """ Vocab size. + """ + return len(self.vocab) + + +class Chinese(Phonetics): + """Normalize Chinese text sequence and convert it into ids. + """ + + def __init__(self): + # self.opencc_backend = OpenCC('t2s.json') + self.backend = G2pM() + self.phonemes = self._get_all_syllables() + self.punctuations = get_punctuations("cn") + self.vocab = Vocab(self.phonemes + self.punctuations) + + def _get_all_syllables(self): + all_syllables = set([ + syllable for k, v in self.backend.cedict.items() for syllable in v + ]) + return list(all_syllables) + + def phoneticize(self, sentence): + """ Normalize the input text sequence and convert it into pronunciation sequence. + Args: + sentence(str): The input text sequence. + Returns: + List[str]: The list of pronunciation sequence. + """ + # simplified = self.opencc_backend.convert(sentence) + simplified = sentence + phonemes = self.backend(simplified) + start = self.vocab.start_symbol + end = self.vocab.end_symbol + phonemes = ([] if start is None else [start]) \ + + phonemes \ + + ([] if end is None else [end]) + return self._filter_symbols(phonemes) + + def _filter_symbols(self, phonemes): + cleaned_phonemes = [] + for item in phonemes: + if item in self.vocab.stoi: + cleaned_phonemes.append(item) + else: + for char in item: + if char in self.vocab.stoi: + cleaned_phonemes.append(char) + return cleaned_phonemes + + def numericalize(self, phonemes): + """ Convert pronunciation sequence into pronunciation id sequence. + Args: + phonemes(List[str]): The list of pronunciation sequence. + Returns: + List[int]: The list of pronunciation id sequence. + """ + ids = [self.vocab.lookup(item) for item in phonemes] + return ids + + def __call__(self, sentence): + """ Convert the input text sequence into pronunciation id sequence. + Args: + sentence (str): The input text sequence. + Returns: + List[str]: The list of pronunciation id sequence. + """ + return self.numericalize(self.phoneticize(sentence)) + + @property + def vocab_size(self): + """ Vocab size. + """ + return len(self.vocab) + + def reverse(self, ids): + """ Reverse the list of pronunciation id sequence to a list of pronunciation sequence. + Args: + ids (List[int]): The list of pronunciation id sequence. + Returns: + List[str]: The list of pronunciation sequence. + """ + return [self.vocab.reverse(i) for i in ids] diff --git a/ernie-sat/paddlespeech/t2s/frontend/punctuation.py b/ernie-sat/paddlespeech/t2s/frontend/punctuation.py new file mode 100644 index 0000000..23636dc --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/punctuation.py @@ -0,0 +1,36 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["get_punctuations"] + +EN_PUNCT = [ + " ", + "-", + "...", + ",", + ".", + "?", + "!", +] + +CN_PUNCT = ["、", ",", ";", ":", "。", "?", "!"] + + +def get_punctuations(lang): + if lang == "en": + return EN_PUNCT + elif lang == "cn": + return CN_PUNCT + else: + raise ValueError(f"language {lang} Not supported") diff --git a/ernie-sat/paddlespeech/t2s/frontend/tone_sandhi.py b/ernie-sat/paddlespeech/t2s/frontend/tone_sandhi.py new file mode 100644 index 0000000..07f7fa2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/tone_sandhi.py @@ -0,0 +1,348 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List +from typing import Tuple + +import jieba +from pypinyin import lazy_pinyin +from pypinyin import Style + + +class ToneSandhi(): + def __init__(self): + self.must_neural_tone_words = { + '麻烦', '麻利', '鸳鸯', '高粱', '骨头', '骆驼', '马虎', '首饰', '馒头', '馄饨', '风筝', + '难为', '队伍', '阔气', '闺女', '门道', '锄头', '铺盖', '铃铛', '铁匠', '钥匙', '里脊', + '里头', '部分', '那么', '道士', '造化', '迷糊', '连累', '这么', '这个', '运气', '过去', + '软和', '转悠', '踏实', '跳蚤', '跟头', '趔趄', '财主', '豆腐', '讲究', '记性', '记号', + '认识', '规矩', '见识', '裁缝', '补丁', '衣裳', '衣服', '衙门', '街坊', '行李', '行当', + '蛤蟆', '蘑菇', '薄荷', '葫芦', '葡萄', '萝卜', '荸荠', '苗条', '苗头', '苍蝇', '芝麻', + '舒服', '舒坦', '舌头', '自在', '膏药', '脾气', '脑袋', '脊梁', '能耐', '胳膊', '胭脂', + '胡萝', '胡琴', '胡同', '聪明', '耽误', '耽搁', '耷拉', '耳朵', '老爷', '老实', '老婆', + '老头', '老太', '翻腾', '罗嗦', '罐头', '编辑', '结实', '红火', '累赘', '糨糊', '糊涂', + '精神', '粮食', '簸箕', '篱笆', '算计', '算盘', '答应', '笤帚', '笑语', '笑话', '窟窿', + '窝囊', '窗户', '稳当', '稀罕', '称呼', '秧歌', '秀气', '秀才', '福气', '祖宗', '砚台', + '码头', '石榴', '石头', '石匠', '知识', '眼睛', '眯缝', '眨巴', '眉毛', '相声', '盘算', + '白净', '痢疾', '痛快', '疟疾', '疙瘩', '疏忽', '畜生', '生意', '甘蔗', '琵琶', '琢磨', + '琉璃', '玻璃', '玫瑰', '玄乎', '狐狸', '状元', '特务', '牲口', '牙碜', '牌楼', '爽快', + '爱人', '热闹', '烧饼', '烟筒', '烂糊', '点心', '炊帚', '灯笼', '火候', '漂亮', '滑溜', + '溜达', '温和', '清楚', '消息', '浪头', '活泼', '比方', '正经', '欺负', '模糊', '槟榔', + '棺材', '棒槌', '棉花', '核桃', '栅栏', '柴火', '架势', '枕头', '枇杷', '机灵', '本事', + '木头', '木匠', '朋友', '月饼', '月亮', '暖和', '明白', '时候', '新鲜', '故事', '收拾', + '收成', '提防', '挖苦', '挑剔', '指甲', '指头', '拾掇', '拳头', '拨弄', '招牌', '招呼', + '抬举', '护士', '折腾', '扫帚', '打量', '打算', '打点', '打扮', '打听', '打发', '扎实', + '扁担', '戒指', '懒得', '意识', '意思', '情形', '悟性', '怪物', '思量', '怎么', '念头', + '念叨', '快活', '忙活', '志气', '心思', '得罪', '张罗', '弟兄', '开通', '应酬', '庄稼', + '干事', '帮手', '帐篷', '希罕', '师父', '师傅', '巴结', '巴掌', '差事', '工夫', '岁数', + '屁股', '尾巴', '少爷', '小气', '小伙', '将就', '对头', '对付', '寡妇', '家伙', '客气', + '实在', '官司', '学问', '学生', '字号', '嫁妆', '媳妇', '媒人', '婆家', '娘家', '委屈', + '姑娘', '姐夫', '妯娌', '妥当', '妖精', '奴才', '女婿', '头发', '太阳', '大爷', '大方', + '大意', '大夫', '多少', '多么', '外甥', '壮实', '地道', '地方', '在乎', '困难', '嘴巴', + '嘱咐', '嘟囔', '嘀咕', '喜欢', '喇嘛', '喇叭', '商量', '唾沫', '哑巴', '哈欠', '哆嗦', + '咳嗽', '和尚', '告诉', '告示', '含糊', '吓唬', '后头', '名字', '名堂', '合同', '吆喝', + '叫唤', '口袋', '厚道', '厉害', '千斤', '包袱', '包涵', '匀称', '勤快', '动静', '动弹', + '功夫', '力气', '前头', '刺猬', '刺激', '别扭', '利落', '利索', '利害', '分析', '出息', + '凑合', '凉快', '冷战', '冤枉', '冒失', '养活', '关系', '先生', '兄弟', '便宜', '使唤', + '佩服', '作坊', '体面', '位置', '似的', '伙计', '休息', '什么', '人家', '亲戚', '亲家', + '交情', '云彩', '事情', '买卖', '主意', '丫头', '丧气', '两口', '东西', '东家', '世故', + '不由', '不在', '下水', '下巴', '上头', '上司', '丈夫', '丈人', '一辈', '那个', '菩萨', + '父亲', '母亲', '咕噜', '邋遢', '费用', '冤家', '甜头', '介绍', '荒唐', '大人', '泥鳅', + '幸福', '熟悉', '计划', '扑腾', '蜡烛', '姥爷', '照顾', '喉咙', '吉他', '弄堂', '蚂蚱', + '凤凰', '拖沓', '寒碜', '糟蹋', '倒腾', '报复', '逻辑', '盘缠', '喽啰', '牢骚', '咖喱', + '扫把', '惦记' + } + self.must_not_neural_tone_words = { + "男子", "女子", "分子", "原子", "量子", "莲子", "石子", "瓜子", "电子", "人人", "虎虎" + } + self.punc = ":,;。?!“”‘’':,;.?!" + + # the meaning of jieba pos tag: https://blog.csdn.net/weixin_44174352/article/details/113731041 + # e.g. + # word: "家里" + # pos: "s" + # finals: ['ia1', 'i3'] + def _neural_sandhi(self, word: str, pos: str, + finals: List[str]) -> List[str]: + + # reduplication words for n. and v. e.g. 奶奶, 试试, 旺旺 + for j, item in enumerate(word): + if j - 1 >= 0 and item == word[j - 1] and pos[0] in { + "n", "v", "a" + } and word not in self.must_not_neural_tone_words: + finals[j] = finals[j][:-1] + "5" + ge_idx = word.find("个") + if len(word) >= 1 and word[-1] in "吧呢哈啊呐噻嘛吖嗨呐哦哒额滴哩哟喽啰耶喔诶": + finals[-1] = finals[-1][:-1] + "5" + elif len(word) >= 1 and word[-1] in "的地得": + finals[-1] = finals[-1][:-1] + "5" + # e.g. 走了, 看着, 去过 + elif len(word) == 1 and word in "了着过" and pos in {"ul", "uz", "ug"}: + finals[-1] = finals[-1][:-1] + "5" + elif len(word) > 1 and word[-1] in "们子" and pos in { + "r", "n" + } and word not in self.must_not_neural_tone_words: + finals[-1] = finals[-1][:-1] + "5" + # e.g. 桌上, 地下, 家里 + elif len(word) > 1 and word[-1] in "上下里" and pos in {"s", "l", "f"}: + finals[-1] = finals[-1][:-1] + "5" + # e.g. 上来, 下去 + elif len(word) > 1 and word[-1] in "来去" and word[-2] in "上下进出回过起开": + finals[-1] = finals[-1][:-1] + "5" + # 个做量词 + elif (ge_idx >= 1 and + (word[ge_idx - 1].isnumeric() or + word[ge_idx - 1] in "几有两半多各整每做是")) or word == '个': + finals[ge_idx] = finals[ge_idx][:-1] + "5" + else: + if word in self.must_neural_tone_words or word[ + -2:] in self.must_neural_tone_words: + finals[-1] = finals[-1][:-1] + "5" + + word_list = self._split_word(word) + finals_list = [finals[:len(word_list[0])], finals[len(word_list[0]):]] + for i, word in enumerate(word_list): + # conventional neural in Chinese + if word in self.must_neural_tone_words or word[ + -2:] in self.must_neural_tone_words: + finals_list[i][-1] = finals_list[i][-1][:-1] + "5" + finals = sum(finals_list, []) + return finals + + def _bu_sandhi(self, word: str, finals: List[str]) -> List[str]: + # e.g. 看不懂 + if len(word) == 3 and word[1] == "不": + finals[1] = finals[1][:-1] + "5" + else: + for i, char in enumerate(word): + # "不" before tone4 should be bu2, e.g. 不怕 + if char == "不" and i + 1 < len(word) and finals[i + + 1][-1] == "4": + finals[i] = finals[i][:-1] + "2" + return finals + + def _yi_sandhi(self, word: str, finals: List[str]) -> List[str]: + # "一" in number sequences, e.g. 一零零, 二一零 + if word.find("一") != -1 and all( + [item.isnumeric() for item in word if item != "一"]): + return finals + # "一" between reduplication words shold be yi5, e.g. 看一看 + elif len(word) == 3 and word[1] == "一" and word[0] == word[-1]: + finals[1] = finals[1][:-1] + "5" + # when "一" is ordinal word, it should be yi1 + elif word.startswith("第一"): + finals[1] = finals[1][:-1] + "1" + else: + for i, char in enumerate(word): + if char == "一" and i + 1 < len(word): + # "一" before tone4 should be yi2, e.g. 一段 + if finals[i + 1][-1] == "4": + finals[i] = finals[i][:-1] + "2" + # "一" before non-tone4 should be yi4, e.g. 一天 + else: + # "一" 后面如果是标点,还读一声 + if word[i + 1] not in self.punc: + finals[i] = finals[i][:-1] + "4" + return finals + + def _split_word(self, word: str) -> List[str]: + word_list = jieba.cut_for_search(word) + word_list = sorted(word_list, key=lambda i: len(i), reverse=False) + first_subword = word_list[0] + first_begin_idx = word.find(first_subword) + if first_begin_idx == 0: + second_subword = word[len(first_subword):] + new_word_list = [first_subword, second_subword] + else: + second_subword = word[:-len(first_subword)] + new_word_list = [second_subword, first_subword] + return new_word_list + + def _three_sandhi(self, word: str, finals: List[str]) -> List[str]: + if len(word) == 2 and self._all_tone_three(finals): + finals[0] = finals[0][:-1] + "2" + elif len(word) == 3: + word_list = self._split_word(word) + if self._all_tone_three(finals): + # disyllabic + monosyllabic, e.g. 蒙古/包 + if len(word_list[0]) == 2: + finals[0] = finals[0][:-1] + "2" + finals[1] = finals[1][:-1] + "2" + # monosyllabic + disyllabic, e.g. 纸/老虎 + elif len(word_list[0]) == 1: + finals[1] = finals[1][:-1] + "2" + else: + finals_list = [ + finals[:len(word_list[0])], finals[len(word_list[0]):] + ] + if len(finals_list) == 2: + for i, sub in enumerate(finals_list): + # e.g. 所有/人 + if self._all_tone_three(sub) and len(sub) == 2: + finals_list[i][0] = finals_list[i][0][:-1] + "2" + # e.g. 好/喜欢 + elif i == 1 and not self._all_tone_three(sub) and finals_list[i][0][-1] == "3" and \ + finals_list[0][-1][-1] == "3": + + finals_list[0][-1] = finals_list[0][-1][:-1] + "2" + finals = sum(finals_list, []) + # split idiom into two words who's length is 2 + elif len(word) == 4: + finals_list = [finals[:2], finals[2:]] + finals = [] + for sub in finals_list: + if self._all_tone_three(sub): + sub[0] = sub[0][:-1] + "2" + finals += sub + + return finals + + def _all_tone_three(self, finals: List[str]) -> bool: + return all(x[-1] == "3" for x in finals) + + # merge "不" and the word behind it + # if don't merge, "不" sometimes appears alone according to jieba, which may occur sandhi error + def _merge_bu(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + last_word = "" + for word, pos in seg: + if last_word == "不": + word = last_word + word + if word != "不": + new_seg.append((word, pos)) + last_word = word[:] + if last_word == "不": + new_seg.append((last_word, 'd')) + last_word = "" + return new_seg + + # function 1: merge "一" and reduplication words in it's left and right, e.g. "听","一","听" ->"听一听" + # function 2: merge single "一" and the word behind it + # if don't merge, "一" sometimes appears alone according to jieba, which may occur sandhi error + # e.g. + # input seg: [('听', 'v'), ('一', 'm'), ('听', 'v')] + # output seg: [['听一听', 'v']] + def _merge_yi(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + # function 1 + for i, (word, pos) in enumerate(seg): + if i - 1 >= 0 and word == "一" and i + 1 < len(seg) and seg[i - 1][ + 0] == seg[i + 1][0] and seg[i - 1][1] == "v": + new_seg[i - 1][0] = new_seg[i - 1][0] + "一" + new_seg[i - 1][0] + else: + if i - 2 >= 0 and seg[i - 1][0] == "一" and seg[i - 2][ + 0] == word and pos == "v": + continue + else: + new_seg.append([word, pos]) + seg = new_seg + new_seg = [] + # function 2 + for i, (word, pos) in enumerate(seg): + if new_seg and new_seg[-1][0] == "一": + new_seg[-1][0] = new_seg[-1][0] + word + else: + new_seg.append([word, pos]) + return new_seg + + # the first and the second words are all_tone_three + def _merge_continuous_three_tones( + self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + sub_finals_list = [ + lazy_pinyin( + word, neutral_tone_with_five=True, style=Style.FINALS_TONE3) + for (word, pos) in seg + ] + assert len(sub_finals_list) == len(seg) + merge_last = [False] * len(seg) + for i, (word, pos) in enumerate(seg): + if i - 1 >= 0 and self._all_tone_three( + sub_finals_list[i - 1]) and self._all_tone_three( + sub_finals_list[i]) and not merge_last[i - 1]: + # if the last word is reduplication, not merge, because reduplication need to be _neural_sandhi + if not self._is_reduplication(seg[i - 1][0]) and len( + seg[i - 1][0]) + len(seg[i][0]) <= 3: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + merge_last[i] = True + else: + new_seg.append([word, pos]) + else: + new_seg.append([word, pos]) + + return new_seg + + def _is_reduplication(self, word: str) -> bool: + return len(word) == 2 and word[0] == word[1] + + # the last char of first word and the first char of second word is tone_three + def _merge_continuous_three_tones_2( + self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + sub_finals_list = [ + lazy_pinyin( + word, neutral_tone_with_five=True, style=Style.FINALS_TONE3) + for (word, pos) in seg + ] + assert len(sub_finals_list) == len(seg) + merge_last = [False] * len(seg) + for i, (word, pos) in enumerate(seg): + if i - 1 >= 0 and sub_finals_list[i - 1][-1][-1] == "3" and sub_finals_list[i][0][-1] == "3" and not \ + merge_last[i - 1]: + # if the last word is reduplication, not merge, because reduplication need to be _neural_sandhi + if not self._is_reduplication(seg[i - 1][0]) and len( + seg[i - 1][0]) + len(seg[i][0]) <= 3: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + merge_last[i] = True + else: + new_seg.append([word, pos]) + else: + new_seg.append([word, pos]) + return new_seg + + def _merge_er(self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + for i, (word, pos) in enumerate(seg): + if i - 1 >= 0 and word == "儿": + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + else: + new_seg.append([word, pos]) + return new_seg + + def _merge_reduplication( + self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + new_seg = [] + for i, (word, pos) in enumerate(seg): + if new_seg and word == new_seg[-1][0]: + new_seg[-1][0] = new_seg[-1][0] + seg[i][0] + else: + new_seg.append([word, pos]) + return new_seg + + def pre_merge_for_modify( + self, seg: List[Tuple[str, str]]) -> List[Tuple[str, str]]: + seg = self._merge_bu(seg) + seg = self._merge_yi(seg) + seg = self._merge_reduplication(seg) + seg = self._merge_continuous_three_tones(seg) + seg = self._merge_continuous_three_tones_2(seg) + seg = self._merge_er(seg) + return seg + + def modified_tone(self, word: str, pos: str, + finals: List[str]) -> List[str]: + finals = self._bu_sandhi(word, finals) + finals = self._yi_sandhi(word, finals) + finals = self._neural_sandhi(word, pos, finals) + finals = self._three_sandhi(word, finals) + return finals diff --git a/ernie-sat/paddlespeech/t2s/frontend/vocab.py b/ernie-sat/paddlespeech/t2s/frontend/vocab.py new file mode 100644 index 0000000..76bb3c7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/vocab.py @@ -0,0 +1,120 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import OrderedDict +from typing import Iterable + +__all__ = ["Vocab"] + + +class Vocab(object): + """ Vocabulary. + + Args: + symbols (Iterable[str]): Common symbols. + padding_symbol (str, optional): Symbol for pad. Defaults to "". + unk_symbol (str, optional): Symbol for unknow. Defaults to "" + start_symbol (str, optional): Symbol for start. Defaults to "" + end_symbol (str, optional): Symbol for end. Defaults to "" + """ + + def __init__(self, + symbols: Iterable[str], + padding_symbol="", + unk_symbol="", + start_symbol="", + end_symbol=""): + self.special_symbols = OrderedDict() + for i, item in enumerate( + [padding_symbol, unk_symbol, start_symbol, end_symbol]): + if item: + self.special_symbols[item] = len(self.special_symbols) + + self.padding_symbol = padding_symbol + self.unk_symbol = unk_symbol + self.start_symbol = start_symbol + self.end_symbol = end_symbol + + self.stoi = OrderedDict() + self.stoi.update(self.special_symbols) + + for i, s in enumerate(symbols): + if s not in self.stoi: + self.stoi[s] = len(self.stoi) + self.itos = {v: k for k, v in self.stoi.items()} + + def __len__(self): + return len(self.stoi) + + @property + def num_specials(self): + """ The number of special symbols. + """ + return len(self.special_symbols) + + # special tokens + @property + def padding_index(self): + """ The index of padding symbol + """ + return self.stoi.get(self.padding_symbol, -1) + + @property + def unk_index(self): + """The index of unknow symbol. + """ + return self.stoi.get(self.unk_symbol, -1) + + @property + def start_index(self): + """The index of start symbol. + """ + return self.stoi.get(self.start_symbol, -1) + + @property + def end_index(self): + """ The index of end symbol. + """ + return self.stoi.get(self.end_symbol, -1) + + def __repr__(self): + fmt = "Vocab(size: {},\nstoi:\n{})" + return fmt.format(len(self), self.stoi) + + def __str__(self): + return self.__repr__() + + def lookup(self, symbol): + """ The index that symbol correspond. + """ + return self.stoi[symbol] + + def reverse(self, index): + """ The symbol thar index cottespond. + """ + return self.itos[index] + + def add_symbol(self, symbol): + """ Add a new symbol in vocab. + """ + if symbol in self.stoi: + return + N = len(self.stoi) + self.stoi[symbol] = N + self.itos[N] = symbol + + def add_symbols(self, symbols): + """ Add multiple symbols in vocab. + """ + for symbol in symbols: + self.add_symbol(symbol) diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_frontend.py b/ernie-sat/paddlespeech/t2s/frontend/zh_frontend.py new file mode 100644 index 0000000..bb8ed5b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_frontend.py @@ -0,0 +1,314 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +from typing import Dict +from typing import List + +import jieba.posseg as psg +import numpy as np +import paddle +from g2pM import G2pM +from pypinyin import lazy_pinyin +from pypinyin import load_phrases_dict +from pypinyin import load_single_dict +from pypinyin import Style +from pypinyin_dict.phrase_pinyin_data import large_pinyin + +from paddlespeech.t2s.frontend.generate_lexicon import generate_lexicon +from paddlespeech.t2s.frontend.tone_sandhi import ToneSandhi +from paddlespeech.t2s.frontend.zh_normalization.text_normlization import TextNormalizer + + +class Frontend(): + def __init__(self, + g2p_model="pypinyin", + phone_vocab_path=None, + tone_vocab_path=None): + self.tone_modifier = ToneSandhi() + self.text_normalizer = TextNormalizer() + self.punc = ":,;。?!“”‘’':,;.?!" + # g2p_model can be pypinyin and g2pM + self.g2p_model = g2p_model + if self.g2p_model == "g2pM": + self.g2pM_model = G2pM() + self.pinyin2phone = generate_lexicon( + with_tone=True, with_erhua=False) + else: + self.__init__pypinyin() + self.must_erhua = {"小院儿", "胡同儿", "范儿", "老汉儿", "撒欢儿", "寻老礼儿", "妥妥儿"} + self.not_erhua = { + "虐儿", "为儿", "护儿", "瞒儿", "救儿", "替儿", "有儿", "一儿", "我儿", "俺儿", "妻儿", + "拐儿", "聋儿", "乞儿", "患儿", "幼儿", "孤儿", "婴儿", "婴幼儿", "连体儿", "脑瘫儿", + "流浪儿", "体弱儿", "混血儿", "蜜雪儿", "舫儿", "祖儿", "美儿", "应采儿", "可儿", "侄儿", + "孙儿", "侄孙儿", "女儿", "男儿", "红孩儿", "花儿", "虫儿", "马儿", "鸟儿", "猪儿", "猫儿", + "狗儿" + } + self.vocab_phones = {} + self.vocab_tones = {} + if phone_vocab_path: + with open(phone_vocab_path, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + self.vocab_phones[phn] = int(id) + if tone_vocab_path: + with open(tone_vocab_path, 'rt') as f: + tone_id = [line.strip().split() for line in f.readlines()] + for tone, id in tone_id: + self.vocab_tones[tone] = int(id) + + def __init__pypinyin(self): + large_pinyin.load() + + load_phrases_dict({u'开户行': [[u'ka1i'], [u'hu4'], [u'hang2']]}) + load_phrases_dict({u'发卡行': [[u'fa4'], [u'ka3'], [u'hang2']]}) + load_phrases_dict({u'放款行': [[u'fa4ng'], [u'kua3n'], [u'hang2']]}) + load_phrases_dict({u'茧行': [[u'jia3n'], [u'hang2']]}) + load_phrases_dict({u'行号': [[u'hang2'], [u'ha4o']]}) + load_phrases_dict({u'各地': [[u'ge4'], [u'di4']]}) + load_phrases_dict({u'借还款': [[u'jie4'], [u'hua2n'], [u'kua3n']]}) + load_phrases_dict({u'时间为': [[u'shi2'], [u'jia1n'], [u'we2i']]}) + load_phrases_dict({u'为准': [[u'we2i'], [u'zhu3n']]}) + load_phrases_dict({u'色差': [[u'se4'], [u'cha1']]}) + + # 调整字的拼音顺序 + load_single_dict({ord(u'地'): u'de,di4'}) + + def _get_initials_finals(self, word: str) -> List[List[str]]: + initials = [] + finals = [] + if self.g2p_model == "pypinyin": + orig_initials = lazy_pinyin( + word, neutral_tone_with_five=True, style=Style.INITIALS) + orig_finals = lazy_pinyin( + word, neutral_tone_with_five=True, style=Style.FINALS_TONE3) + for c, v in zip(orig_initials, orig_finals): + if re.match(r'i\d', v): + if c in ['z', 'c', 's']: + v = re.sub('i', 'ii', v) + elif c in ['zh', 'ch', 'sh', 'r']: + v = re.sub('i', 'iii', v) + initials.append(c) + finals.append(v) + elif self.g2p_model == "g2pM": + pinyins = self.g2pM_model(word, tone=True, char_split=False) + for pinyin in pinyins: + pinyin = pinyin.replace("u:", "v") + if pinyin in self.pinyin2phone: + initial_final_list = self.pinyin2phone[pinyin].split(" ") + if len(initial_final_list) == 2: + initials.append(initial_final_list[0]) + finals.append(initial_final_list[1]) + elif len(initial_final_list) == 1: + initials.append('') + finals.append(initial_final_list[1]) + else: + # If it's not pinyin (possibly punctuation) or no conversion is required + initials.append(pinyin) + finals.append(pinyin) + return initials, finals + + # if merge_sentences, merge all sentences into one phone sequence + def _g2p(self, + sentences: List[str], + merge_sentences: bool=True, + with_erhua: bool=True) -> List[List[str]]: + segments = sentences + phones_list = [] + for seg in segments: + phones = [] + # Replace all English words in the sentence + seg = re.sub('[a-zA-Z]+', '', seg) + seg_cut = psg.lcut(seg) + initials = [] + finals = [] + seg_cut = self.tone_modifier.pre_merge_for_modify(seg_cut) + for word, pos in seg_cut: + if pos == 'eng': + continue + sub_initials, sub_finals = self._get_initials_finals(word) + sub_finals = self.tone_modifier.modified_tone(word, pos, + sub_finals) + if with_erhua: + sub_initials, sub_finals = self._merge_erhua( + sub_initials, sub_finals, word, pos) + initials.append(sub_initials) + finals.append(sub_finals) + # assert len(sub_initials) == len(sub_finals) == len(word) + initials = sum(initials, []) + finals = sum(finals, []) + + for c, v in zip(initials, finals): + # NOTE: post process for pypinyin outputs + # we discriminate i, ii and iii + if c and c not in self.punc: + phones.append(c) + if c and c in self.punc: + phones.append('sp') + if v and v not in self.punc: + phones.append(v) + + phones_list.append(phones) + if merge_sentences: + merge_list = sum(phones_list, []) + # rm the last 'sp' to avoid the noise at the end + # cause in the training data, no 'sp' in the end + if merge_list[-1] == 'sp': + merge_list = merge_list[:-1] + phones_list = [] + phones_list.append(merge_list) + return phones_list + + def _merge_erhua(self, + initials: List[str], + finals: List[str], + word: str, + pos: str) -> List[List[str]]: + if word not in self.must_erhua and (word in self.not_erhua or + pos in {"a", "j", "nr"}): + return initials, finals + # "……" 等情况直接返回 + if len(finals) != len(word): + return initials, finals + + assert len(finals) == len(word) + + new_initials = [] + new_finals = [] + for i, phn in enumerate(finals): + if i == len(finals) - 1 and word[i] == "儿" and phn in { + "er2", "er5" + } and word[-2:] not in self.not_erhua and new_finals: + new_finals[-1] = new_finals[-1][:-1] + "r" + new_finals[-1][-1] + else: + new_finals.append(phn) + new_initials.append(initials[i]) + return new_initials, new_finals + + def _p2id(self, phonemes: List[str]) -> np.array: + # replace unk phone with sp + phonemes = [ + phn if phn in self.vocab_phones else "sp" for phn in phonemes + ] + phone_ids = [self.vocab_phones[item] for item in phonemes] + return np.array(phone_ids, np.int64) + + def _t2id(self, tones: List[str]) -> np.array: + # replace unk phone with sp + tones = [tone if tone in self.vocab_tones else "0" for tone in tones] + tone_ids = [self.vocab_tones[item] for item in tones] + return np.array(tone_ids, np.int64) + + def _get_phone_tone(self, phonemes: List[str], + get_tone_ids: bool=False) -> List[List[str]]: + phones = [] + tones = [] + if get_tone_ids and self.vocab_tones: + for full_phone in phonemes: + # split tone from finals + match = re.match(r'^(\w+)([012345])$', full_phone) + if match: + phone = match.group(1) + tone = match.group(2) + # if the merged erhua not in the vocab + # assume that the input is ['iaor3'] and 'iaor' not in self.vocab_phones, we split 'iaor' into ['iao','er'] + # and the tones accordingly change from ['3'] to ['3','2'], while '2' is the tone of 'er2' + if len(phone) >= 2 and phone != "er" and phone[ + -1] == 'r' and phone not in self.vocab_phones and phone[: + -1] in self.vocab_phones: + phones.append(phone[:-1]) + phones.append("er") + tones.append(tone) + tones.append("2") + else: + phones.append(phone) + tones.append(tone) + else: + phones.append(full_phone) + tones.append('0') + else: + for phone in phonemes: + # if the merged erhua not in the vocab + # assume that the input is ['iaor3'] and 'iaor' not in self.vocab_phones, change ['iaor3'] to ['iao3','er2'] + if len(phone) >= 3 and phone[:-1] != "er" and phone[ + -2] == 'r' and phone not in self.vocab_phones and ( + phone[:-2] + phone[-1]) in self.vocab_phones: + phones.append((phone[:-2] + phone[-1])) + phones.append("er2") + else: + phones.append(phone) + return phones, tones + + def get_phonemes(self, + sentence: str, + merge_sentences: bool=True, + with_erhua: bool=True, + robot: bool=False, + print_info: bool=False) -> List[List[str]]: + sentences = self.text_normalizer.normalize(sentence) + phonemes = self._g2p( + sentences, merge_sentences=merge_sentences, with_erhua=with_erhua) + # change all tones to `1` + if robot: + new_phonemes = [] + for sentence in phonemes: + new_sentence = [] + for item in sentence: + # `er` only have tone `2` + if item[-1] in "12345" and item != "er2": + item = item[:-1] + "1" + new_sentence.append(item) + new_phonemes.append(new_sentence) + phonemes = new_phonemes + if print_info: + print("----------------------------") + print("text norm results:") + print(sentences) + print("----------------------------") + print("g2p results:") + print(phonemes) + print("----------------------------") + return phonemes + + def get_input_ids(self, + sentence: str, + merge_sentences: bool=True, + get_tone_ids: bool=False, + robot: bool=False, + print_info: bool=False) -> Dict[str, List[paddle.Tensor]]: + phonemes = self.get_phonemes( + sentence, + merge_sentences=merge_sentences, + print_info=print_info, + robot=robot) + result = {} + phones = [] + tones = [] + temp_phone_ids = [] + temp_tone_ids = [] + for part_phonemes in phonemes: + phones, tones = self._get_phone_tone( + part_phonemes, get_tone_ids=get_tone_ids) + if tones: + tone_ids = self._t2id(tones) + tone_ids = paddle.to_tensor(tone_ids) + temp_tone_ids.append(tone_ids) + if phones: + phone_ids = self._p2id(phones) + phone_ids = paddle.to_tensor(phone_ids) + temp_phone_ids.append(phone_ids) + if temp_tone_ids: + result["tone_ids"] = temp_tone_ids + if temp_phone_ids: + result["phone_ids"] = temp_phone_ids + return result diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/README.md b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/README.md new file mode 100644 index 0000000..92eea9f --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/README.md @@ -0,0 +1,16 @@ +## Supported NSW (Non-Standard-Word) Normalization + +|NSW type|raw|normalized| +|:--|:-|:-| +|serial number|电影中梁朝伟扮演的陈永仁的编号27149|电影中梁朝伟扮演的陈永仁的编号二七一四九| +|cardinal|这块黄金重达324.75克
我们班的最高总分为583分|这块黄金重达三百二十四点七五克
我们班的最高总分为五百八十三分| +|numeric range |12\~23
-1.5\~2|十二到二十三
负一点五到二| +|date|她出生于86年8月18日,她弟弟出生于1995年3月1日|她出生于八六年八月十八日, 她弟弟出生于一九九五年三月一日| +|time|等会请在12:05请通知我|等会请在十二点零五分请通知我 +|temperature|今天的最低气温达到-10°C|今天的最低气温达到零下十度 +|fraction|现场有7/12的观众投出了赞成票|现场有十二分之七的观众投出了赞成票| +|percentage|明天有62%的概率降雨|明天有百分之六十二的概率降雨| +|money|随便来几个价格12块5,34.5元,20.1万|随便来几个价格十二块五,三十四点五元,二十点一万| +|telephone|这是固话0421-33441122
这是手机+86 18544139121|这是固话零四二一三三四四一一二二
这是手机八六一八五四四一三九一二一| +## References +[Pull requests #658 of DeepSpeech](https://github.com/PaddlePaddle/DeepSpeech/pull/658/files) diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/__init__.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/__init__.py new file mode 100644 index 0000000..a9d1f44 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.t2s.frontend.zh_normalization.text_normlization import * diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/char_convert.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/char_convert.py new file mode 100644 index 0000000..dcf95d7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/char_convert.py @@ -0,0 +1,46 @@ +# coding=utf-8 +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Traditional and simplified Chinese conversion, a simplified character may correspond to multiple traditional characters. +""" +simplified_charcters = '制咖片型超声盘鉴定仔点他命书歌粉巾字帐恤手指记忆棒形转弯沟光○〇㐄㐅㐆㐌㐖毒㐜㐡㐤㐰㐺㑇㑳㒳㒸㔾㗂㗎㝵㞎㞙㞞以㢲㢴㤅㥁㥯㨗㫺㬎㮎㮚㮸㲋㲱㲾㳮涧㵪㶸㷖㷭㹢㹴犬㺢狓㺵碗㽮㿝䍃䔢䖟䖸䗈䗥䗪䝓射䥯䦉䯝鲃鱼䲔䳗鹅䵹鼄䶑一对应映射丁不识下儿子做二休世丘之貉并中台原则串为甚谓干净了百事无成八变五十些人得道鸡升天代如并来去个国政策劲幽灵在欧洲游荡接样萝卜坑侧化传价元论醇共再准刀两断切分耕耘收获钱货物向看旧就绪险刻千金动劳永逸匙零夜半卡通回复返影踪反常态口咬气句话同吐快吹周味呼诺呜品红锅哄而散起唱和问三知生熟团漆黑火糟堆场空块面塌糊涂尘染壁厢夔已足多情露水大早到晚夫妻当关万莫开失古恨套所料既往孔见提师要家主审寸阴难买斗牛小撮部阵局展身层巴掌帆风顺席地带过年计于春头载四季期被蛇怕井绳度愿式份弹顷深前律径心意念差愁孤行俱全房厅交遮打技长把抓死拿眼泪鼻涕钥锁折段抿拍即合扫排掬挥拨拥上入击洞掷揽改故辙败文值名斑方面旁族日秋餐隔雅里终父旦时晌会霎间晃暴寒曝更月望垠际朝夕本正经利杯羹东西板枝独秀根筋杆进条龙服务概模次函数又性程总付步脚印趋登毛拔呵氧氮碳决雌雄波未平派谎言流清楚白准溜烟潭有获闻是处降琴鹤甲病发可拾沙目然了直以相眨穿睹瞥瞬矢的解石鸟神教秉虔诚秘种窝蜂穷窍笑置笔苟勾销抹杀煞等奖箍节吃箭仇双雕诗筹箩筐系列纸级士官统丝毫挂维网尽线微吭响股脑胎脉承腔臂力致效资源址器举功投般说讲规贸易叶障着慎满皆输号木电池衣倾钟高低视仁觉醒览遗角银币触溃九鼎蔽抄出驷马追重语破贫洗贯走路安蹴至几蹶振跃役胆汗较辈轮辞赞退六连遍递边针血锤音错门思闪真倒项栽雾类保护川先惊乍体哄鳞爪鸣滴泡邻域党专鼓作齐炒丑烯亥克内酯冬加奴卯肝炎基尺梁街裤镐客宠庭巳汝昌烷玲磊糖肇酉醛啷青县韪良香骨鲷丂七集河市弦喜嘴张舌堵区工业姊妹星架构巧彩扭歪拼凑余热曜武州爷浮屠美乡老阶树荤素碎落能魄鳃鳗珠丄丅丆万俟丈尚摸母娘量管群亚虎必我堂令申件装伏位博侠义界表女墟台戏臭皮匠胜诸葛亮赛顶倍催请运算包立叉戟离疫苗土史志演围揭瓦晒夷姑婆帝村宝烂尖杉碱屉桌山岔岛由纪峡坝库镇废从德后拗汤治旬食明昧曹朋友框栏极权幂曲归依猫民氟硼氯磷铁江侗自旅法司洋浦梅园温暖湾焦班幸用田略番叠皇炮捶硝苯酸腺苷棱草镜穗跳远索锦纲聚氰胺联店胚膲爱色堇紫罗兰芝茶饭菱云虫藏藩乱叛苏亲债凳学座恐恋柱测肌腹衩锥系貂企乌跪叩军车农题迭都甘油屯奏键短阿姨陪姐只顾茅庐槽驾魂鲜鹿页其菜单乘任供势午齿汉组织吊调泻唇坡城报坟外夸将尉建筑岸岗公床扬新剑升杭林栗校楼标款汽社浣海商馆剧院钢华港机械广媒环球融第医科证券综财乐育游涨犹岭疏瘾睑确兵领导缴肢膛船艾瑟尔苍蔡虞效衫覆访诉课谕议轨述野钩限敌鞋颌颔颚饶首龈站例修凡划垂届属崽颏厨拜挫摆放旋削棋榻槛礼沉注滑营狱画确仪聘花葬诏员跌辖周达酒锚闸陷陆雨雪飞威丌于丹久乏予理评产亢卑亦乎舞己悲矩圆词害志但住佞佳便俗信票案幅翁倦伦假偏倚斜亏鬼敲停备伤脾胃仅此像俭匮免宜穴焉戴兼容许冻伯仲负彼昼皂轩轾实刊划颠卫战哥比省非好黄饰别拘束掩奶睬选择摇扰烦苦枚写协厌及格受欢迎约只估侵犯割状告或缺抗拒挽撤救药喻磨灭端倪少逆逾越避靠适吉誉吝玉含延咎歹听啻渊善谋均匀堪忍够太惹妙妥妨孕症孝术室完纳推冠积宣疑辩栗碴称屈挠屑干涉衡待很忙恶忿怎么怠急耻恭息悦惑惜惟想愉愧怍慌愤启懂懈怀材才紧招认扣抵拉舍也罢插揣冒搭撞南墙扩核支攻敢雷攀敬里吗需景智暇曾罪遇朽枉止况竞争辱求愈渝溶济左右袒困补爽特寂寞示弱找谢畏强疾徐痛痒冤符眠睦瞅董何厚云措活疲羞者轻玻璃祥兆禁移稂莠稳佛换答简结果盟绝缕途给谈否羁翼耐肖胫毋宁兴舒若菲莱痕迹窠臼虚衰脸兔撒鹰棺范该详讳抬泰让须眉象众赀账费灰赖奇虑训辍辨菽麦辛近送透逞徒速续逮捕遂遑违逊斧钺艰醉锈随观弃显饱脂肪使丏丐帮丒且慢末丕替桃宗王尊凉爵各图屋脊粮署录坛吾禄职胄袭君厦丗北壑桐疹损逢陵鹬丙寅戌氨腈唑纶辰酮脱氢酶醚丞丢现掉纱帽弄扯炮碗丠両丣坐存激肩臻蒂莲悖序驱丨丩丫挺杈髻鬟细介俄伊犁京尼布订普渡央委监察检查剂圈设警队斯督剩震境航舶革防托播促质版蝾螈锋研艺历残消频谱精密制造陲邮候埔坚压坜凹汇执府究邦俘摄寮彬狼岳肺肿庸英讯诊埋粒胞括控码韩暑枪枢砥澳哇牟寿甸钻探篇签缀缝继耳肯照妇埃悬璧轴柜台辣搁浅邪跑纤阮阳私囊魔丮丰姿采丱烧丳丵丶丷丸参寨朗桂瑞砂衷霞貌凤仆舰因嫌宰峰干络牌持旨祭祷簿编罚宾办丼丿乀乂乃乄仰慕盛旷留考验阔乆乇么丑麽乊湖燃乑乒乓乕乖僻忤戾离谬迕乗危肥劫除隙浪婿乙炔肠酰吡咯盐乚乛乜嘢卿玄宫尾狐龟塔嶷兄弟泉章霄钉耙乞扎哀怜恕讨乢乣乤乥乧乨乩童乪乫乭乳晕汁液瑶浆牙癌突窦罩腐胶猪酪蛋糕菌瘤乴乵乶乷乸乹乺乼乾俸冰嘉哕嚎坤妈尸垒旱枯涸俐渴潮涩煸豆燥爹瘦瘪癣瞪袋脆姜贝隆馏乿亀亁叫咕攘扔搞男砸窜蓬麻亃亄亅却亇迟典今临繁累卵奉婚聪躬巨与迁添裂副宿岁怪恶尕仑愣杆硅硫钛铀锰芑杂异钠砷胂磺琥珀舱棍簧胡茬盗浩盆贩郎腿亍洪亐互欠助勉惠操斥诿系户译亓墓碑刑铃卅渠缤纷斗米旗宪钒灯徽瘟祖拳福谷丰脏腑绑肉腌苓蕴桥铺霸颜闹判喷冈底蛙陉矿亖亘亜罕们娜桑那努哈喀弗烈曼松森杜氏杯奥琛敦戊穆圣裔汇薛孙亟亡佚虏羊牢奋释卷卸契媾感额睫缠谊趾塞挤纽阻还配驰庄亨洛祚亪享津沪畿郊慈菴枇杷膏亭阁锃丽亳亶亹诛初责翻疯偶杰丛稠妖拖寰居吸授慧蜗吞壮魅狗矛盾益渣患忧稀描猿梦暂涯畜祸缘沸搜引擎臣横纭谁混援蒸兽狮税剖亻亼亽亡什献刹邡么仂仃仄仆富怨仈仉毕昔晨壳绍仍仏仒仕宦仗欺恃腰叹叹炬梓讫施仙后琼逝仚仝仞仟悔仡佬偿填泊拓扑簇羔购顿钦佩发棻阃驭养亿儆尤借帧赈凌叙帖李柔刚沃眦睚戒讹取飨读仨仫仮著泳卧躺韶夏裁仳仵唯贤凭钓诞仿似宋佛讽伀硕盼鹅伄儅伈伉俪柯始娃迈戈坦堡帕茨萨庙玛莉莎藤霍姆伋伍奢胥廷芳豪伎俩侍汛勒希羲雏伐憩整谟闲闲伕伙伴颐伜伝伢叔恒兹恩翰伱伲侣伶俜悧鼬伸懒缩喇叭伹伺伻伽倻辐伾似佃伫布乔妮墨佉卢佌贷劣廉昂档浓矮伞洼缓耗胸谷迷挡率龋宅沫舍疗佐贰佑占优据铧尝呢须鲁晓佗佘余坪寺瓜铳僧蒙芒陀龛哼呕坊奸孽弊揖祟茧缚誓贼佝偻瞀佟你夺赶佡佢佣佤佧贾佪佫佯佰佱洁绩酿肴佴卷佶佷佸佹佺佻佼佽佾具唤窘坏娱怒慨硬习惯聋膨胀蔓骇贵痹侀侁侂侃侄侅鸿燕侇侈糜靡侉侌妾侏儒仓鼠侐侑侔仑侘侚链侜偎傍钴循柳葫芦附価侮骂蔑侯岩截蚀局贴壶嬛宴捷携桶笺酌俣狭膝狄俅俉俊俏俎俑俓俔谚俚俛黎健呈固墒增守康箱湿祐镖镳杠盒靖膜龄俞豹猎噪孚封札筒托衍鸽剪撰稿炼厂禊练缮葺俯瞰撑冲效俳俴俵俶俷俺备俾伥倂倅储卒惶敷猝逃颉蓄崇隐倌倏忽刺蜡烛噍嚼坍扁抽毙葱楣灌灶粪背薮卖赔闭霉腾倓倔幸倘倜傥倝借箸挹浇阅倡狂倢倣値倥偬倨傲倩匡嗣冲柝珍倬倭寇猩倮倶倷倹勤赞偁偃充伪吏嗓寐惺扮拱芫茜藉虢钞偈伟晶偌宕距析滤殿疼瘫注颇偓偕鸭歇滞偝偟偢忘怡旺偨偩逼偫偭偯偰偱偲侦缉蹄偷减惰漏窥窃偸偺迹傀儡傅傈僳骂篱傎奎琳迪叟芭傒傔傕伧悉荒傜傞傢傣芽逼佣婢傮睨寄檄诵谣颂伛担辜弓惨蒿悼疤傺傻屄臆巢泄箧羡盖轧颓傿㑩僄僇佥僊働僎侨僔僖僚僝伪僣僤侥僦猴偾僩僬僭僮僯僰雇僵殖签静僾僿征陇儁侬儃儇侩朴薄儊儋儌儍傧儓俦侪拟尽儜儞儤儦儩汰哉寡渥裕酷儭儱罐儳儵儹傩俨儽兀臬臲鹫允勋勋宙宵帅憝彝谐嫂阋畅沛溢盈饥赫凶悍狠猛顽愚妣斩秦遣鞭耀敏荣槃泽爆碟磁秃缆辉霁卤朵娄孜烽酱勃汀箕裘钳耶蒙蕾彻兑软遭黜兎児韵媳爸兕觥兖兙兛兜售鍪肚兝兞兟兡兢兣樽殓涅睡禀籍赘泌啡肽奸幕涵涝熵疚眷稃衬讧赴焕椒歼植跏没试误猜栖窗肋袖颊兪卦撇胡岐廓轿疸枫茴珑厕秩募勺吨寓斤历亩迫筷厘最淫螺韬兮宽匪筛襄赢轭复兲诈刃堰戎痞蚁饷它冀铸冂冃円冇冉册嫁厉砺竭醮冏牧冑冓冔冕冖冗冘冞冢窄抑诬冥冫烘菇蛰冷凝坨橇淇淋炭饼砖碛窖醋雕雹霜冱冶炉艳嘲峻滩淡漠煖飕饮冼冽凃凄怆梗凅凇净凊凋敝蒙凔凛遵汞脢凞几凢処凰凯凵凶焰凸折刷纹预丧喽奔巡榜殡芙蓉租笼辑鞘萃凼锯镬刁蛮刂娩崩批拆摊掰蘖骤歧颗秒袂赃勿嘱忌磋琢肤刈羽刎讼戮舂桨艇刓刖霹雳刜创犊刡恙墅帜筵致劫劫刨昏默攸尿欲熏润薰圭删刮痧铲刱刲刳刴刵踏磅戳柏槐绣芹苋猬舟铭鹄鹜劫剁剃辫刭锉履铅克剌姻咽哨廊掠桅沿召瞻翅赵卜渺茫郭剒剔剕沥剚愎毅讷才剜剥啄采剞剟剡剣剤䌽剐肾驶黏剰袍剀紊铲剸剺剽剿劁劂札劈啪柴扳啦刘奭姥夼昫涓熙禅禹锡翔雁鹗刽刿弩柄蜻蛉劒劓劖劘劙澜篑赏矶釜晋甜薪逐劦熔纣虐赤囚劬劭労劵效劻劼劾峭艮勅勇励勍勐腊脖庞漫饲荡粥辄勖勗勘骄馁碌泮雇捐竹骑殊阱绩朴恳谨剿勧勩勯勰劢勋勷劝惩慰诫谏勹芡践阑匁庇拯粟扎袱裹饺匆遽匈匉匊匋匍匐茎匏匕妆痰脓蛹斋苑烤蹈塘羌熊阀螳螂疆碚竿纬荷茵邙魏匚匜匝匟扶稷匣匦拢匸匹耦匽匾匿卂叮疮禧轸堤棚迢钧炼卄卆遐卉瓷盲瓶当胱腱裸卋卌卍卐怯污贱鄙龌龊陋卓溪唐梯渔陈枣泥漳浔涧梨芬谯赡辕迦郑単驴弈洽鳌卛占筮卝卞卟吩啉屎翠厄卣卨卪卬卮榫袄玺绶钮蚤惧殆笃耸卲帘帙绕恤卼卽厂厎厓厔厖厗奚厘厍厜厝谅厕厤厥厪腻孢厮厰厳厣厹厺粕垢芜菁厼厾叁悟茸薯叄吵笄悌哺讥坫垄弧芯杠潜婴刍袁诘贪谍煽馈驳収岳缔灾贿骗叚叡吻拦蘑蜜诀燧玩砚筝椎蔺铜逗骊另觅叨唠谒杵姓喊嚷嚣咚咛塑寻恼憎擦只泣渗蝠叱吒咄咤喝籀黛舵舷叵叶铎懿昭穰苴辽叻叼吁堑嫖赌瞧爬众抒吅吆夥卺橡涤抱纵摩郡唁坠扇篮膀袜颈吋忾谘酬哭妓媛暗表缰迩妃羿絮蕃浑拐葵暮隅吔吖啶嗪戚吜啬噬咽吟哦咏吠吧唧嗒咐吪隽咀征燐苞茹钙哧吮吰吱嘎吲哚吴栋娇窟孟箫忠晗淞阖闾趼宇呐睛嘘拂捧疵熄竽笛糠吼吽呀吕韦蒙呃呆笨呇贡呉罄呋喃呎呏呔呠呡痴呣呤呦呧瑛眩扒晬淑姬瑜璇鹃呪呫哔嚅嗫呬呯呰呱呲咧噌钝呴呶呷呸呺呻哱咻啸噜吁坎坷逻呿咁咂咆哮咇咈咋蟹煦珅蔼咍咑咒诅咔哒嚓咾哝哩喱咗咠咡咢咣咥咦咨嗟询咩咪咫啮啮咭咮咱咲咳呛嗽咴啕咸咹咺呙喉咿婉恸悯赋矜绿茗蓝哂抢瞒哆嗦啰噻啾滨彗哋哌哎唷哟哏哐哞哢哤哪里哫啼喘哰哲萎蚌哳咩哽哿呗唅唆唈唉唎唏哗尧棣殇璜睿肃唔睇唕吣唞唣喳唪唬唰喏唲唳唵嘛唶唸唹唻唼唾唿啁啃鹦鹉啅埠栈榷祺铺鞅飙啊啍啎啐啓啕啖啗啜哑祈啢衔啤啥啫啱啲啵啺饥啽噶昆沁喁喂喆裙喈咙喋喌喎喑喒喓喔粗喙幛庆滋鹊喟喣喤喥喦喧骚喨喩梆吃葡萄喭驼挑吓碰枞瓣纯疱藻趟铬喵営喹喺喼喿嗀嗃嗄嗅嗈嗉嗊嗍嗐嗑嗔诟嗕嗖嗙嗛嗜痂癖嗝嗡嗤嗥嗨唢嗬嗯嗰嗲嗵叽嗷嗹嗾嗿嘀嘁嘂嘅惋嘈峪禾荫啀嘌嘏嘐嘒啯啧嘚唛嘞嘟囔嘣嘥嘦嘧嘬嘭这谑严敞馋松哓嘶嗥呒虾嘹嘻啴嘿噀噂噅噇噉噎噏噔噗噘噙噚咝噞噢噤蝉皿噩噫噭嗳噱哙噳嚏涌洒欲巫霏噷噼嚃嚄嚆抖哜尝嚔苏嚚嚜嚞嚟呖嚬嚭嚮嚯亸喾饬按竣苛嚵嘤啭冁呓膪谦囍囒囓囗囘萧酚飘溅谛囝溯眸纥銮鹘囟殉囡団囤囥囧囨囱囫囵囬囮囯囲図囶囷囸囹圄圉拟囻囿圀圂圃圊粹蠹赦圌垦圏滚鲱凿枘圕圛圜圞坯埂壤骸炕祠窑豚绅魠鲮鳖圧握圩圪垯圬圮圯炸岬幔毯祇窨菩溉圳圴圻圾坂坆沾坋坌舛壈昆垫墩椅坒坓坩埚坭坰坱坳坴坵坻坼杨挣涎帘垃垈垌垍垓垔垕垗垚垛垝垣垞垟垤垧垮垵垺垾垿埀畔埄埆埇埈埌殃隍埏埒埕埗埜垭埤埦埧埭埯埰埲埳埴埵埶绋埸培怖桩础辅埼埽堀诃侄庑堃堄摧磐贞韧砌堈堉垩堋堌堍堎垴堙堞堠礁堧堨舆堭堮蜓摘堲堳堽堿塁塄塈煤茔棵塍垲埘塓绸塕鸦沽虱塙冢塝缪塡坞埙塥塩塬塱场螨塼塽塾塿墀墁墈墉墐夯増毁墝墠墦渍钵墫墬堕墰墺墙橱壅壆壊壌壎壒榨蒜壔壕壖圹垆壜壝垅壡壬壭壱売壴壹壻壸寝壿夂夅夆変夊夌漱邑夓腕泄甥御骼夗夘夙衮瑙妊娠醣枭珊莺鹭戗幻魇夤蹀秘擂鸫姚宛闺屿庾挞拇賛蛤裨菠氅漓捞湄蚊霆鲨箐篆篷荆肆舅荔鲆巷惭骰辟邱镕镰阪漂烩鲵鲽鳄鸨胪鹏妒峨谭枰晏玑癸祝秤竺牡籁恢罡蝼蝎赐绒御梭夬夭砣榆怙枕夶夹馅奄崛葩谲奈贺祀赠奌奂奓奕䜣詝奘奜奠奡奣陶奨奁魁奫奬奰娲孩贬隶酥宄狡猾她姹嫣妁毡荼皋膻蝇嫔妄妍嫉媚娆妗趣妚妞妤碍妬娅妯娌妲妳妵妺姁姅姉姗姒姘姙姜姝姞姣姤姧姫姮娥姱姸姺姽婀娀诱慑胁娉婷娑娓娟娣娭娯娵娶娸娼婊婐婕婞婤婥溪孺婧婪婬婹婺婼婽媁媄媊媕媞媟媠媢媬媮妫媲媵媸媺媻媪眯媿嫄嫈袅嫏嫕妪嫘嫚嫜嫠嫡嫦嫩嫪毐嫫嫬嫰妩嫺娴嫽嫿妫嬃嬅嬉耍婵痴艳嬔嬖嬗嫱袅嫒嬢嬷嬦嬬嬭幼嬲嬴婶嬹嬾嬿孀娘孅娈孏曰癫屏孑孓雀孖斟篓谜摺孛矻鸠崮轲祜鸾孥邈毓棠膑孬孭孰孱孳孵泛罔衔孻孪宀宁冗拙株薇掣抚琪瓿榴谧弥宊濂祁瑕宍宏碁宓邸谳実潢町宥宧宨宬徵崎骏掖阙臊煮禽蚕宸豫寀寁寥寃檐庶寎暄碜寔寖寘寙寛寠苫寤肘洱滥蒗陕核寪弘绰螽宝擅疙瘩晷対檐専尃尅赎绌缭畴衅尌峙醌襟痲碧屁昊槌淘恵瀑牝畑莓缸羚觑蔻脏躁尔尓锐尗尙尜尟尢尥尨尪尬尭尰擒尲尶尴尸尹潽蠖蛾尻扣梢蚴鳍脬蹲屇屌蚵屐屃挪屖屘屙屛屝屡屣峦嶂岩舄屧屦屩屪屃屮戍驻钾崖嵛巅旮旯楂榄榉芋茱萸靛麓屴屹屺屼岀岊岌岍阜岑彭巩岒岝岢岚岣岧岨岫岱岵岷峁峇峋峒峓峞峠嵋峨峰峱岘峹峿崀崁崆祯崋崌崃岖昆崒崔嵬巍萤颢崚崞崟崠峥巆崤崦崧殂岽崱崳崴崶崿嵂嵇嵊泗嵌嵎嵒嵓岁嵙嵞嵡嵩嵫嵯嵴嵼嵾嵝崭崭晴嶋嶌嶒嶓嵚崂嶙嶝嶞峤嶡嶢峄嶨嶭嶮嶰嶲岙嵘巂巃巇巉岿巌巓巘巛滇芎巟巠弋回巣巤炊擘蜥蟒蛊觋巰蜀彦淖杏茂甫楞巻巽帼巿帛斐鲫蕊帑帔帗帚琉汶帟帡帣帨裙帯帰帷帹暆帏幄帮幋幌幏帻幙帮幞幠幡幢幦幨幩幪帱幭幯幰遥蹉跎馀庚鉴幵幷稚邃庀庁広庄庈庉笠庋跋庖牺庠庤庥鲸庬庱庳庴庵馨衢庹庿廃厩廆廋廌廎廏廐廑廒荫廖廛厮搏锣廞弛袤廥廧廨廪廱绵踵髓廸迫瓯邺廻廼廾廿躔弁皱弇弌弍弎弐弑吊诡憾荐弝弢弣弤弨弭弮弰弪霖繇焘斌旭溥骞弶弸弼弾彀彄别累纠强彔彖彘彟彟陌彤贻彧绘虹彪炳雕蔚鸥彰瘅彲彳彴仿彷徉徨彸彽踩敛旆徂徇徊渭畲铉裼従筌徘徙徜徕膳苏萌渐徬徭醺徯徳徴潘徻徼忀瘁胖燎怦悸颤扉犀澎湃砰恍惚绞隘忉惮挨饿忐忑忒忖応忝忞耿忡忪忭忮忱忸怩忻悠懑怏遏怔怗怚怛怞怼黍讶怫怭懦怱怲恍怵惕怸怹恁恂恇恉恌恏恒恓恔恘恚恛恝恞恟恠恣恧眄恪恫恬澹恰恿悀悁悃悄悆悊悐悒晦悚悛悜悝悤您悩悪悮悰悱凄恻德悴怅惘闷悻悾惄愫钟蒐惆惇惌惎惏惓惔惙惛耄惝疟浊恿惦德恽惴蠢惸拈愀愃愆愈愊愍愐愑愒愓愔愕恪氓蠢騃昵惬赧悫愬愮愯恺愼慁恿慅慆慇霭慉慊愠慝慥怄怂慬慱悭慴慵慷戚焚憀灼郁憃惫憋憍眺捏轼愦憔憖憙憧憬憨憪憭怃憯憷憸憹憺懃懅懆邀懊懋怿懔懐懞懠懤懥恹懫懮懰懱毖懵遁梁雍忏懽戁戄戆戉戋戕戛戝戛戠戡戢戣戤戥戦戬戭戯轰戱披菊牖戸戹戺戻卯戽锹扂楔扃扆扈扊杖牵绢铐镯赉扐搂搅烊盹瞌跟趸镲靶鼾払扗玫腮扛扞扠扡扢盔押扤扦扱罾揄绥鞍郤窾扻扼扽抃抆抈抉抌抏瞎抔缳缢擞抜拗択抨摔歉蹿牾抶抻搐泵菸拃拄拊髀抛拌脯拎拏拑擢秧沓曳挛迂拚拝拠拡拫拭拮踢拴拶拷攒拽掇芥橐簪摹疔挈瓢骥捺蹻挌挍挎挐拣挓挖掘浚挙揍聩挲挶挟挿捂捃捄捅捆捉捋胳膊揎捌捍捎躯蛛捗捘捙捜捥捩扪捭据捱捻捼捽掀掂抡臀膘掊掎掏掐笙掔掗掞棉芍掤搪阐掫掮掯揉掱掲掽掾揃揅揆搓揌诨揕揗揘揜揝揞揠揥揩揪揫橥遒麈揰揲揵揶揸背揺搆搉搊搋搌搎搔搕撼橹捣搘搠搡搢搣搤搥搦搧搨搬楦裢讪赸掏搰搲搳搴揾搷搽搾搿摀摁摂摃摎掴摒摓跤摙摛掼摞摠摦喉羯摭摮挚摰摲抠摴抟摷掺摽撂撃撅稻撊撋挦锏泼撕撙撚㧑挢撢掸撦撅撩撬撱朔揿蚍蜉挝捡擀掳闯擉缶觚擐擕擖擗擡擣擤澡腚擧擨擩擫擭摈拧撷擸撸擽擿攃摅撵攉攥攐攓撄搀撺每攩攫辔澄攮攰攲攴轶攷砭讦攽碘敁敃敇敉叙敎筏敔敕敖闰诲敜煌敧敪敳敹敺敻敿斁衽斄牒绉诌斉斎斓鹑谰驳鳢斒筲斛斝斞斠斡斢斨斫斮晾沂潟颖绛邵斲斸釳於琅斾斿旀旗旃旄涡旌旎旐旒旓旖旛旝旟旡旣浴旰獭魃旴时旻旼旽昀昃昄昇昉晰躲澈熹皎皓矾昑昕昜昝昞昡昤晖笋昦昨是昱昳昴昶昺昻晁蹇隧蔬髦晄晅晒晛晜晞晟晡晢晤晥曦晩萘莹顗晿暁暋暌暍暐暔暕煅旸暝暠暡曚暦暨暪朦胧昵暲殄冯暵暸暹暻暾曀晔昙曈曌曏曐暧曘曙曛叠昽曩骆曱甴肱曷牍禺锟曽沧耽朁朅朆杪栓夸竟粘绦朊膺朏朐朓朕朘朙瞄觐溘饔飧朠朢朣栅椆淀虱朩朮朰朱炆璋钰炽鹮朳槿朵朾朿杅杇杌陧欣钊湛漼楷瀍煜玟缨翱肇舜贽适逵杓杕杗杙荀蘅杝杞脩珓筊杰榔狍閦颦缅莞杲杳眇杴杶杸杻杼枋枌枒枓衾葄翘纾逋枙狸桠枟槁枲枳枴枵枷枸橼枹枻柁柂柃柅柈柊柎某柑橘柒柘柙柚柜柞栎柟柢柣柤柩柬柮柰柲橙柶柷柸柺査柿栃栄栒栔栘栝栟柏栩栫栭栱栲栳栴檀栵栻桀骜桁镁桄桉桋桎梏椹葚桓桔桕桜桟桫椤桭杯桯桲桴桷桹湘溟梃梊梍梐潼栀枧梜梠梡梣梧梩梱梲梳梴梵梹棁棃樱棐棑棕榈簑绷蓑枨棘棜棨棩棪棫棬棯棰棱棳棸棹椁棼碗椄苕椈椊椋椌椐椑椓椗検椤椪椰椳椴椵椷椸椽椿楀匾楅篪楋楍楎楗楘楙楛楝楟楠楢楥桢楩楪楫楬楮楯楰梅楸楹楻楽榀榃榊榎槺榕榖榘榛狉莽搒笞榠榡榤榥榦榧杩榭榰榱梿霰榼榾桤槊闩槎槑槔槖様槜槢槥椠槪槭椮槱槲槻槼槾樆樊樏樑樕樗樘樛樟樠樧樨権樲樴樵猢狲桦樻罍樾樿橁橄橆桡笥龠橕橚橛辆椭橤橧竖膈跨橾橿檩檃檇柽檍檎檑檖檗桧槚檠樯檨檫檬梼槟檴檵柠棹櫆櫌栉櫜椟櫡槠栌枥榇栊櫹棂茄櫽欀欂欃欐欑栾欙棂溴欨欬欱欵欶欷歔欸欹欻欼欿歁歃歆艎歈歊莳蝶歓歕歘歙歛歜欤歠蹦诠镶蹒跚升陟歩歮歯歰歳歴璞歺瞑歾殁夭殈殍殑殗殜殙殛殒殢殣殥殪殚僵殰殳荃殷殸殹蛟殻肴谤殴毈毉喂毎毑蕈毗毘毚茛邓毧毬毳毷毹毽毾毵牦氄氆靴氉氊氇氍氐聊氕氖気氘氙氚氛氜氝氡汹焊痉氤氲氥氦铝锌氪烃氩铵痤汪浒漉痘盂碾菖蒲蕹蛭螅氵冰氹氺氽烫氾氿渚汆汊汋汍汎汏汐汔汕褟汙汚汜蓠沼秽蔑汧汨汩汭汲汳汴堤汾沄沅沆瀣沇沈葆浸沦湎溺痼疴沌沍沏沐沔沕沘浜畹砾沚沢沬沭沮沰沱灢沴沷籽沺烹濡洄泂肛泅泆涌肓泐泑泒泓泔泖泙泚泜泝泠漩馍涛粼泞藓鳅泩泫泭泯铢泱泲洇洊泾琵琶荽蓟箔洌洎洏洑潄濯洙洚洟洢洣洧洨洩痢滔洫洮洳洴洵洸洹洺洼洿淌蜚浄浉浙赣渫浠浡浤浥淼瀚浬浭翩萍浯浰蜃淀苔蛞蝓蜇螵蛸煲鲤浃浼浽溦涂涊涐涑涒涔滂莅涘涙涪涫涬涮涴涶涷涿淄淅淆淊凄黯淓淙涟淜淝淟淠淢淤渌淦淩猥藿亵淬淮淯淰淳诣涞纺淸淹炖癯绮渇済渉渋渓渕涣渟渢滓渤澥渧渨渮渰渲渶渼湅湉湋湍湑湓湔黔湜湝浈湟湢湣湩湫湮麟湱湲湴涅満沩溍溎溏溛舐漭溠溤溧驯溮溱溲溳溵溷溻溼溽溾滁滃滉滊荥滏稽滕滘汇滝滫滮羼耷卤滹浐煎漈漊漎绎漕漖漘漙沤漜漪漾漥漦漯漰溆漶漷濞潀颍潎潏潕潗潚潝潞潠潦祉疡潲潵滗潸潺潾涠澁澂澃澉澌澍澐澒澔澙渑澣澦澧澨澫澬浍澰澴澶澼熏郁濆濇濈濉濊貊濔疣濜濠濩觞浚濮盥潍濲泺瀁滢渎渖瀌浏瀒瀔濒泸瀛潇潆瀡潴泷濑瀬弥潋瀳瀵瀹瀺瀼沣滠灉灋灒漓灖灏灞灠滦灥灨滟灪蜴灮烬獴灴灸灺炁炅鱿炗炘炙炤炫疽烙钎炯炰炱炲炴炷毁炻烀烋瘴鲳烓烔焙烜烝烳饪烺焃焄耆焌焐焓焗焜焞焠焢焮焯焱焼煁煃煆煇煊熠煍熬煐炜煕暖熏硷霾煚煝煟煠茕矸煨琐炀萁煳煺煻熀熅熇熉罴荧穹炝熘熛熜稔谙烁熤熨熯熰眶蚂颎熳熸熿燀烨燂燄盏燊燋燏燔隼燖焖燠燡灿燨燮燹燻燽燿爇爊爓爚爝爟爨蟾爯爰为爻丬爿牀牁牂牄牋窗牏牓窗釉牚腩蒡虻牠虽蛎牣牤牮牯牲牳牴牷牸牼绊牿靬犂犄犆犇犉犍犎犒荦犗犛犟犠犨犩犪犮犰狳犴犵犺狁甩狃狆狎狒獾狘狙黠狨狩狫狴狷狺狻豕狈蜘猁猇猈猊猋猓猖獗猗猘狰狞犸猞猟獕猭猱猲猳猷猸猹猺玃獀獃獉獍獏獐獒毙獙獚獜獝獞獠獢獣獧鼇蹊狯猃獬豸狝獯鬻獳犷猕猡玁菟玅玆玈珉糁禛郅玍玎玓瓅玔玕玖玗玘玞玠玡玢玤玥玦珏瑰玭玳瑁玶玷玹玼珂珇珈瑚珌馐馔珔珖珙珛珞珡珣珥珧珩珪佩珶珷珺珽琀琁陨玡琇琖琚琠琤琦琨琫琬琭琮琯琰琱琲琅琴珐珲瑀瑂瑄瑉玮瑑瑔瑗瑢瑭瑱瑲瑳瑽瑾瑿璀璨璁璅璆璈琏璊璐璘璚璝璟璠璡璥瑷璩璪璫璯璲玙璸璺璿瓀璎瓖瓘瓒瓛脐瓞瓠瓤瓧瓩瓮瓰瓱瓴瓸瓻瓼甀甁甃甄甇甋甍甎甏甑甒甓甔瓮甖甗饴蔗甙诧钜粱盎锈团甡褥産甪甬甭甮宁铠甹甽甾甿畀畁畇畈畊畋畎畓畚畛畟鄂畤畦畧荻畯畳畵畷畸畽畾疃叠疋疍疎箪疐疒疕疘疝疢疥疧疳疶疿痁痄痊痌痍痏痐痒痔痗瘢痚痠痡痣痦痩痭痯痱痳痵痻痿瘀痖瘃瘈瘉瘊瘌瘏瘐痪瘕瘖瘙瘚瘛疭瘜瘝瘗瘠瘥瘨瘭瘆瘯瘰疬瘳疠瘵瘸瘺瘘瘼癃痨痫癈癎癐癔癙癜癠疖症癞蟆癪瘿痈発踔绀蔫酵皙砬砒翎翳蔹钨镴皑鹎驹暨粤褶皀皁荚皃镈皈皌皋皒朱皕皖皘皜皝皞皤皦皨皪皫皭糙绽皴皲皻皽盅盋碗盍盚盝踞盦盩秋千盬盭眦睁瞤盯盱眙裰盵盻睐眂眅眈眊県眑眕眚眛眞眢眣眭眳眴眵眹瞓眽郛睃睅睆睊睍睎困睒睖睙睟睠睢睥睪睾睯睽睾眯瞈瞋瞍逛瞏瞕瞖眍䁖瞟瞠瞢瞫瞭瞳瞵瞷瞹瞽阇瞿眬矉矍铄矔矗矙瞩矞矟矠矣矧矬矫矰矱硪碇磙罅舫阡、矼矽礓砃砅砆砉砍砑砕砝砟砠砢砦砧砩砫砮砳艏砵砹砼硇硌硍硎硏硐硒硜硖砗磲茚钡硭硻硾碃碉碏碣碓碔碞碡碪碫碬砀碯碲砜碻礴磈磉磎硙磔磕磖磛磟磠磡磤磥蹭磪磬磴磵磹磻硗礀硚礅礌礐礚礜礞礤礧礮砻礲礵礽礿祂祄祅祆禳祊祍祏祓祔祕祗祘祛祧祫祲祻祼饵脔锢禂禇禋祦禔祎隋禖禘禚禜禝禠祃禢禤禥禨禫祢禴禸秆秈秊闱飒秋秏秕笈蘵赁秠秣秪秫秬秭秷秸稊稌稍稑稗稙稛稞稬秸稲稹稼颡稿穂穄穇穈穉穋稣贮穏穜穟秾穑穣穤穧穨穭穮穵穸窿阒窀窂窅窆窈窕窊窋窌窒窗窔窞窣窬黩蹙窑窳窴窵窭窸窗竁竃竈竑竜并竦竖篦篾笆鲛竾笉笊笎笏笐靥笓笤箓笪笫笭笮笰笱笲笳笵笸笻筀筅筇筈筎筑筘筠筤筥筦笕筒筭箸筰筱筳筴宴筸箂个箊箎箑箒箘箙箛箜篌箝箠箬镞箯箴箾篁筼筜篘篙篚篛篜篝篟篠篡篢篥篧篨篭篰篲筚篴篶篹篼箦簁簃簆簉簋簌簏簜簟簠簥簦簨簬簰簸簻籊藤籒籓籔签籚篯箨籣籥籧笾簖籫籯芾麴籵籸籹籼粁秕粋粑粔粝粛粞粢粧粨粲粳稗粻粽辟粿糅糆糈糌糍糒糔萼糗蛆蹋糢糨糬粽糯糱籴粜糸糺紃蹼鲣霉纡纨绔纫闽襻紑纰纮锭鸢鹞纴紞紟扎紩紬绂绁纻紽紾绐絁絃絅経絍绗絏缡褵絓絖絘絜绚絣螯絪絫聒絰絵绝絺絻絿綀绡綅绠绨绣綌綍綎捆綖綘継続缎绻綦綪线綮綯绾罟蝽綷縩绺绫緁绲緅緆缁绯緌緎総緑绱緖缃缄缂绵缗緤褓缌纂緪緰缑缈缏缇縁縃縄萦缙缒縏缣縕缞縚缜缟缛縠縡縢縦绦縯縰骋缧縳纤缦絷缥縻衙縿繄缫繈繊繋繐缯繖繘繙繠缋繣繨缰缲繸繻缱纁纆纇缬缵纩纑纕缵纙纚纛缾罃罆坛罋罂罎罏罖罘罛罝罠罣罥罦罨罫罭锾罳罶罹罻罽罿羂羃羇芈蕉51鸵羑羖羌羜羝羢羣羟羧羭羮羰羱羵羶羸藜鲐翀翃翅翊翌翏翕翛翟翡翣翥翦跹翪翫翚翮翯翱翽翾翿板饕鸹锨耋耇耎耏专耒耜耔耞耡耤耨耩耪耧耰鬓耵聍聃聆聎聝聡聦聱聴聂聼阈聿肄肏肐肕腋肙肜肟肧胛肫肬肭肰肴肵肸肼胊胍胏胑胔胗胙胝胠铨胤胦胩胬胭胯胰胲胴胹胻胼胾脇脘脝脞脡脣脤脥脧脰脲脳腆腊腌臜腍腒腓胨腜腠脶腥腧腬腯踝蹬镣腴腶蠕诽膂腽嗉膇膋膔腘膗膙膟黐膣膦膫膰膴膵膷脍臃臄臇臈臌臐臑臓膘臖臙臛臝臞臧蓐诩臽臾臿舀舁鳑鲏舋舎舔舗馆舝舠舡舢舨舭舲舳舴舸舺艁艄艅艉艋艑艕艖艗艘艚艜艟艣舣艨艩舻艬艭荏艴艳艸艹艻艿芃芄芊萰陂藭芏芔芘芚蕙芟芣芤茉芧芨芩芪芮芰鲢芴芷芸荛豢芼芿苄苒苘苙苜蓿苠苡苣荬苤苎苪镑苶苹苺苻苾茀茁范蠡萣茆茇茈茌茍茖茞茠茢茥茦菰茭茯茳藨茷藘茼荁荄荅荇荈菅蜢鸮荍荑荘豆荵荸荠莆莒莔莕莘莙莚莛莜莝莦莨菪莩莪莭莰莿菀菆菉菎菏菐菑菓菔芲菘菝菡菢菣菥蓂菧菫毂蓥菶菷菹醢菺菻菼菾萅萆苌萋萏萐萑萜萩萱萴莴扁萻葇葍葎葑荭葖葙葠葥苇葧葭药葳葴葶葸葹葽蒄蒎莼茏薹莅蒟蒻蒢蒦蒨蒭藁蒯蒱鉾蒴蒹蒺蒽荪蓁蓆蓇蓊蓌蓍蓏蓓蓖蓧蓪蓫荜跣藕苁蓰蓱莼蓷蓺蓼蔀蔂蔃蔆蔇蔉蔊蔋蔌蔎蔕蔘蔙蒌蔟锷蒋雯茑蔯蔳麻蔵蔸蔾荨蒇蕋蕍荞蕐蕑芸莸蕖蕗蕝蕞蕠蕡蒉蕣蕤蕨蕳蓣蕸蕺蕻薀薁薃薅薆荟薉芗薏薐蔷薖薘剃谔钗薜薠薢薤薧薨薫薬薳薶薷薸薽薾薿藄藇藋荩藐藙藚藟藦藳藴苈藷藾蘀蘁蕲苹蘗蘘蘝蘤蘧蘩蘸蘼虀虆虍蟠虒虓虖虡虣虥虩虬虰蛵蛇虷鳟虺虼蚆蚈蚋蚓蚔蚖蚘蚜蚡蚣蚧蚨蚩蚪蚯蚰蜒蚱蚳蚶蚹蚺蚻蚿蛀蛁蛄蛅蝮蛌蛍蛐蟮蛑蛓蛔蛘蛚蛜蛡蛣蜊蛩蛱蜕螫蜅蚬蜈蝣蜋蜍蜎蜑蠊蜛饯蜞蜣蜨蜩蜮蜱蜷蜺蜾蜿蝀蝃蝋蝌蝍蝎蝏蝗蝘蝙蝝鲼蝡蝤蝥猿蝰虻蝲蝴蝻螃蠏蛳螉螋螒螓螗螘螙螚蟥螟螣螥螬螭䗖螾螀蟀蟅蝈蟊蟋蟑蟓蟛蟜蟟蟢虮蟨蟪蟭蛲蟳蛏蟷蟺蟿蠁蠂蠃虿蠋蛴蠓蚝蠗蠙蠚蠛蠜蠧蟏蠩蜂蠮蠰蠲蠵蠸蠼蠽衁衄衄衇衈衉衋衎衒同衖胡衞裳钩衭衲衵衹衺衿袈裟袗袚袟袢袪袮袲袴袷袺袼褙袽裀裉袅裋夹裍裎裒裛裯裱裲裴裾褀褂褉褊裈褎褐褒褓褔褕袆褚褡褢褦褧褪褫袅褯褰褱裆褛褽褾襁褒襆裥襉襋襌襏襚襛襜裣襞襡襢褴襦襫襬襭襮襕襶襼襽襾覂覃覅霸覉覊覌覗觇覚覜觍觎覧覩觊觏覰観觌觔觕觖觜觽觝觡酲觩觫觭觱觳觯觷觼觾觿言赅讣訇訏訑訒诂讬訧訬訳訹证訾詀詅诋毁詈詊讵詑诒诐詗诎察詨诜詶詸詹詻诙诖誂誃诔锄诓誋诳诶悖誙诮诰誧説読誯谇訚谄谆諆諌诤诹诼諕谂谀諝谝諟喧谥諴諵谌谖誊謆謇歌謍謏謑谡谥謡謦謪谪讴謷謼谩哗譅譆譈譊讹譒撰谮鑫譞噪譩谵譬譱譲谴譸譹谫讅讆詟䜩雠讐谗谶讙谠讟谽豁豉豇岂豊豋豌豏豔豞豖豗豜豝豣豦豨豭豱豳豵豶豷豺豻貅貆狸猊貔貘䝙貜貤餍贳餸贶贲赂賏赊赇赒賝赓赕賨赍斗賮賵賸赚赙赜赟贉赆赑贕赝赬赭赱赳迄趁趂趄趐趑趒趔趡趦趫趮趯趱趴趵趷趹趺趿跁跂跅跆踬跄跐跕跖跗跙跛跦跧跩跫跬跮跱跲跴跺跼跽踅踆踈踉踊踒踖踘踜踟躇蹰踠踡踣踤踥踦踧跷踫踮逾踱踊踶踹踺踼踽躞蹁蹂躏蹎蹐蹓蹔跸蹚蹜蹝迹蹠蹡蹢跶蹧蹩蹪蹯鞠蹽躃躄躅踌跻躐踯跞躘躙躗躝躠蹑躜躧躩躭躰躬躶軃軆辊軏轫軘軜軝腭転軥軨軭軱轱辘軷轵轺軽軿輀輂辇辂辁輈挽輗辄辎辋輠輤輬輭輮辏輴輵輶輹輼辗辒轇轏轑轒辚轕轖轗轘轙轝轞轹轳罪辣辞辵辶辺込辿迅迋迍麿迓迣迤逦迥迨迮迸迺迻迿逄逅逌逍逑逓迳逖逡逭逯逴逶逹遄遅侦遘遛遝遢遨遫遯遰遴绕遹遻邂邅邉邋邎邕邗邘邛邠邢邧邨邯郸邰邲邳邴邶邷邽邾邿郃郄郇郈郔郕郗郙郚郜郝郞郏郠郢郪郫郯郰郲郳郴郷郹郾郿鄀鄄郓鄇鄈鄋鄍鄎鄏鄐鄑邹邬鄕郧鄗鄘鄚鄜鄞鄠鄢鄣鄤鄦鄩鄫鄬鄮鄯鄱郐鄷鄹邝鄻鄾鄿酃酅酆酇郦酊酋酎酏酐酣酔酕醄酖酗酞酡酢酤酩酴酹酺醁醅醆醊醍醐醑醓醖醝酝醡醤醨醪醭醯醰酦醲醴醵醸醹醼醽醾釂酾酽釆釈鲈镏阊钆钇钌钯钋鼢鼹钐钏釪釬釭釱钍釸钕钫鈃钭鈆鈇钚鈊鈌钤钣鈒鈤钬钪鈬铌铈钶铛钹铍钸钿鉄鉆铊铇鉌铋鉏铂钷铆钵鉥钲鉨钼钽鉱鉲鉶铰铒鉼铪銍銎铣銕镂铫铦铑铷銤铱铟銧铥铕铯銭銰焊銶锑锉汞鋂锒鋆鋈鋊铤鋍铗鋐鋑鋕鋘鋙锊锓锔锇铓鋭铖锆锂铽鋳鋹鋺鉴镚钎錀锞锖锫锩錍铔锕錔锱铮锛錞锬锜錤錩錬録铼錼锝钔锴鍉镀鍏鍐铡鍚锻锽锸锲锘鍫鍭鍱鍴锶鍹锗针锺锿镅鎉鎋鎌鎍鎏鎒鎓鎗镉鎚鎞镃鎤铩锼鎭鎯镒镍鎴镓鎸鎹镎镟鏊镆镠镝鏖铿锵鏚镗镘镛鏠鏦錾镤鏸镪鏻鏽鏾铙鐄鐇鐏铹镦镡鐗馗镫镢镨鐡锎镄鐩镌鐬鐱镭鐶鐻鐽镱鑀鑅镔鑐鑕鑚鑛鑢鑤镥鑪镧鑯鑱鑴鑵镊镢钃镻闫闬闶闳閒闵閗閟阂関合閤哄阆閲阉閺阎阏阍阌暗闉阕阗闑闒闿闘闚阚闟闠闤闼阞阢阤阨阬阯阹阼阽陁陑陔陛陜陡陥陬骘陴険陼陾阴隃隈隒隗隞隠隣隤隩隮隰颧隳隷隹雂雈雉雊雎雑雒雗雘雚雝雟雩雰雱驿霂霅霈霊沾霒霓霙霝霢霣霤霨霩霪霫霮靁叇叆靑靓靣腼靪靮靰靳靷靸靺靼靿鞀鞃鞄鞍鞗鞙鞚鞝鞞鞡鞣鞨鞫鞬鞮鞶鞹鞾鞑韅鞯驮韍韎韔韖韘韝韫韡韣韭韭韱韹韺頀刮頄顸顼頍颀颃颁頖頞頠頫頬颅頯頲颕頼悴顋顑颙颛颜顕顚顜颟顣颥颞飐飑台飓颸飏飖颽颾颿飀飂飚飌翻飡飣饲飥饨饫飮飧飶餀餂饸饹餇餈饽哺馂餖餗餚馄馃餟餠餤餧餩餪餫糊餮糇餲饧馎糕饩馈馊馌馒饇馑馓膳饎饐饘饟馕馘馥馝馡馣骝骡馵馹駃駄駅駆駉駋驽駓驵駗骀驸駜骂骈駪駬骃駴骎駹駽駾騂騄骓騆騉騋骒骐麟騑騒験騕骛騠騢騣騤騧骧騵驺骟騺蓦骖骠骢驆驈骅驌骁驎骣驒驔驖驙驦驩驫骺鲠骫骭肮骱骴骶骷髅骾髁髂髄髆膀髇髑髌髋髙髝髞髟髡髣髧髪髫髭髯髲髳髹髺髽髾鬁鬃鬅鬈鬋鬎鬏鬐鬑鬒鬖鬗鬘鬙鬠鬣斗鬫鬬阄鬯鬰鬲鬵鬷魆魈魊魋魍魉魑魖鳔魛魟魣魦魨魬鲂魵魸鮀鲅鮆鲧鲇鲍鲋鮓鲒鲕鮟鱇鮠鮦鮨鲔鲑鮶鮸鮿鲧鯄鯆鲩鯈鲻鯕鲭鲞鯙鯠鲲鯥鲰鲶鳀鯸鳊鲗䲠鹣鳇鰋鳄鳆鰕鰛鰜鲥鰤鳏鰦鳎鳐鳁鳓鰶鲦鲡鰼鰽鱀鱄鳙鱆鳕鱎鱐鳝鳝鳜鲟鲎鱠鳣鱨鲚鱮鱲鱵鱻鲅鳦凫鳯鳲鳷鳻鴂鴃鴄鸩鴈鴎鸰鴔鴗鸳鸯鸲鹆鸱鴠鴢鸪鴥鸸鹋鴳鸻鴷鴽鵀鵁鸺鹁鵖鵙鹈鹕鹅鵟鵩鹌鵫鵵鵷鵻鹍鶂鶊鶏鶒鹙鶗鶡鶤鶦鶬鶱鹟鶵鶸鶹鹡鶿鹚鷁鷃鷄鷇䴘䴘鷊鷏鹧鷕鹥鸷鷞鷟鸶鹪鹩鷩鷫鷭鹇鹇鸴鷾䴙鸂鸇䴙鸏鸑鸒鸓鸬鹳鸜鹂鹸咸鹾麀麂麃麄麇麋麌麐麑麒麚麛麝麤麸面麫麮麯麰麺麾黁黈黉黢黒黓黕黙黝黟黥黦黧黮黰黱黪黶黹黻黼黾鼋鼂鼃鼅鼈鼍鼏鼐鼒冬鼖鼙鼚鼛鼡鼩鼱鼪鼫鼯鼷鼽齁齆齇齈齉齌赍齑龀齕齗龅齚龇齞龃龉龆齢出齧齩齮齯齰齱齵齾厐龑龒龚龖龘龝龡龢龤' + +traditional_characters = '制咖片型超聲盤鑒定仔點他命書歌粉巾字帳恤手指記憶棒形轉彎溝光○〇㐄㐅㐆㐌㐖毒㐜㐡㐤㐰㐺㑇㑳㒳㒸㔾㗂㗎㝵㞎㞙㞞㠯㢲㢴㤅㥁㥯㨗㫺㬎㮎㮚㮸㲋㲱㲾㳮㵎㵪㶸㷖㷭㹢㹴犬㺢狓㺵㼝㽮㿝䍃䔢䖟䖸䗈䗥䗪䝓䠶䥯䦉䯝䰾魚䲔䳗䳘䵹鼄䶑一對應映射丁不識下兒子做二休世丘之貉並中台原則串為甚謂乾淨了百事無成八變五十些人得道雞升天代如併來去個國政策勁幽靈在歐洲遊蕩接樣蘿蔔坑側化傳價元論醇共再准刀兩斷切分耕耘收穫錢貨物向看舊就緒險刻千金動勞永逸匙零夜半卡通回復返影蹤反常態口咬氣句話同吐快吹周味呼諾嗚品紅鍋哄而散起唱和問三知生熟團漆黑火糟堆場空塊麵塌糊塗塵染壁廂夔已足多情露水大早到晚夫妻當關萬莫開失古恨套所料既往孔見提師要家主審寸陰難買鬥牛小撮部陣局展身層巴掌帆風順席地帶過年計於春頭載四季期被蛇怕井繩度願式份彈頃深前律徑心意念差愁孤行俱全房廳交遮打技長把抓死拿眼淚鼻涕鑰鎖折段抿拍即合掃排掬揮撥擁上入擊洞擲攬改故轍敗文值名斑方面旁族日秋餐隔雅里終父旦時晌會霎間晃暴寒曝更月望垠際朝夕本正經利杯羹東西板枝獨秀根筋桿進條龍服務概模次函數又性程總付步腳印趨登毛拔呵氧氮碳決雌雄波未平派謊言流清楚白準溜煙潭有獲聞是處降琴鶴甲病發可拾沙目然瞭直以相眨穿睹瞥瞬矢的解石鳥神教秉虔誠秘種窩蜂窮竅笑置筆苟勾銷抹殺煞等獎箍節吃箭仇雙鵰詩籌籮筐系列紙級士官統絲毫掛維網盡線微吭響股腦胎脈承腔臂力致效資源址器舉功投般說講規貿易葉障著慎滿皆輸號木電池衣傾鐘高低視仁覺醒覽遺角銀幣觸潰九鼎蔽抄出駟馬追重語破貧洗貫走路安蹴至幾蹶振躍役膽汗較輩輪辭贊退六連遍遞邊針血錘音錯門思閃真倒項栽霧類保護川先驚乍體鬨鱗爪鳴滴泡鄰域黨專鼓作齊炒丑烯亥克內酯冬加奴卯肝炎基尺梁街褲鎬客寵庭巳汝昌烷玲磊糖肇酉醛啷青縣韙良香骨鯛丂七集河市弦喜嘴張舌堵區工業姊妹星架構巧彩扭歪拼湊餘熱曜武州爺浮屠美鄉老階樹葷素碎落能魄鰓鰻珠丄丅丆万俟丈尚摸母娘量管群亞虎必我堂令申件裝伏位博俠義界表女墟臺戲臭皮匠勝諸葛亮賽頂倍催請運算包立叉戟離疫苗土史志演圍揭瓦曬夷姑婆帝村寶爛尖杉鹼屜桌山岔島由紀峽壩庫鎮廢從德後拗湯治旬食明昧曹朋友框欄極權冪曲歸依貓民氟硼氯磷鐵江侗自旅法司洋浦梅園溫暖灣焦班幸用田略番疊皇炮捶硝苯酸腺苷稜草鏡穗跳遠索錦綱聚氰胺聯店胚膲愛色堇紫羅蘭芝茶飯菱雲蟲藏藩亂叛蘇親債凳學座恐戀柱測肌腹衩錐係貂企烏跪叩軍車農題迭都甘油屯奏鍵短阿姨陪姐隻顧茅廬槽駕魂鮮鹿頁其菜單乘任供勢午齒漢組織吊調瀉唇坡城報墳外夸將尉建築岸崗公床揚新劍昇杭林栗校樓標款汽社浣海商館劇院鋼華港機械廣媒環球融第醫科證券綜財樂育游漲猶嶺疏癮瞼確兵領導繳肢膛船艾瑟爾蒼蔡虞傚衫覆訪訴課諭議軌述野鉤限敵鞋頜頷顎饒首齦站例修凡劃垂屆屬崽頦廚拜挫擺放旋削棋榻檻禮沉注滑營獄畫确儀聘花葬詔員跌轄週達酒錨閘陷陸雨雪飛威丌于丹久乏予理評產亢卑亦乎舞己悲矩圓詞害誌但住佞佳便俗信票案幅翁倦倫假偏倚斜虧鬼敲停備傷脾胃僅此像儉匱免宜穴焉戴兼容許凍伯仲負彼晝皂軒輊實刊划顛衛戰哥比省非好黃飾別拘束掩奶睬選擇搖擾煩苦枚寫協厭及格受歡迎約只估侵犯割狀告或缺抗拒挽撤救藥喻磨滅端倪少逆逾越避靠適吉譽吝玉含延咎歹聽啻淵善謀均勻堪忍夠太惹妙妥妨孕症孝術室完納推冠積宣疑辯慄碴稱屈撓屑干涉衡待很忙惡忿怎麼怠急恥恭息悅惑惜惟想愉愧怍慌憤啟懂懈懷材才緊招認扣抵拉捨也罷插揣冒搭撞南牆擴核支攻敢雷攀敬裡嗎需景智暇曾罪遇朽枉止況競爭辱求癒渝溶濟左右袒困補爽特寂寞示弱找謝畏強疾徐痛癢冤符眠睦瞅董何厚云措活疲羞者輕玻璃祥兆禁移稂莠穩佛換答簡結果盟絕縷途給談否羈翼耐肖脛毋寧興舒若菲萊痕跡窠臼虛衰臉兔撒鷹棺範該詳諱抬泰讓鬚眉象眾貲賬費灰賴奇慮訓輟辨菽麥辛近送透逞徒速續逮捕遂遑違遜斧鉞艱醉鏽隨觀棄顯飽脂肪使丏丐幫丒且慢末丕替桃宗王尊涼爵各圖屋脊糧署錄壇吾祿職胄襲君廈丗北壑桐疹損逢陵鷸丙寅戌氨腈唑綸辰酮脫氫酶醚丞丟現掉紗帽弄扯砲碗丠両丣坐存激肩臻蒂蓮悖序驅丨丩丫挺杈髻鬟細介俄伊犁京尼布訂普渡央委監察檢查劑圈設警隊斯督剩震境航舶革防托播促質版蠑螈鋒研藝歷殘消頻譜精密製造陲郵候埔堅壓壢凹匯執府究邦俘攝寮彬狼嶽肺腫庸英訊診埋粒胞括控碼韓暑槍樞砥澳哇牟壽甸鑽探篇簽綴縫繼耳肯照婦埃懸璧軸櫃檯辣擱淺邪跑纖阮陽私囊魔丮丰姿采丱燒丳丵丶丷丸參寨朗桂瑞砂衷霞貌鳳僕艦因嫌宰峰幹絡牌持旨祭禱簿編罰賓辦丼丿乀乂乃乄仰慕盛曠留考驗闊乆乇么醜麼乊湖燃乑乒乓乕乖僻忤戾离謬迕乗危肥劫除隙浪婿乙炔腸酰吡咯鹽乚乛乜嘢卿玄宮尾狐龜塔嶷兄弟泉章霄釘耙乞扎哀憐恕討乢乣乤乥乧乨乩童乪乫乭乳暈汁液瑤漿牙癌突竇罩腐膠豬酪蛋糕菌瘤乴乵乶乷乸乹乺乼乾俸冰嘉噦嚎坤媽屍壘旱枯涸俐渴潮澀煸豆燥爹瘦癟癬瞪袋脆薑貝隆餾乿亀亁叫咕攘扔搞男砸竄蓬麻亃亄亅卻亇遲典今臨繁累卵奉婚聰躬巨與遷添裂副宿歲怪噁尕崙愣杆硅硫鈦鈾錳芑雜異鈉砷胂磺琥珀艙棍簧胡茬盜浩盆販郎腿亍洪亐互欠助勉惠操斥諉繫戶譯亓墓碑刑鈴卅渠繽紛斗米旗憲釩燈徽瘟祖拳福穀豐臟腑綁肉醃苓蘊橋鋪霸顏鬧判噴岡底蛙陘礦亖亙亜罕們娜桑那努哈喀弗烈曼松森杜氏盃奧琛敦戊穆聖裔彙薛孫亟亡佚虜羊牢奮釋卷卸契媾感額睫纏誼趾塞擠紐阻還配馳莊亨洛祚亪享津滬畿郊慈菴枇杷膏亭閣鋥麗亳亶亹誅初責翻瘋偶傑叢稠妖拖寰居吸授慧蝸吞壯魅狗矛盾益渣患憂稀描猿夢暫涯畜禍緣沸搜引擎臣橫紜誰混援蒸獸獅稅剖亻亼亽亾什獻剎邡麽仂仃仄仆富怨仈仉畢昔晨殼紹仍仏仒仕宦仗欺恃腰嘆歎炬梓訖施仙后瓊逝仚仝仞仟悔仡佬償填泊拓撲簇羔購頓欽佩髮棻閫馭養億儆尤藉幀賑凌敘帖李柔剛沃眥睚戒訛取饗讀仨仫仮著泳臥躺韶夏裁仳仵唯賢憑釣誕仿似宋彿諷伀碩盼鵝伄儅伈伉儷柯始娃邁戈坦堡帕茨薩廟瑪莉莎藤霍姆伋伍奢胥廷芳豪伎倆侍汛勒希羲雛伐憩整謨閑閒伕伙伴頤伜伝伢叔恆茲恩翰伱伲侶伶俜悧鼬伸懶縮喇叭伹伺伻伽倻輻伾佀佃佇佈喬妮墨佉盧佌貸劣廉昂檔濃矮傘窪緩耗胸谷迷擋率齲宅沫舍療佐貳佑佔優據鏵嘗呢須魯曉佗佘余坪寺瓜銃僧蒙芒陀龕哼嘔坊姦孽弊揖祟繭縛誓賊佝僂瞀佟你奪趕佡佢佣佤佧賈佪佫佯佰佱潔績釀餚佴捲佶佷佸佹佺佻佼佽佾具喚窘壞娛怒慨硬習慣聾膨脹蔓駭貴痺侀侁侂侃侄侅鴻燕侇侈糜靡侉侌妾侏儒倉鼠侐侑侔侖侘侚鏈侜偎傍鈷循柳葫蘆附価侮罵蔑侯岩截蝕侷貼壺嬛宴捷攜桶箋酌俁狹膝狄俅俉俊俏俎俑俓俔諺俚俛黎健呈固墒增守康箱濕祐鏢鑣槓盒靖膜齡俞豹獵噪孚封札筒託衍鴿剪撰稿煉廠禊練繕葺俯瞰撐衝俲俳俴俵俶俷俺俻俾倀倂倅儲卒惶敷猝逃頡蓄崇隱倌倏忽刺蠟燭噍嚼坍扁抽斃蔥楣灌灶糞背藪賣賠閉霉騰倓倔倖倘倜儻倝借箸挹澆閱倡狂倢倣値倥傯倨傲倩匡嗣沖柝珍倬倭寇猩倮倶倷倹勤讚偁偃充偽吏嗓寐惺扮拱芫茜藉虢鈔偈偉晶偌宕距析濾殿疼癱註頗偓偕鴨歇滯偝偟偢忘怡旺偨偩偪偫偭偯偰偱偲偵緝蹄偷減惰漏窺竊偸偺迹傀儡傅傈僳傌籬傎奎琳迪叟芭傒傔傕傖悉荒傜傞傢傣芽逼傭婢傮睨寄檄誦謠頌傴擔辜弓慘蒿悼疤傺傻屄臆巢洩篋羨蓋軋頹傿儸僄僇僉僊働僎僑僔僖僚僝僞僣僤僥僦猴僨僩僬僭僮僯僰僱僵殖籤靜僾僿征隴儁儂儃儇儈朴薄儊儋儌儍儐儓儔儕儗儘儜儞儤儦儩汰哉寡渥裕酷儭儱罐儳儵儹儺儼儽兀臬臲鷲允勛勳宙宵帥憝彞諧嫂鬩暢沛溢盈飢赫兇悍狠猛頑愚妣斬秦遣鞭耀敏榮槃澤爆碟磁禿纜輝霽鹵朵婁孜烽醬勃汀箕裘鉗耶懞蕾徹兌軟遭黜兎児韻媳爸兕觥兗兙兛兜售鍪肚兝兞兟兡兢兣樽殮涅睡稟籍贅泌啡肽奸幕涵澇熵疚眷稃襯訌赴煥椒殲植跏沒試誤猜棲窗肋袖頰兪卦撇鬍岐廓轎疸楓茴瓏廁秩募勺噸寓斤曆畝迫筷釐最淫螺韜兮寬匪篩襄贏軛複兲詐刃堰戎痞蟻餉它冀鑄冂冃円冇冉冊嫁厲礪竭醮冏牧冑冓冔冕冖冗冘冞冢窄抑誣冥冫烘菇蟄冷凝坨橇淇淋炭餅磚磧窖醋雕雹霜冱冶爐艷嘲峻灘淡漠煖颼飲冼冽凃凄愴梗凅凇凈凊凋敝濛凔凜遵汞脢凞几凢処凰凱凵凶焰凸摺刷紋預喪嘍奔巡榜殯芙蓉租籠輯鞘萃凼鋸鑊刁蠻刂娩崩批拆攤掰櫱驟歧顆秒袂贓勿囑忌磋琢膚刈羽刎訟戮舂槳艇刓刖霹靂刜創犢刡恙墅幟筵緻刦刧刨昏默攸尿慾薰潤薰圭刪刮痧鏟刱刲刳刴刵踏磅戳柏槐繡芹莧蝟舟銘鵠鶩刼剁剃辮剄剉履鉛剋剌姻咽哨廊掠桅沿召瞻翅趙卜渺茫郭剒剔剕瀝剚愎毅訥纔剜剝啄採剞剟剡剣剤綵剮腎駛黏剰袍剴紊剷剸剺剽剿劁劂劄劈啪柴扳啦劉奭姥夼昫涓熙禪禹錫翔雁鶚劊劌弩柄蜻蛉劒劓劖劘劙瀾簣賞磯釜晉甜薪逐劦熔紂虐赤囚劬劭労劵効劻劼劾峭艮勅勇勵勍勐臘脖龐漫飼盪粥輒勖勗勘驕餒碌泮雇捐竹騎殊阱勣樸懇謹勦勧勩勯勰勱勲勷勸懲慰誡諫勹芡踐闌匁庇拯粟紮袱裹餃匆遽匈匉匊匋匍匐莖匏匕妝痰膿蛹齋苑烤蹈塘羌熊閥螳螂疆碚竿緯荷茵邙魏匚匜匝匟扶稷匣匭攏匸匹耦匽匾匿卂叮瘡禧軫堤棚迢鈞鍊卄卆遐卉瓷盲瓶噹胱腱裸卋卌卍卐怯污賤鄙齷齪陋卓溪唐梯漁陳棗泥漳潯澗梨芬譙贍轅迦鄭単驢弈洽鰲卛占筮卝卞卟吩啉屎翠厄卣卨卪卬卮榫襖璽綬鈕蚤懼殆篤聳卲帘帙繞卹卼卽厂厎厓厔厖厗奚厘厙厜厝諒厠厤厥厪膩孢厮厰厳厴厹厺粕垢蕪菁厼厾叁悟茸薯叄吵笄悌哺譏坫壟弧芯杠潛嬰芻袁詰貪諜煽饋駁収岳締災賄騙叚叡吻攔蘑蜜訣燧玩硯箏椎藺銅逗驪另覓叨嘮謁杵姓喊嚷囂咚嚀塑尋惱憎擦祇泣滲蝠叱吒咄咤喝籀黛舵舷叵叶鐸懿昭穰苴遼叻叼吁塹嫖賭瞧爬衆抒吅吆夥巹橡滌抱縱摩郡唁墜扇籃膀襪頸吋愾諮酬哭妓媛暗錶韁邇妃羿絮蕃渾拐葵暮隅吔吖啶嗪戚吜嗇噬嚥吟哦詠吠吧唧嗒咐吪雋咀徵燐苞茹鈣哧吮吰吱嘎吲哚吳棟嬌窟孟簫忠晗淞闔閭趼宇吶睛噓拂捧疵熄竽笛糠吼吽呀呂韋矇呃呆笨呇貢呉罄呋喃呎呏呔呠呡癡呣呤呦呧瑛眩扒晬淑姬瑜璇鵑呪呫嗶嚅囁呬呯呰呱呲咧噌鈍呴呶呷呸呺呻哱咻嘯嚕籲坎坷邏呿咁咂咆哮咇咈咋蟹煦珅藹咍咑咒詛咔噠嚓咾噥哩喱咗咠咡咢咣咥咦咨嗟詢咩咪咫嚙齧咭咮咱咲咳嗆嗽咴咷咸咹咺咼喉咿婉慟憫賦矜綠茗藍哂搶瞞哆嗦囉噻啾濱彗哋哌哎唷喲哏哐哞哢哤哪裏哫啼喘哰哲萎蚌哳哶哽哿唄唅唆唈唉唎唏嘩堯棣殤璜睿肅唔睇唕唚唞唣喳唪唬唰喏唲唳唵嘛唶唸唹唻唼唾唿啁啃鸚鵡啅埠棧榷祺舖鞅飆啊啍啎啐啓啕啖啗啜啞祈啢啣啤啥啫啱啲啵啺饑啽噶崑沁喁喂喆裙喈嚨喋喌喎喑喒喓喔粗喙幛慶滋鵲喟喣喤喥喦喧騷喨喩梆喫葡萄喭駝挑嚇碰樅瓣純皰藻趟鉻喵営喹喺喼喿嗀嗃嗄嗅嗈嗉嗊嗍嗐嗑嗔詬嗕嗖嗙嗛嗜痂癖嗝嗡嗤嗥嗨嗩嗬嗯嗰嗲嗵嘰嗷嗹嗾嗿嘀嘁嘂嘅惋嘈峪禾蔭嘊嘌嘏嘐嘒嘓嘖嘚嘜嘞嘟囔嘣嘥嘦嘧嘬嘭這謔嚴敞饞鬆嘵嘶嘷嘸蝦嘹嘻嘽嘿噀噂噅噇噉噎噏噔噗噘噙噚噝噞噢噤蟬皿噩噫噭噯噱噲噳嚏涌灑欲巫霏噷噼嚃嚄嚆抖嚌嚐嚔囌嚚嚜嚞嚟嚦嚬嚭嚮嚯嚲嚳飭按竣苛嚵嚶囀囅囈膪謙囍囒囓囗囘蕭酚飄濺諦囝溯眸紇鑾鶻囟殉囡団囤囥囧囨囪囫圇囬囮囯囲図囶囷囸囹圄圉擬囻囿圀圂圃圊粹蠹赦圌墾圏滾鯡鑿枘圕圛圜圞坯埂壤骸炕祠窯豚紳魠鯪鱉圧握圩圪垯圬圮圯炸岬幔毯祇窨菩溉圳圴圻圾坂坆沾坋坌舛壈昆墊墩椅坒坓坩堝坭坰坱坳坴坵坻坼楊掙涎簾垃垈垌垍垓垔垕垗垚垛垝垣垞垟垤垧垮垵垺垾垿埀畔埄埆埇埈埌殃隍埏埒埕埗埜埡埤埦埧埭埯埰埲埳埴埵埶紼埸培怖樁礎輔埼埽堀訶姪廡堃堄摧磐貞韌砌堈堉堊堋堌堍堎堖堙堞堠礁堧堨輿堭堮蜓摘堲堳堽堿塁塄塈煤塋棵塍塏塒塓綢塕鴉沽虱塙塚塝繆塡塢塤塥塩塬塱塲蟎塼塽塾塿墀墁墈墉墐夯増毀墝墠墦漬缽墫墬墮墰墺墻櫥壅壆壊壌壎壒榨蒜壔壕壖壙壚壜壝壠壡壬壭壱売壴壹壻壼寢壿夂夅夆変夊夌漱邑夓腕泄甥禦骼夗夘夙袞瑙妊娠醣梟珊鶯鷺戧幻魘夤蹀祕擂鶇姚宛閨嶼庾撻拇賛蛤裨菠氅漓撈湄蚊霆鯊箐篆篷荊肆舅荔鮃巷慚骰辟邱鎔鐮阪漂燴鯢鰈鱷鴇臚鵬妒峨譚枰晏璣癸祝秤竺牡籟恢罡螻蠍賜絨御梭夬夭砣榆怙枕夶夾餡奄崛葩譎奈賀祀贈奌奐奓奕訢詝奘奜奠奡奣陶奨奩魁奫奬奰媧孩貶隸酥宄狡猾她奼嫣妁氈荼皋膻蠅嬪妄妍嫉媚嬈妗趣妚妞妤礙妬婭妯娌妲妳妵妺姁姅姉姍姒姘姙姜姝姞姣姤姧姫姮娥姱姸姺姽婀娀誘懾脅娉婷娑娓娟娣娭娯娵娶娸娼婊婐婕婞婤婥谿孺婧婪婬婹婺婼婽媁媄媊媕媞媟媠媢媬媮媯媲媵媸媺媻媼眯媿嫄嫈嫋嫏嫕嫗嫘嫚嫜嫠嫡嫦嫩嫪毐嫫嫬嫰嫵嫺嫻嫽嫿嬀嬃嬅嬉耍嬋痴豔嬔嬖嬗嬙嬝嬡嬢嬤嬦嬬嬭幼嬲嬴嬸嬹嬾嬿孀孃孅孌孏曰癲屏孑孓雀孖斟簍謎摺孛矻鳩崮軻祜鸞孥邈毓棠臏孬孭孰孱孳孵泛罔銜孻孿宀宁宂拙株薇掣撫琪瓿榴謐彌宊濂祁瑕宍宏碁宓邸讞実潢町宥宧宨宬徵崎駿掖闕臊煮禽蠶宸豫寀寁寥寃簷庶寎暄磣寔寖寘寙寛寠苫寤肘洱濫蒗陝覈寪弘綽螽寳擅疙瘩晷対檐専尃尅贖絀繚疇釁尌峙醌襟痲碧屁昊槌淘恵瀑牝畑莓缸羚覷蔻髒躁尒尓銳尗尙尜尟尢尥尨尪尬尭尰擒尲尶尷尸尹潽蠖蛾尻釦梢蚴鰭脬蹲屇屌蚵屐屓挪屖屘屙屛屝屢屣巒嶂巖舄屧屨屩屪屭屮戍駐鉀崖嵛巔旮旯楂欖櫸芋茱萸靛麓屴屹屺屼岀岊岌岍阜岑彭鞏岒岝岢嵐岣岧岨岫岱岵岷峁峇峋峒峓峞峠嵋峩峯峱峴峹峿崀崁崆禎崋崌崍嶇崐崒崔嵬巍螢顥崚崞崟崠崢巆崤崦崧殂崬崱崳崴崶崿嵂嵇嵊泗嵌嵎嵒嵓嵗嵙嵞嵡嵩嵫嵯嵴嵼嵾嶁嶃嶄晴嶋嶌嶒嶓嶔嶗嶙嶝嶞嶠嶡嶢嶧嶨嶭嶮嶰嶲嶴嶸巂巃巇巉巋巌巓巘巛滇芎巟巠弋迴巣巤炊擘蜥蟒蠱覡巰蜀彥淖杏茂甫楞巻巽幗巿帛斐鯽蕊帑帔帗帚琉汶帟帡帣帨帬帯帰帷帹暆幃幄幇幋幌幏幘幙幚幞幠幡幢幦幨幩幪幬幭幯幰遙蹉跎餘庚鑑幵幷稚邃庀庁広庄庈庉笠庋跋庖犧庠庤庥鯨庬庱庳庴庵馨衢庹庿廃廄廆廋廌廎廏廐廑廒廕廖廛廝搏鑼廞弛袤廥廧廨廩廱綿踵髓廸廹甌鄴廻廼廾廿躔弁皺弇弌弍弎弐弒弔詭憾薦弝弢弣弤弨弭弮弰弳霖繇燾斌旭溥騫弶弸弼弾彀彄彆纍糾彊彔彖彘彟彠陌彤貽彧繪虹彪炳彫蔚鷗彰癉彲彳彴彷彷徉徨彸彽踩斂旆徂徇徊渭畬鉉裼従筌徘徙徜徠膳甦萌漸徬徭醺徯徳徴潘徻徼忀瘁胖燎怦悸顫扉犀澎湃砰恍惚絞隘忉憚挨餓忐忑忒忖応忝忞耿忡忪忭忮忱忸怩忻悠懣怏遏怔怗怚怛怞懟黍訝怫怭懦怱怲怳怵惕怸怹恁恂恇恉恌恏恒恓恔恘恚恛恝恞恟恠恣恧眄恪恫恬澹恰恿悀悁悃悄悆悊悐悒晦悚悛悜悝悤您悩悪悮悰悱悽惻悳悴悵惘悶悻悾惄愫鍾蒐惆惇惌惎惏惓惔惙惛耄惝瘧濁惥惦惪惲惴惷惸拈愀愃愆愈愊愍愐愑愒愓愔愕愙氓蠢騃昵愜赧愨愬愮愯愷愼慁慂慅慆慇靄慉慊慍慝慥慪慫慬慱慳慴慵慷慼焚憀灼鬱憃憊憋憍眺捏軾憒憔憖憙憧憬憨憪憭憮憯憷憸憹憺懃懅懆邀懊懋懌懍懐懞懠懤懥懨懫懮懰懱毖懵遁樑雍懺懽戁戄戇戉戔戕戛戝戞戠戡戢戣戤戥戦戩戭戯轟戱披菊牖戸戹戺戻戼戽鍬扂楔扃扆扈扊杖牽絹銬鐲賚扐摟攪烊盹瞌跟躉鑔靶鼾払扗玫腮扛扞扠扡扢盔押扤扦扱罾揄綏鞍郤窾扻扼扽抃抆抈抉抌抏瞎抔繯縊擻抜抝択抨摔歉躥牾抶抻搐泵菸拃拄拊髀拋拌脯拎拏拑擢秧沓曳攣迂拚拝拠拡拫拭拮踢拴拶拷攢拽掇芥橐簪摹疔挈瓢驥捺蹻挌挍挎挐揀挓挖掘浚挙揍聵挲挶挾挿捂捃捄捅捆捉捋胳膊揎捌捍捎軀蛛捗捘捙捜捥捩捫捭据捱捻捼捽掀掂掄臀膘掊掎掏掐笙掔掗掞棉芍掤搪闡掫掮掯揉掱掲掽掾揃揅揆搓揌諢揕揗揘揜揝揞揠揥揩揪揫櫫遒麈揰揲揵揶揸揹揺搆搉搊搋搌搎搔搕撼櫓搗搘搠搡搢搣搤搥搦搧搨搬楦褳訕赸搯搰搲搳搴搵搷搽搾搿摀摁摂摃摎摑摒摓跤摙摛摜摞摠摦睺羯摭摮摯摰摲摳摴摶摷摻摽撂撃撅稻撊撋撏鐧潑撕撙撚撝撟撢撣撦撧撩撬撱朔撳蚍蜉撾撿擀擄闖擉缶觚擐擕擖擗擡擣擤澡腚擧擨擩擫擭擯擰擷擸擼擽擿攃攄攆攉攥攐攓攖攙攛每攩攫轡澄攮攰攲攴軼攷砭訐攽碘敁敃敇敉敍敎筏敔敕敖閏誨敜煌敧敪敱敹敺敻敿斁衽斄牒縐謅斉斎斕鶉讕駮鱧斒筲斛斝斞斠斡斢斨斫斮晾沂潟穎絳邵斲斸釳於琅斾斿旀旂旃旄渦旌旎旐旒旓旖旛旝旟旡旣浴旰獺魃旴旹旻旼旽昀昃昄昇昉晰躲澈熹皎皓礬昑昕昜昝昞昡昤暉筍昦昨昰昱昳昴昶昺昻晁蹇隧蔬髦晄晅晒晛晜晞晟晡晢晤晥曦晩萘瑩顗晿暁暋暌暍暐暔暕煅暘暝暠暡曚暦暨暪朦朧暱暲殄馮暵暸暹暻暾曀曄曇曈曌曏曐曖曘曙曛曡曨曩駱曱甴肱曷牘禺錕曽滄耽朁朅朆杪栓誇竟粘絛朊膺朏朐朓朕朘朙瞄覲溘饔飧朠朢朣柵椆澱蝨朩朮朰朱炆璋鈺熾鹮朳槿朶朾朿杅杇杌隉欣釗湛漼楷瀍煜玟纓翱肈舜贄适逵杓杕杗杙荀蘅杝杞脩珓筊杰榔狍閦顰緬莞杲杳眇杴杶杸杻杼枋枌枒枓衾葄翹紓逋枙狸椏枟槁枲枳枴枵枷枸櫞枹枻柁柂柃柅柈柊柎某柑橘柒柘柙柚柜柞櫟柟柢柣柤柩柬柮柰柲橙柶柷柸柺査柿栃栄栒栔栘栝栟栢栩栫栭栱栲栳栴檀栵栻桀驁桁鎂桄桉桋桎梏椹葚桓桔桕桜桟桫欏桭桮桯桲桴桷桹湘溟梃梊梍梐潼梔梘梜梠梡梣梧梩梱梲梳梴梵梹棁棃櫻棐棑棕櫚簑繃蓑棖棘棜棨棩棪棫棬棯棰棱棳棸棹槨棼椀椄苕椈椊椋椌椐椑椓椗検椤椪椰椳椴椵椷椸椽椿楀楄楅篪楋楍楎楗楘楙楛楝楟楠楢楥楨楩楪楫楬楮楯楰楳楸楹楻楽榀榃榊榎槺榕榖榘榛狉莽榜笞榠榡榤榥榦榧榪榭榰榱槤霰榼榾榿槊閂槎槑槔槖様槜槢槥槧槪槭槮槱槲槻槼槾樆樊樏樑樕樗樘樛樟樠樧樨権樲樴樵猢猻樺樻罍樾樿橁橄橆橈笥龠橕橚橛輛橢橤橧豎膈跨橾橿檁檃檇檉檍檎檑檖檗檜檟檠檣檨檫檬檮檳檴檵檸櫂櫆櫌櫛櫜櫝櫡櫧櫨櫪櫬櫳櫹櫺茄櫽欀欂欃欐欑欒欙欞溴欨欬欱欵欶欷歔欸欹欻欼欿歁歃歆艎歈歊蒔蝶歓歕歘歙歛歜歟歠蹦詮鑲蹣跚陞陟歩歮歯歰歳歴璞歺瞑歾歿殀殈殍殑殗殜殙殛殞殢殣殥殪殫殭殰殳荃殷殸殹蛟殻殽謗毆毈毉餵毎毑蕈毗毘毚茛鄧毧毬毳毷毹毽毾毿氂氄氆靴氉氊氌氍氐聊氕氖気氘氙氚氛氜氝氡洶焊痙氤氳氥氦鋁鋅氪烴氬銨痤汪滸漉痘盂碾菖蒲蕹蛭螅氵氷氹氺氽燙氾氿渚汆汊汋汍汎汏汐汔汕褟汙汚汜蘺沼穢衊汧汨汩汭汲汳汴隄汾沄沅沆瀣沇沈葆浸淪湎溺痼痾沌沍沏沐沔沕沘浜畹礫沚沢沬沭沮沰沱灢沴沷籽沺烹濡洄泂肛泅泆湧肓泐泑泒泓泔泖泙泚泜泝泠漩饃濤粼濘蘚鰍泩泫泭泯銖泱泲洇洊涇琵琶荽薊箔洌洎洏洑潄濯洙洚洟洢洣洧洨洩痢滔洫洮洳洴洵洸洹洺洼洿淌蜚浄浉浙贛渫浠浡浤浥淼瀚浬浭翩萍浯浰蜃淀苔蛞蝓蜇螵蛸煲鯉浹浼浽溦涂涊涐涑涒涔滂涖涘涙涪涫涬涮涴涶涷涿淄淅淆淊淒黯淓淙漣淜淝淟淠淢淤淥淦淩猥藿褻淬淮淯淰淳詣淶紡淸淹燉癯綺渇済渉渋渓渕渙渟渢滓渤澥渧渨渮渰渲渶渼湅湉湋湍湑湓湔黔湜湝湞湟湢湣湩湫湮麟湱湲湴湼満溈溍溎溏溛舐漭溠溤溧馴溮溱溲溳溵溷溻溼溽溾滁滃滉滊滎滏稽滕滘滙滝滫滮羼耷滷滹滻煎漈漊漎繹漕漖漘漙漚漜漪漾漥漦漯漰漵漶漷濞潀潁潎潏潕潗潚潝潞潠潦祉瘍潲潵潷潸潺潾潿澁澂澃澉澌澍澐澒澔澙澠澣澦澧澨澫澬澮澰澴澶澼熏郁濆濇濈濉濊貊濔疣濜濠濩觴濬濮盥濰濲濼瀁瀅瀆瀋瀌瀏瀒瀔瀕瀘瀛瀟瀠瀡瀦瀧瀨瀬瀰瀲瀳瀵瀹瀺瀼灃灄灉灋灒灕灖灝灞灠灤灥灨灩灪蜴灮燼獴灴灸灺炁炅魷炗炘炙炤炫疽烙釺炯炰炱炲炴炷燬炻烀烋瘴鯧烓烔焙烜烝烳飪烺焃焄耆焌焐焓焗焜焞焠焢焮焯焱焼煁煃煆煇煊熠煍熬煐煒煕煗燻礆霾煚煝煟煠煢矸煨瑣煬萁煳煺煻熀熅熇熉羆熒穹熗熘熛熜稔諳爍熤熨熯熰眶螞熲熳熸熿燀燁燂燄盞燊燋燏燔隼燖燜燠燡燦燨燮燹燻燽燿爇爊爓爚爝爟爨蟾爯爰爲爻爿爿牀牁牂牄牋牎牏牓牕釉牚腩蒡虻牠雖蠣牣牤牮牯牲牳牴牷牸牼絆牿靬犂犄犆犇犉犍犎犒犖犗犛犟犠犨犩犪犮犰狳犴犵犺狁甩狃狆狎狒獾狘狙黠狨狩狫狴狷狺狻豕狽蜘猁猇猈猊猋猓猖獗猗猘猙獰獁猞猟獕猭猱猲猳猷猸猹猺玃獀獃獉獍獏獐獒獘獙獚獜獝獞獠獢獣獧鼇蹊獪獫獬豸獮獯鬻獳獷獼玀玁菟玅玆玈珉糝禛郅玍玎玓瓅玔玕玖玗玘玞玠玡玢玤玥玦玨瑰玭玳瑁玶玷玹玼珂珇珈瑚珌饈饌珔珖珙珛珞珡珣珥珧珩珪珮珶珷珺珽琀琁隕琊琇琖琚琠琤琦琨琫琬琭琮琯琰琱琲瑯琹琺琿瑀瑂瑄瑉瑋瑑瑔瑗瑢瑭瑱瑲瑳瑽瑾瑿璀璨璁璅璆璈璉璊璐璘璚璝璟璠璡璥璦璩璪璫璯璲璵璸璺璿瓀瓔瓖瓘瓚瓛臍瓞瓠瓤瓧瓩瓮瓰瓱瓴瓸瓻瓼甀甁甃甄甇甋甍甎甏甑甒甓甔甕甖甗飴蔗甙詫鉅粱盎銹糰甡褥産甪甬甭甮甯鎧甹甽甾甿畀畁畇畈畊畋畎畓畚畛畟鄂畤畦畧荻畯畳畵畷畸畽畾疃疉疋疍疎簞疐疒疕疘疝疢疥疧疳疶疿痁痄痊痌痍痏痐痒痔痗瘢痚痠痡痣痦痩痭痯痱痳痵痻痿瘀瘂瘃瘈瘉瘊瘌瘏瘐瘓瘕瘖瘙瘚瘛瘲瘜瘝瘞瘠瘥瘨瘭瘮瘯瘰癧瘳癘瘵瘸瘺瘻瘼癃癆癇癈癎癐癔癙癜癠癤癥癩蟆癪癭癰発踔紺蔫酵皙砬砒翎翳蘞鎢鑞皚鵯駒鱀粵褶皀皁莢皃鎛皈皌皐皒硃皕皖皘皜皝皞皤皦皨皪皫皭糙綻皴皸皻皽盅盋盌盍盚盝踞盦盩鞦韆盬盭眦睜瞤盯盱眙裰盵盻睞眂眅眈眊県眑眕眚眛眞眢眣眭眳眴眵眹瞓眽郛睃睅睆睊睍睎睏睒睖睙睟睠睢睥睪睪睯睽睾瞇瞈瞋瞍逛瞏瞕瞖瞘瞜瞟瞠瞢瞫瞭瞳瞵瞷瞹瞽闍瞿矓矉矍鑠矔矗矙矚矞矟矠矣矧矬矯矰矱硪碇磙罅舫阡、矼矽礓砃砅砆砉砍砑砕砝砟砠砢砦砧砩砫砮砳艏砵砹砼硇硌硍硎硏硐硒硜硤硨磲茚鋇硭硻硾碃碉碏碣碓碔碞碡碪碫碬碭碯碲碸碻礡磈磉磎磑磔磕磖磛磟磠磡磤磥蹭磪磬磴磵磹磻磽礀礄礅礌礐礚礜礞礤礧礮礱礲礵礽礿祂祄祅祆禳祊祍祏祓祔祕祗祘祛祧祫祲祻祼餌臠錮禂禇禋禑禔禕隋禖禘禚禜禝禠禡禢禤禥禨禫禰禴禸稈秈秊闈颯秌秏秕笈蘵賃秠秣秪秫秬秭秷秸稊稌稍稑稗稙稛稞稬稭稲稹稼顙稾穂穄穇穈穉穋穌貯穏穜穟穠穡穣穤穧穨穭穮穵穸窿闃窀窂窅窆窈窕窊窋窌窒窓窔窞窣窬黷蹙窰窳窴窵窶窸窻竁竃竈竑竜竝竦竪篦篾笆鮫竾笉笊笎笏笐靨笓笤籙笪笫笭笮笰笱笲笳笵笸笻筀筅筇筈筎筑筘筠筤筥筦筧筩筭筯筰筱筳筴讌筸箂箇箊箎箑箒箘箙箛箜篌箝箠箬鏃箯箴箾篁篔簹篘篙篚篛篜篝篟篠篡篢篥篧篨篭篰篲篳篴篶篹篼簀簁簃簆簉簋簌簏簜簟簠簥簦簨簬簰簸簻籊籐籒籓籔籖籚籛籜籣籥籧籩籪籫籯芾麴籵籸籹籼粁粃粋粑粔糲粛粞粢粧粨粲粳粺粻粽闢粿糅糆糈糌糍糒糔萼糗蛆蹋糢糨糬糭糯糱糴糶糸糺紃蹼鰹黴紆紈絝紉閩襻紑紕紘錠鳶鷂紝紞紟紥紩紬紱紲紵紽紾紿絁絃絅経絍絎絏縭褵絓絖絘絜絢絣螯絪絫聒絰絵絶絺絻絿綀綃綅綆綈綉綌綍綎綑綖綘継続緞綣綦綪綫綮綯綰罟蝽綷縩綹綾緁緄緅緆緇緋緌緎総緑緔緖緗緘緙緜緡緤緥緦纂緪緰緱緲緶緹縁縃縄縈縉縋縏縑縕縗縚縝縞縟縠縡縢縦縧縯縰騁縲縳縴縵縶縹縻衙縿繄繅繈繊繋繐繒繖繘繙繠繢繣繨繮繰繸繻繾纁纆纇纈纉纊纑纕纘纙纚纛缾罃罆罈罋罌罎罏罖罘罛罝罠罣罥罦罨罫罭鍰罳罶罹罻罽罿羂羃羇羋蕉51鴕羑羖羗羜羝羢羣羥羧羭羮羰羱羵羶羸藜鮐翀翃翄翊翌翏翕翛翟翡翣翥翦躚翪翫翬翮翯翺翽翾翿闆饕鴰鍁耋耇耎耏耑耒耜耔耞耡耤耨耩耪耬耰鬢耵聹聃聆聎聝聡聦聱聴聶聼閾聿肄肏肐肕腋肙肜肟肧胛肫肬肭肰肴肵肸肼胊胍胏胑胔胗胙胝胠銓胤胦胩胬胭胯胰胲胴胹胻胼胾脇脘脝脞脡脣脤脥脧脰脲脳腆腊腌臢腍腒腓腖腜腠腡腥腧腬腯踝蹬鐐腴腶蠕誹膂膃膆膇膋膔膕膗膙膟黐膣膦膫膰膴膵膷膾臃臄臇臈臌臐臑臓臕臖臙臛臝臞臧蓐詡臽臾臿舀舁鰟鮍舋舎舔舗舘舝舠舡舢舨舭舲舳舴舸舺艁艄艅艉艋艑艕艖艗艘艚艜艟艣艤艨艩艫艬艭荏艴艶艸艹艻艿芃芄芊萰陂藭芏芔芘芚蕙芟芣芤茉芧芨芩芪芮芰鰱芴芷芸蕘豢芼芿苄苒苘苙苜蓿苠苡苣蕒苤苧苪鎊苶苹苺苻苾茀茁范蠡萣茆茇茈茌茍茖茞茠茢茥茦菰茭茯茳藨茷藘茼荁荄荅荇荈菅蜢鴞荍荑荘荳荵荸薺莆莒莔莕莘莙莚莛莜莝莦莨菪莩莪莭莰莿菀菆菉菎菏菐菑菓菔菕菘菝菡菢菣菥蓂菧菫轂鎣菶菷菹醢菺菻菼菾萅萆萇萋萏萐萑萜萩萱萴萵萹萻葇葍葎葑葒葖葙葠葥葦葧葭葯葳葴葶葸葹葽蒄蒎蒓蘢薹蒞蒟蒻蒢蒦蒨蒭藁蒯蒱鉾蒴蒹蒺蒽蓀蓁蓆蓇蓊蓌蓍蓏蓓蓖蓧蓪蓫蓽跣藕蓯蓰蓱蓴蓷蓺蓼蔀蔂蔃蔆蔇蔉蔊蔋蔌蔎蔕蔘蔙蔞蔟鍔蔣雯蔦蔯蔳蔴蔵蔸蔾蕁蕆蕋蕍蕎蕐蕑蕓蕕蕖蕗蕝蕞蕠蕡蕢蕣蕤蕨蕳蕷蕸蕺蕻薀薁薃薅薆薈薉薌薏薐薔薖薘薙諤釵薜薠薢薤薧薨薫薬薳薶薷薸薽薾薿藄藇藋藎藐藙藚藟藦藳藴藶藷藾蘀蘁蘄蘋蘗蘘蘝蘤蘧蘩蘸蘼虀虆虍蟠虒虓虖虡虣虥虩虯虰蛵虵虷鱒虺虼蚆蚈蚋蚓蚔蚖蚘蚜蚡蚣蚧蚨蚩蚪蚯蚰蜒蚱蚳蚶蚹蚺蚻蚿蛀蛁蛄蛅蝮蛌蛍蛐蟮蛑蛓蛔蛘蛚蛜蛡蛣蜊蛩蛺蛻螫蜅蜆蜈蝣蜋蜍蜎蜑蠊蜛餞蜞蜣蜨蜩蜮蜱蜷蜺蜾蜿蝀蝃蝋蝌蝍蝎蝏蝗蝘蝙蝝鱝蝡蝤蝥蝯蝰蝱蝲蝴蝻螃蠏螄螉螋螒螓螗螘螙螚蟥螟螣螥螬螭螮螾螿蟀蟅蟈蟊蟋蟑蟓蟛蟜蟟蟢蟣蟨蟪蟭蟯蟳蟶蟷蟺蟿蠁蠂蠃蠆蠋蠐蠓蠔蠗蠙蠚蠛蠜蠧蠨蠩蠭蠮蠰蠲蠵蠸蠼蠽衁衂衄衇衈衉衋衎衒衕衖衚衞裳鈎衭衲衵衹衺衿袈裟袗袚袟袢袪袮袲袴袷袺袼褙袽裀裉裊裋裌裍裎裒裛裯裱裲裴裾褀褂褉褊褌褎褐褒褓褔褕褘褚褡褢褦褧褪褫褭褯褰褱襠褸褽褾襁襃襆襇襉襋襌襏襚襛襜襝襞襡襢襤襦襫襬襭襮襴襶襼襽襾覂覃覅覇覉覊覌覗覘覚覜覥覦覧覩覬覯覰観覿觔觕觖觜觽觝觡酲觩觫觭觱觳觶觷觼觾觿言賅訃訇訏訑訒詁託訧訬訳訹証訾詀詅詆譭詈詊詎詑詒詖詗詘詧詨詵詶詸詹詻詼詿誂誃誄鋤誆誋誑誒誖誙誚誥誧説読誯誶誾諂諄諆諌諍諏諑諕諗諛諝諞諟諠諡諴諵諶諼謄謆謇謌謍謏謑謖謚謡謦謪謫謳謷謼謾譁譅譆譈譊譌譒譔譖鑫譞譟譩譫譬譱譲譴譸譹譾讅讆讋讌讎讐讒讖讙讜讟谽豁豉豇豈豊豋豌豏豔豞豖豗豜豝豣豦豨豭豱豳豵豶豷豺豻貅貆貍貎貔貘貙貜貤饜貰餸貺賁賂賏賒賕賙賝賡賧賨賫鬭賮賵賸賺賻賾贇贉贐贔贕贗赬赭赱赳迄趁趂趄趐趑趒趔趡趦趫趮趯趲趴趵趷趹趺趿跁跂跅跆躓蹌跐跕跖跗跙跛跦跧跩跫跬跮跱跲跴跺跼跽踅踆踈踉踊踒踖踘踜踟躇躕踠踡踣踤踥踦踧蹺踫踮踰踱踴踶踹踺踼踽躞蹁蹂躪蹎蹐蹓蹔蹕蹚蹜蹝蹟蹠蹡蹢躂蹧蹩蹪蹯鞠蹽躃躄躅躊躋躐躑躒躘躙躛躝躠躡躦躧躩躭躰躳躶軃軆輥軏軔軘軜軝齶転軥軨軭軱軲轆軷軹軺軽軿輀輂輦輅輇輈輓輗輙輜輞輠輤輬輭輮輳輴輵輶輹輼輾轀轇轏轑轒轔轕轖轗轘轙轝轞轢轤辠辢辤辵辶辺込辿迅迋迍麿迓迣迤邐迥迨迮迸迺迻迿逄逅逌逍逑逓逕逖逡逭逯逴逶逹遄遅遉遘遛遝遢遨遫遯遰遴遶遹遻邂邅邉邋邎邕邗邘邛邠邢邧邨邯鄲邰邲邳邴邶邷邽邾邿郃郄郇郈郔郕郗郙郚郜郝郞郟郠郢郪郫郯郰郲郳郴郷郹郾郿鄀鄄鄆鄇鄈鄋鄍鄎鄏鄐鄑鄒鄔鄕鄖鄗鄘鄚鄜鄞鄠鄢鄣鄤鄦鄩鄫鄬鄮鄯鄱鄶鄷鄹鄺鄻鄾鄿酃酅酆酇酈酊酋酎酏酐酣酔酕醄酖酗酞酡酢酤酩酴酹酺醁醅醆醊醍醐醑醓醖醝醞醡醤醨醪醭醯醰醱醲醴醵醸醹醼醽醾釂釃釅釆釈鱸鎦閶釓釔釕鈀釙鼢鼴釤釧釪釬釭釱釷釸釹鈁鈃鈄鈆鈇鈈鈊鈌鈐鈑鈒鈤鈥鈧鈬鈮鈰鈳鐺鈸鈹鈽鈿鉄鉆鉈鉋鉌鉍鉏鉑鉕鉚鉢鉥鉦鉨鉬鉭鉱鉲鉶鉸鉺鉼鉿銍銎銑銕鏤銚銛銠銣銤銥銦銧銩銪銫銭銰銲銶銻銼銾鋂鋃鋆鋈鋊鋌鋍鋏鋐鋑鋕鋘鋙鋝鋟鋦鋨鋩鋭鋮鋯鋰鋱鋳鋹鋺鋻鏰鐱錀錁錆錇錈錍錏錒錔錙錚錛錞錟錡錤錩錬録錸錼鍀鍆鍇鍉鍍鍏鍐鍘鍚鍛鍠鍤鍥鍩鍫鍭鍱鍴鍶鍹鍺鍼鍾鎄鎇鎉鎋鎌鎍鎏鎒鎓鎗鎘鎚鎞鎡鎤鎩鎪鎭鎯鎰鎳鎴鎵鎸鎹鎿鏇鏊鏌鏐鏑鏖鏗鏘鏚鏜鏝鏞鏠鏦鏨鏷鏸鏹鏻鏽鏾鐃鐄鐇鐏鐒鐓鐔鐗馗鐙鐝鐠鐡鐦鐨鐩鐫鐬鐱鐳鐶鐻鐽鐿鑀鑅鑌鑐鑕鑚鑛鑢鑤鑥鑪鑭鑯鑱鑴鑵鑷钁钃镻閆閈閌閎閒閔閗閟閡関閤閤閧閬閲閹閺閻閼閽閿闇闉闋闐闑闒闓闘闚闞闟闠闤闥阞阢阤阨阬阯阹阼阽陁陑陔陛陜陡陥陬騭陴険陼陾隂隃隈隒隗隞隠隣隤隩隮隰顴隳隷隹雂雈雉雊雎雑雒雗雘雚雝雟雩雰雱驛霂霅霈霊霑霒霓霙霝霢霣霤霨霩霪霫霮靁靆靉靑靚靣靦靪靮靰靳靷靸靺靼靿鞀鞃鞄鞌鞗鞙鞚鞝鞞鞡鞣鞨鞫鞬鞮鞶鞹鞾韃韅韉馱韍韎韔韖韘韝韞韡韣韭韮韱韹韺頀颳頄頇頊頍頎頏頒頖頞頠頫頬顱頯頲頴頼顇顋顑顒顓顔顕顚顜顢顣顬顳颭颮颱颶颸颺颻颽颾颿飀飂飈飌飜飡飣飤飥飩飫飮飱飶餀餂餄餎餇餈餑餔餕餖餗餚餛餜餟餠餤餧餩餪餫餬餮餱餲餳餺餻餼餽餿饁饅饇饉饊饍饎饐饘饟饢馘馥馝馡馣騮騾馵馹駃駄駅駆駉駋駑駓駔駗駘駙駜駡駢駪駬駰駴駸駹駽駾騂騄騅騆騉騋騍騏驎騑騒験騕騖騠騢騣騤騧驤騵騶騸騺驀驂驃驄驆驈驊驌驍驎驏驒驔驖驙驦驩驫骺鯁骫骭骯骱骴骶骷髏骾髁髂髄髆髈髐髑髕髖髙髝髞髟髡髣髧髪髫髭髯髲髳髹髺髽髾鬁鬃鬅鬈鬋鬎鬏鬐鬑鬒鬖鬗鬘鬙鬠鬣鬪鬫鬬鬮鬯鬰鬲鬵鬷魆魈魊魋魍魎魑魖鰾魛魟魣魦魨魬魴魵魸鮀鮁鮆鮌鮎鮑鮒鮓鮚鮞鮟鱇鮠鮦鮨鮪鮭鮶鮸鮿鯀鯄鯆鯇鯈鯔鯕鯖鯗鯙鯠鯤鯥鯫鯰鯷鯸鯿鰂鰆鶼鰉鰋鰐鰒鰕鰛鰜鰣鰤鰥鰦鰨鰩鰮鰳鰶鰷鱺鰼鰽鱀鱄鱅鱆鱈鱎鱐鱓鱔鱖鱘鱟鱠鱣鱨鱭鱮鱲鱵鱻鲅鳦鳧鳯鳲鳷鳻鴂鴃鴄鴆鴈鴎鴒鴔鴗鴛鴦鴝鵒鴟鴠鴢鴣鴥鴯鶓鴳鴴鴷鴽鵀鵁鵂鵓鵖鵙鵜鶘鵞鵟鵩鵪鵫鵵鵷鵻鵾鶂鶊鶏鶒鶖鶗鶡鶤鶦鶬鶱鶲鶵鶸鶹鶺鶿鷀鷁鷃鷄鷇鷈鷉鷊鷏鷓鷕鷖鷙鷞鷟鷥鷦鷯鷩鷫鷭鷳鷴鷽鷾鷿鸂鸇鸊鸏鸑鸒鸓鸕鸛鸜鸝鹸鹹鹺麀麂麃麄麇麋麌麐麑麒麚麛麝麤麩麪麫麮麯麰麺麾黁黈黌黢黒黓黕黙黝黟黥黦黧黮黰黱黲黶黹黻黼黽黿鼂鼃鼅鼈鼉鼏鼐鼒鼕鼖鼙鼚鼛鼡鼩鼱鼪鼫鼯鼷鼽齁齆齇齈齉齌齎齏齔齕齗齙齚齜齞齟齬齠齢齣齧齩齮齯齰齱齵齾龎龑龒龔龖龘龝龡龢龤' + +assert len(simplified_charcters) == len(simplified_charcters) + +s2t_dict = {} +t2s_dict = {} +for i, item in enumerate(simplified_charcters): + s2t_dict[item] = traditional_characters[i] + t2s_dict[traditional_characters[i]] = item + + +def tranditional_to_simplified(text: str) -> str: + return "".join( + [t2s_dict[item] if item in t2s_dict else item for item in text]) + + +def simplified_to_traditional(text: str) -> str: + return "".join( + [s2t_dict[item] if item in s2t_dict else item for item in text]) + + +if __name__ == "__main__": + text = "一般是指存取一個應用程式啟動時始終顯示在網站或網頁瀏覽器中的一個或多個初始網頁等畫面存在的站點" + print(text) + text_simple = tranditional_to_simplified(text) + print(text_simple) + text_traditional = simplified_to_traditional(text_simple) + print(text_traditional) diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/chronology.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/chronology.py new file mode 100644 index 0000000..ea4558e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/chronology.py @@ -0,0 +1,134 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .num import DIGITS +from .num import num2str +from .num import verbalize_cardinal +from .num import verbalize_digit + + +def _time_num2str(num_string: str) -> str: + """A special case for verbalizing number in time.""" + result = num2str(num_string.lstrip('0')) + if num_string.startswith('0'): + result = DIGITS['0'] + result + return result + + +# 时刻表达式 +RE_TIME = re.compile(r'([0-1]?[0-9]|2[0-3])' + r':([0-5][0-9])' + r'(:([0-5][0-9]))?') + +# 时间范围,如8:30-12:30 +RE_TIME_RANGE = re.compile(r'([0-1]?[0-9]|2[0-3])' + r':([0-5][0-9])' + r'(:([0-5][0-9]))?' + r'(~|-)' + r'([0-1]?[0-9]|2[0-3])' + r':([0-5][0-9])' + r'(:([0-5][0-9]))?') + + +def replace_time(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + + is_range = len(match.groups()) > 5 + + hour = match.group(1) + minute = match.group(2) + second = match.group(4) + + if is_range: + hour_2 = match.group(6) + minute_2 = match.group(7) + second_2 = match.group(9) + + result = f"{num2str(hour)}点" + if minute.lstrip('0'): + if int(minute) == 30: + result += "半" + else: + result += f"{_time_num2str(minute)}分" + if second and second.lstrip('0'): + result += f"{_time_num2str(second)}秒" + + if is_range: + result += "至" + result += f"{num2str(hour_2)}点" + if minute_2.lstrip('0'): + if int(minute) == 30: + result += "半" + else: + result += f"{_time_num2str(minute_2)}分" + if second_2 and second_2.lstrip('0'): + result += f"{_time_num2str(second_2)}秒" + + return result + + +RE_DATE = re.compile(r'(\d{4}|\d{2})年' + r'((0?[1-9]|1[0-2])月)?' + r'(((0?[1-9])|((1|2)[0-9])|30|31)([日号]))?') + + +def replace_date(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + year = match.group(1) + month = match.group(3) + day = match.group(5) + result = "" + if year: + result += f"{verbalize_digit(year)}年" + if month: + result += f"{verbalize_cardinal(month)}月" + if day: + result += f"{verbalize_cardinal(day)}{match.group(9)}" + return result + + +# 用 / 或者 - 分隔的 YY/MM/DD 或者 YY-MM-DD 日期 +RE_DATE2 = re.compile( + r'(\d{4})([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])') + + +def replace_date2(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + year = match.group(1) + month = match.group(3) + day = match.group(4) + result = "" + if year: + result += f"{verbalize_digit(year)}年" + if month: + result += f"{verbalize_cardinal(month)}月" + if day: + result += f"{verbalize_cardinal(day)}日" + return result diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/constants.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/constants.py new file mode 100644 index 0000000..5d2b0b3 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/constants.py @@ -0,0 +1,62 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +import string + +from pypinyin.constants import SUPPORT_UCS4 + +# 全角半角转换 +# 英文字符全角 -> 半角映射表 (num: 52) +F2H_ASCII_LETTERS = { + chr(ord(char) + 65248): char + for char in string.ascii_letters +} + +# 英文字符半角 -> 全角映射表 +H2F_ASCII_LETTERS = {value: key for key, value in F2H_ASCII_LETTERS.items()} + +# 数字字符全角 -> 半角映射表 (num: 10) +F2H_DIGITS = {chr(ord(char) + 65248): char for char in string.digits} +# 数字字符半角 -> 全角映射表 +H2F_DIGITS = {value: key for key, value in F2H_DIGITS.items()} + +# 标点符号全角 -> 半角映射表 (num: 32) +F2H_PUNCTUATIONS = {chr(ord(char) + 65248): char for char in string.punctuation} +# 标点符号半角 -> 全角映射表 +H2F_PUNCTUATIONS = {value: key for key, value in F2H_PUNCTUATIONS.items()} + +# 空格 (num: 1) +F2H_SPACE = {'\u3000': ' '} +H2F_SPACE = {' ': '\u3000'} + +# 非"有拼音的汉字"的字符串,可用于NSW提取 +if SUPPORT_UCS4: + RE_NSW = re.compile(r'(?:[^' + r'\u3007' # 〇 + r'\u3400-\u4dbf' # CJK扩展A:[3400-4DBF] + r'\u4e00-\u9fff' # CJK基本:[4E00-9FFF] + r'\uf900-\ufaff' # CJK兼容:[F900-FAFF] + r'\U00020000-\U0002A6DF' # CJK扩展B:[20000-2A6DF] + r'\U0002A703-\U0002B73F' # CJK扩展C:[2A700-2B73F] + r'\U0002B740-\U0002B81D' # CJK扩展D:[2B740-2B81D] + r'\U0002F80A-\U0002FA1F' # CJK兼容扩展:[2F800-2FA1F] + r'])+') +else: + RE_NSW = re.compile( # pragma: no cover + r'(?:[^' + r'\u3007' # 〇 + r'\u3400-\u4dbf' # CJK扩展A:[3400-4DBF] + r'\u4e00-\u9fff' # CJK基本:[4E00-9FFF] + r'\uf900-\ufaff' # CJK兼容:[F900-FAFF] + r'])+') diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/num.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/num.py new file mode 100644 index 0000000..a83b42a --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/num.py @@ -0,0 +1,238 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Rules to verbalize numbers into Chinese characters. +https://zh.wikipedia.org/wiki/中文数字#現代中文 +""" +import re +from collections import OrderedDict +from typing import List + +DIGITS = {str(i): tran for i, tran in enumerate('零一二三四五六七八九')} +UNITS = OrderedDict({ + 1: '十', + 2: '百', + 3: '千', + 4: '万', + 8: '亿', +}) + +COM_QUANTIFIERS = '(所|朵|匹|张|座|回|场|尾|条|个|首|阙|阵|网|炮|顶|丘|棵|只|支|袭|辆|挑|担|颗|壳|窠|曲|墙|群|腔|砣|座|客|贯|扎|捆|刀|令|打|手|罗|坡|山|岭|江|溪|钟|队|单|双|对|出|口|头|脚|板|跳|枝|件|贴|针|线|管|名|位|身|堂|课|本|页|家|户|层|丝|毫|厘|分|钱|两|斤|担|铢|石|钧|锱|忽|(千|毫|微)克|毫|厘|(公)分|分|寸|尺|丈|里|寻|常|铺|程|(千|分|厘|毫|微)米|米|撮|勺|合|升|斗|石|盘|碗|碟|叠|桶|笼|盆|盒|杯|钟|斛|锅|簋|篮|盘|桶|罐|瓶|壶|卮|盏|箩|箱|煲|啖|袋|钵|年|月|日|季|刻|时|周|天|秒|分|小时|旬|纪|岁|世|更|夜|春|夏|秋|冬|代|伏|辈|丸|泡|粒|颗|幢|堆|条|根|支|道|面|片|张|颗|块|元|(亿|千万|百万|万|千|百)|(亿|千万|百万|万|千|百|美|)元|(亿|千万|百万|万|千|百|)块|角|毛|分)' + +# 分数表达式 +RE_FRAC = re.compile(r'(-?)(\d+)/(\d+)') + + +def replace_frac(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + nominator = match.group(2) + denominator = match.group(3) + sign: str = "负" if sign else "" + nominator: str = num2str(nominator) + denominator: str = num2str(denominator) + result = f"{sign}{denominator}分之{nominator}" + return result + + +# 百分数表达式 +RE_PERCENTAGE = re.compile(r'(-?)(\d+(\.\d+)?)%') + + +def replace_percentage(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + percent = match.group(2) + sign: str = "负" if sign else "" + percent: str = num2str(percent) + result = f"{sign}百分之{percent}" + return result + + +# 整数表达式 +# 带负号的整数 -10 +RE_INTEGER = re.compile(r'(-)' r'(\d+)') + + +def replace_negative_num(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + number = match.group(2) + sign: str = "负" if sign else "" + number: str = num2str(number) + result = f"{sign}{number}" + return result + + +# 编号-无符号整形 +# 00078 +RE_DEFAULT_NUM = re.compile(r'\d{3}\d*') + + +def replace_default_num(match): + """ + Args: + match (re.Match) + Returns: + str + """ + number = match.group(0) + return verbalize_digit(number) + + +# 数字表达式 +# 纯小数 +RE_DECIMAL_NUM = re.compile(r'(-?)((\d+)(\.\d+))' r'|(\.(\d+))') +# 正整数 + 量词 +RE_POSITIVE_QUANTIFIERS = re.compile(r"(\d+)([多余几\+])?" + COM_QUANTIFIERS) +RE_NUMBER = re.compile(r'(-?)((\d+)(\.\d+)?)' r'|(\.(\d+))') + + +def replace_positive_quantifier(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + number = match.group(1) + match_2 = match.group(2) + if match_2 == "+": + match_2 = "多" + match_2: str = match_2 if match_2 else "" + quantifiers: str = match.group(3) + number: str = num2str(number) + result = f"{number}{match_2}{quantifiers}" + return result + + +def replace_number(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + number = match.group(2) + pure_decimal = match.group(5) + if pure_decimal: + result = num2str(pure_decimal) + else: + sign: str = "负" if sign else "" + number: str = num2str(number) + result = f"{sign}{number}" + return result + + +# 范围表达式 +# match.group(1) and match.group(8) are copy from RE_NUMBER + +RE_RANGE = re.compile( + r'((-?)((\d+)(\.\d+)?)|(\.(\d+)))[-~]((-?)((\d+)(\.\d+)?)|(\.(\d+)))') + + +def replace_range(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + first, second = match.group(1), match.group(8) + first = RE_NUMBER.sub(replace_number, first) + second = RE_NUMBER.sub(replace_number, second) + result = f"{first}到{second}" + return result + + +def _get_value(value_string: str, use_zero: bool=True) -> List[str]: + stripped = value_string.lstrip('0') + if len(stripped) == 0: + return [] + elif len(stripped) == 1: + if use_zero and len(stripped) < len(value_string): + return [DIGITS['0'], DIGITS[stripped]] + else: + return [DIGITS[stripped]] + else: + largest_unit = next( + power for power in reversed(UNITS.keys()) if power < len(stripped)) + first_part = value_string[:-largest_unit] + second_part = value_string[-largest_unit:] + return _get_value(first_part) + [UNITS[largest_unit]] + _get_value( + second_part) + + +def verbalize_cardinal(value_string: str) -> str: + if not value_string: + return '' + + # 000 -> '零' , 0 -> '零' + value_string = value_string.lstrip('0') + if len(value_string) == 0: + return DIGITS['0'] + + result_symbols = _get_value(value_string) + # verbalized number starting with '一十*' is abbreviated as `十*` + if len(result_symbols) >= 2 and result_symbols[0] == DIGITS[ + '1'] and result_symbols[1] == UNITS[1]: + result_symbols = result_symbols[1:] + return ''.join(result_symbols) + + +def verbalize_digit(value_string: str, alt_one=False) -> str: + result_symbols = [DIGITS[digit] for digit in value_string] + result = ''.join(result_symbols) + if alt_one: + result = result.replace("一", "幺") + return result + + +def num2str(value_string: str) -> str: + integer_decimal = value_string.split('.') + if len(integer_decimal) == 1: + integer = integer_decimal[0] + decimal = '' + elif len(integer_decimal) == 2: + integer, decimal = integer_decimal + else: + raise ValueError( + f"The value string: '${value_string}' has more than one point in it." + ) + + result = verbalize_cardinal(integer) + + decimal = decimal.rstrip('0') + if decimal: + # '.22' is verbalized as '零点二二' + # '3.20' is verbalized as '三点二 + result = result if result else "零" + result += '点' + verbalize_digit(decimal) + return result diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/phonecode.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/phonecode.py new file mode 100644 index 0000000..06b5d41 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/phonecode.py @@ -0,0 +1,63 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .num import verbalize_digit + +# 规范化固话/手机号码 +# 手机 +# http://www.jihaoba.com/news/show/13680 +# 移动:139、138、137、136、135、134、159、158、157、150、151、152、188、187、182、183、184、178、198 +# 联通:130、131、132、156、155、186、185、176 +# 电信:133、153、189、180、181、177 +RE_MOBILE_PHONE = re.compile( + r"(? str: + if mobile: + sp_parts = phone_string.strip('+').split() + result = ','.join( + [verbalize_digit(part, alt_one=True) for part in sp_parts]) + return result + else: + sil_parts = phone_string.split('-') + result = ','.join( + [verbalize_digit(part, alt_one=True) for part in sil_parts]) + return result + + +def replace_phone(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + return phone2str(match.group(0), mobile=False) + + +def replace_mobile(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + return phone2str(match.group(0)) diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/quantifier.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/quantifier.py new file mode 100644 index 0000000..268d722 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/quantifier.py @@ -0,0 +1,37 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from .num import num2str + +# 温度表达式,温度会影响负号的读法 +# -3°C 零下三度 +RE_TEMPERATURE = re.compile(r'(-?)(\d+(\.\d+)?)(°C|℃|度|摄氏度)') + + +def replace_temperature(match) -> str: + """ + Args: + match (re.Match) + Returns: + str + """ + sign = match.group(1) + temperature = match.group(2) + unit = match.group(3) + sign: str = "零下" if sign else "" + temperature: str = num2str(temperature) + unit: str = "摄氏度" if unit == "摄氏度" else "度" + result = f"{sign}{temperature}{unit}" + return result diff --git a/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/text_normlization.py b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/text_normlization.py new file mode 100644 index 0000000..bc663c7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/frontend/zh_normalization/text_normlization.py @@ -0,0 +1,116 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re +from typing import List + +from .char_convert import tranditional_to_simplified +from .chronology import RE_DATE +from .chronology import RE_DATE2 +from .chronology import RE_TIME +from .chronology import RE_TIME_RANGE +from .chronology import replace_date +from .chronology import replace_date2 +from .chronology import replace_time +from .constants import F2H_ASCII_LETTERS +from .constants import F2H_DIGITS +from .constants import F2H_SPACE +from .num import RE_DECIMAL_NUM +from .num import RE_DEFAULT_NUM +from .num import RE_FRAC +from .num import RE_INTEGER +from .num import RE_NUMBER +from .num import RE_PERCENTAGE +from .num import RE_POSITIVE_QUANTIFIERS +from .num import RE_RANGE +from .num import replace_default_num +from .num import replace_frac +from .num import replace_negative_num +from .num import replace_number +from .num import replace_percentage +from .num import replace_positive_quantifier +from .num import replace_range +from .phonecode import RE_MOBILE_PHONE +from .phonecode import RE_NATIONAL_UNIFORM_NUMBER +from .phonecode import RE_TELEPHONE +from .phonecode import replace_mobile +from .phonecode import replace_phone +from .quantifier import RE_TEMPERATURE +from .quantifier import replace_temperature + + +class TextNormalizer(): + def __init__(self): + self.SENTENCE_SPLITOR = re.compile(r'([:、,;。?!,;?!][”’]?)') + + def _split(self, text: str, lang="zh") -> List[str]: + """Split long text into sentences with sentence-splitting punctuations. + Args: + text (str): The input text. + Returns: + List[str]: Sentences. + """ + # Only for pure Chinese here + if lang == "zh": + text = text.replace(" ", "") + # 过滤掉特殊字符 + text = re.sub(r'[《》【】<=>{}()()#&@“”^_|…\\]', '', text) + text = self.SENTENCE_SPLITOR.sub(r'\1\n', text) + text = text.strip() + sentences = [sentence.strip() for sentence in re.split(r'\n+', text)] + return sentences + + def _post_replace(self, sentence: str) -> str: + sentence = sentence.replace('/', '每') + sentence = sentence.replace('~', '至') + + return sentence + + def normalize_sentence(self, sentence: str) -> str: + # basic character conversions + sentence = tranditional_to_simplified(sentence) + sentence = sentence.translate(F2H_ASCII_LETTERS).translate( + F2H_DIGITS).translate(F2H_SPACE) + + # number related NSW verbalization + sentence = RE_DATE.sub(replace_date, sentence) + sentence = RE_DATE2.sub(replace_date2, sentence) + + # range first + sentence = RE_TIME_RANGE.sub(replace_time, sentence) + sentence = RE_TIME.sub(replace_time, sentence) + + sentence = RE_TEMPERATURE.sub(replace_temperature, sentence) + sentence = RE_FRAC.sub(replace_frac, sentence) + sentence = RE_PERCENTAGE.sub(replace_percentage, sentence) + sentence = RE_MOBILE_PHONE.sub(replace_mobile, sentence) + + sentence = RE_TELEPHONE.sub(replace_phone, sentence) + sentence = RE_NATIONAL_UNIFORM_NUMBER.sub(replace_phone, sentence) + + sentence = RE_RANGE.sub(replace_range, sentence) + sentence = RE_INTEGER.sub(replace_negative_num, sentence) + sentence = RE_DECIMAL_NUM.sub(replace_number, sentence) + sentence = RE_POSITIVE_QUANTIFIERS.sub(replace_positive_quantifier, + sentence) + sentence = RE_DEFAULT_NUM.sub(replace_default_num, sentence) + sentence = RE_NUMBER.sub(replace_number, sentence) + sentence = self._post_replace(sentence) + + return sentence + + def normalize(self, text: str) -> List[str]: + sentences = self._split(text) + + sentences = [self.normalize_sentence(sent) for sent in sentences] + return sentences diff --git a/ernie-sat/paddlespeech/t2s/models/__init__.py b/ernie-sat/paddlespeech/t2s/models/__init__.py new file mode 100644 index 0000000..41be7c1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .fastspeech2 import * +from .hifigan import * +from .melgan import * +from .parallel_wavegan import * +from .speedyspeech import * +from .tacotron2 import * +from .transformer_tts import * +from .waveflow import * +from .wavernn import * diff --git a/ernie-sat/paddlespeech/t2s/models/fastspeech2/__init__.py b/ernie-sat/paddlespeech/t2s/models/fastspeech2/__init__.py new file mode 100644 index 0000000..52925ef --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/fastspeech2/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .fastspeech2 import * +from .fastspeech2_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2.py b/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2.py new file mode 100644 index 0000000..c2f1e21 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2.py @@ -0,0 +1,1057 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Fastspeech2 related modules for paddle""" +from typing import Dict +from typing import List +from typing import Sequence +from typing import Tuple +from typing import Union + +import numpy as np +import paddle +import paddle.nn.functional as F +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.nets_utils import make_non_pad_mask +from paddlespeech.t2s.modules.nets_utils import make_pad_mask +from paddlespeech.t2s.modules.predictor.duration_predictor import DurationPredictor +from paddlespeech.t2s.modules.predictor.duration_predictor import DurationPredictorLoss +from paddlespeech.t2s.modules.predictor.length_regulator import LengthRegulator +from paddlespeech.t2s.modules.predictor.variance_predictor import VariancePredictor +from paddlespeech.t2s.modules.tacotron2.decoder import Postnet +from paddlespeech.t2s.modules.transformer.encoder import CNNDecoder +from paddlespeech.t2s.modules.transformer.encoder import CNNPostnet +from paddlespeech.t2s.modules.transformer.encoder import ConformerEncoder +from paddlespeech.t2s.modules.transformer.encoder import TransformerEncoder + + +class FastSpeech2(nn.Layer): + """FastSpeech2 module. + + This is a module of FastSpeech2 described in `FastSpeech 2: Fast and + High-Quality End-to-End Text to Speech`_. Instead of quantized pitch and + energy, we use token-averaged value introduced in `FastPitch: Parallel + Text-to-speech with Pitch Prediction`_. + + .. _`FastSpeech 2: Fast and High-Quality End-to-End Text to Speech`: + https://arxiv.org/abs/2006.04558 + .. _`FastPitch: Parallel Text-to-speech with Pitch Prediction`: + https://arxiv.org/abs/2006.06873 + + Args: + + Returns: + + """ + + def __init__( + self, + # network structure related + idim: int, + odim: int, + adim: int=384, + aheads: int=4, + elayers: int=6, + eunits: int=1536, + dlayers: int=6, + dunits: int=1536, + postnet_layers: int=5, + postnet_chans: int=512, + postnet_filts: int=5, + postnet_dropout_rate: float=0.5, + positionwise_layer_type: str="conv1d", + positionwise_conv_kernel_size: int=1, + use_scaled_pos_enc: bool=True, + use_batch_norm: bool=True, + encoder_normalize_before: bool=True, + decoder_normalize_before: bool=True, + encoder_concat_after: bool=False, + decoder_concat_after: bool=False, + reduction_factor: int=1, + encoder_type: str="transformer", + decoder_type: str="transformer", + # for transformer + transformer_enc_dropout_rate: float=0.1, + transformer_enc_positional_dropout_rate: float=0.1, + transformer_enc_attn_dropout_rate: float=0.1, + transformer_dec_dropout_rate: float=0.1, + transformer_dec_positional_dropout_rate: float=0.1, + transformer_dec_attn_dropout_rate: float=0.1, + # for conformer + conformer_pos_enc_layer_type: str="rel_pos", + conformer_self_attn_layer_type: str="rel_selfattn", + conformer_activation_type: str="swish", + use_macaron_style_in_conformer: bool=True, + use_cnn_in_conformer: bool=True, + zero_triu: bool=False, + conformer_enc_kernel_size: int=7, + conformer_dec_kernel_size: int=31, + # for CNN Decoder + cnn_dec_dropout_rate: float=0.2, + cnn_postnet_dropout_rate: float=0.2, + cnn_postnet_resblock_kernel_sizes: List[int]=[256, 256], + cnn_postnet_kernel_size: int=5, + cnn_decoder_embedding_dim: int=256, + # duration predictor + duration_predictor_layers: int=2, + duration_predictor_chans: int=384, + duration_predictor_kernel_size: int=3, + duration_predictor_dropout_rate: float=0.1, + # energy predictor + energy_predictor_layers: int=2, + energy_predictor_chans: int=384, + energy_predictor_kernel_size: int=3, + energy_predictor_dropout: float=0.5, + energy_embed_kernel_size: int=9, + energy_embed_dropout: float=0.5, + stop_gradient_from_energy_predictor: bool=False, + # pitch predictor + pitch_predictor_layers: int=2, + pitch_predictor_chans: int=384, + pitch_predictor_kernel_size: int=3, + pitch_predictor_dropout: float=0.5, + pitch_embed_kernel_size: int=9, + pitch_embed_dropout: float=0.5, + stop_gradient_from_pitch_predictor: bool=False, + # spk emb + spk_num: int=None, + spk_embed_dim: int=None, + spk_embed_integration_type: str="add", + # tone emb + tone_num: int=None, + tone_embed_dim: int=None, + tone_embed_integration_type: str="add", + # training related + init_type: str="xavier_uniform", + init_enc_alpha: float=1.0, + init_dec_alpha: float=1.0, ): + """Initialize FastSpeech2 module. + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + adim (int): Attention dimension. + aheads (int): Number of attention heads. + elayers (int): Number of encoder layers. + eunits (int): Number of encoder hidden units. + dlayers (int): Number of decoder layers. + dunits (int): Number of decoder hidden units. + postnet_layers (int): Number of postnet layers. + postnet_chans (int): Number of postnet channels. + postnet_filts (int): Kernel size of postnet. + postnet_dropout_rate (float): Dropout rate in postnet. + use_scaled_pos_enc (bool): Whether to use trainable scaled pos encoding. + use_batch_norm (bool): Whether to use batch normalization in encoder prenet. + encoder_normalize_before (bool): Whether to apply layernorm layer before encoder block. + decoder_normalize_before (bool): Whether to apply layernorm layer before decoder block. + encoder_concat_after (bool): Whether to concatenate attention layer's input and output in encoder. + decoder_concat_after (bool): Whether to concatenate attention layer's input and output in decoder. + reduction_factor (int): Reduction factor. + encoder_type (str): Encoder type ("transformer" or "conformer"). + decoder_type (str): Decoder type ("transformer" or "conformer"). + transformer_enc_dropout_rate (float): Dropout rate in encoder except attention and positional encoding. + transformer_enc_positional_dropout_rate (float): Dropout rate after encoder positional encoding. + transformer_enc_attn_dropout_rate (float): Dropout rate in encoder self-attention module. + transformer_dec_dropout_rate (float): Dropout rate in decoder except attention & positional encoding. + transformer_dec_positional_dropout_rate (float): Dropout rate after decoder positional encoding. + transformer_dec_attn_dropout_rate (float): Dropout rate in decoder self-attention module. + conformer_pos_enc_layer_type (str): Pos encoding layer type in conformer. + conformer_self_attn_layer_type (str): Self-attention layer type in conformer + conformer_activation_type (str): Activation function type in conformer. + use_macaron_style_in_conformer (bool): Whether to use macaron style FFN. + use_cnn_in_conformer (bool): Whether to use CNN in conformer. + zero_triu (bool): Whether to use zero triu in relative self-attention module. + conformer_enc_kernel_size (int): Kernel size of encoder conformer. + conformer_dec_kernel_size (int): Kernel size of decoder conformer. + duration_predictor_layers (int): Number of duration predictor layers. + duration_predictor_chans (int): Number of duration predictor channels. + duration_predictor_kernel_size (int): Kernel size of duration predictor. + duration_predictor_dropout_rate (float): Dropout rate in duration predictor. + pitch_predictor_layers (int): Number of pitch predictor layers. + pitch_predictor_chans (int): Number of pitch predictor channels. + pitch_predictor_kernel_size (int): Kernel size of pitch predictor. + pitch_predictor_dropout_rate (float): Dropout rate in pitch predictor. + pitch_embed_kernel_size (float): Kernel size of pitch embedding. + pitch_embed_dropout_rate (float): Dropout rate for pitch embedding. + stop_gradient_from_pitch_predictor (bool): Whether to stop gradient from pitch predictor to encoder. + energy_predictor_layers (int): Number of energy predictor layers. + energy_predictor_chans (int): Number of energy predictor channels. + energy_predictor_kernel_size (int): Kernel size of energy predictor. + energy_predictor_dropout_rate (float): Dropout rate in energy predictor. + energy_embed_kernel_size (float): Kernel size of energy embedding. + energy_embed_dropout_rate (float): Dropout rate for energy embedding. + stop_gradient_from_energy_predictor(bool): Whether to stop gradient from energy predictor to encoder. + spk_num (Optional[int]): Number of speakers. If not None, assume that the spk_embed_dim is not None, + spk_ids will be provided as the input and use spk_embedding_table. + spk_embed_dim (Optional[int]): Speaker embedding dimension. If not None, + assume that spk_emb will be provided as the input or spk_num is not None. + spk_embed_integration_type (str): How to integrate speaker embedding. + tone_num (Optional[int]): Number of tones. If not None, assume that the + tone_ids will be provided as the input and use tone_embedding_table. + tone_embed_dim (Optional[int]): Tone embedding dimension. If not None, assume that tone_num is not None. + tone_embed_integration_type (str): How to integrate tone embedding. + init_type (str): How to initialize transformer parameters. + init_enc_alpha (float): Initial value of alpha in scaled pos encoding of the encoder. + init_dec_alpha (float): Initial value of alpha in scaled pos encoding of the decoder. + + """ + assert check_argument_types() + super().__init__() + + # store hyperparameters + self.idim = idim + self.odim = odim + self.eos = idim - 1 + self.reduction_factor = reduction_factor + self.encoder_type = encoder_type + self.decoder_type = decoder_type + self.stop_gradient_from_pitch_predictor = stop_gradient_from_pitch_predictor + self.stop_gradient_from_energy_predictor = stop_gradient_from_energy_predictor + self.use_scaled_pos_enc = use_scaled_pos_enc + + self.spk_embed_dim = spk_embed_dim + if self.spk_embed_dim is not None: + self.spk_embed_integration_type = spk_embed_integration_type + + self.tone_embed_dim = tone_embed_dim + if self.tone_embed_dim is not None: + self.tone_embed_integration_type = tone_embed_integration_type + + # use idx 0 as padding idx + self.padding_idx = 0 + + # initialize parameters + initialize(self, init_type) + + if spk_num and self.spk_embed_dim: + self.spk_embedding_table = nn.Embedding( + num_embeddings=spk_num, + embedding_dim=self.spk_embed_dim, + padding_idx=self.padding_idx) + + if self.tone_embed_dim is not None: + self.tone_embedding_table = nn.Embedding( + num_embeddings=tone_num, + embedding_dim=self.tone_embed_dim, + padding_idx=self.padding_idx) + + # get positional encoding layer type + transformer_pos_enc_layer_type = "scaled_abs_pos" if self.use_scaled_pos_enc else "abs_pos" + + # define encoder + encoder_input_layer = nn.Embedding( + num_embeddings=idim, + embedding_dim=adim, + padding_idx=self.padding_idx) + + if encoder_type == "transformer": + print("encoder_type is transformer") + self.encoder = TransformerEncoder( + idim=idim, + attention_dim=adim, + attention_heads=aheads, + linear_units=eunits, + num_blocks=elayers, + input_layer=encoder_input_layer, + dropout_rate=transformer_enc_dropout_rate, + positional_dropout_rate=transformer_enc_positional_dropout_rate, + attention_dropout_rate=transformer_enc_attn_dropout_rate, + pos_enc_layer_type=transformer_pos_enc_layer_type, + normalize_before=encoder_normalize_before, + concat_after=encoder_concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, ) + elif encoder_type == "conformer": + print("encoder_type is conformer") + self.encoder = ConformerEncoder( + idim=idim, + attention_dim=adim, + attention_heads=aheads, + linear_units=eunits, + num_blocks=elayers, + input_layer=encoder_input_layer, + dropout_rate=transformer_enc_dropout_rate, + positional_dropout_rate=transformer_enc_positional_dropout_rate, + attention_dropout_rate=transformer_enc_attn_dropout_rate, + normalize_before=encoder_normalize_before, + concat_after=encoder_concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, + macaron_style=use_macaron_style_in_conformer, + pos_enc_layer_type=conformer_pos_enc_layer_type, + selfattention_layer_type=conformer_self_attn_layer_type, + activation_type=conformer_activation_type, + use_cnn_module=use_cnn_in_conformer, + cnn_module_kernel=conformer_enc_kernel_size, + zero_triu=zero_triu, ) + else: + raise ValueError(f"{encoder_type} is not supported.") + + # define additional projection for speaker embedding + if self.spk_embed_dim is not None: + if self.spk_embed_integration_type == "add": + self.spk_projection = nn.Linear(self.spk_embed_dim, adim) + else: + self.spk_projection = nn.Linear(adim + self.spk_embed_dim, adim) + + # define additional projection for tone embedding + if self.tone_embed_dim is not None: + if self.tone_embed_integration_type == "add": + self.tone_projection = nn.Linear(self.tone_embed_dim, adim) + else: + self.tone_projection = nn.Linear(adim + self.tone_embed_dim, + adim) + + # define duration predictor + self.duration_predictor = DurationPredictor( + idim=adim, + n_layers=duration_predictor_layers, + n_chans=duration_predictor_chans, + kernel_size=duration_predictor_kernel_size, + dropout_rate=duration_predictor_dropout_rate, ) + + # define pitch predictor + self.pitch_predictor = VariancePredictor( + idim=adim, + n_layers=pitch_predictor_layers, + n_chans=pitch_predictor_chans, + kernel_size=pitch_predictor_kernel_size, + dropout_rate=pitch_predictor_dropout, ) + # We use continuous pitch + FastPitch style avg + self.pitch_embed = nn.Sequential( + nn.Conv1D( + in_channels=1, + out_channels=adim, + kernel_size=pitch_embed_kernel_size, + padding=(pitch_embed_kernel_size - 1) // 2, ), + nn.Dropout(pitch_embed_dropout), ) + + # define energy predictor + self.energy_predictor = VariancePredictor( + idim=adim, + n_layers=energy_predictor_layers, + n_chans=energy_predictor_chans, + kernel_size=energy_predictor_kernel_size, + dropout_rate=energy_predictor_dropout, ) + # We use continuous enegy + FastPitch style avg + self.energy_embed = nn.Sequential( + nn.Conv1D( + in_channels=1, + out_channels=adim, + kernel_size=energy_embed_kernel_size, + padding=(energy_embed_kernel_size - 1) // 2, ), + nn.Dropout(energy_embed_dropout), ) + + # define length regulator + self.length_regulator = LengthRegulator() + + # define decoder + # NOTE: we use encoder as decoder + # because fastspeech's decoder is the same as encoder + if decoder_type == "transformer": + print("decoder_type is transformer") + self.decoder = TransformerEncoder( + idim=0, + attention_dim=adim, + attention_heads=aheads, + linear_units=dunits, + num_blocks=dlayers, + # in decoder, don't need layer before pos_enc_class (we use embedding here in encoder) + input_layer=None, + dropout_rate=transformer_dec_dropout_rate, + positional_dropout_rate=transformer_dec_positional_dropout_rate, + attention_dropout_rate=transformer_dec_attn_dropout_rate, + pos_enc_layer_type=transformer_pos_enc_layer_type, + normalize_before=decoder_normalize_before, + concat_after=decoder_concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, ) + elif decoder_type == "conformer": + print("decoder_type is conformer") + self.decoder = ConformerEncoder( + idim=0, + attention_dim=adim, + attention_heads=aheads, + linear_units=dunits, + num_blocks=dlayers, + input_layer=None, + dropout_rate=transformer_dec_dropout_rate, + positional_dropout_rate=transformer_dec_positional_dropout_rate, + attention_dropout_rate=transformer_dec_attn_dropout_rate, + normalize_before=decoder_normalize_before, + concat_after=decoder_concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, + macaron_style=use_macaron_style_in_conformer, + pos_enc_layer_type=conformer_pos_enc_layer_type, + selfattention_layer_type=conformer_self_attn_layer_type, + activation_type=conformer_activation_type, + use_cnn_module=use_cnn_in_conformer, + cnn_module_kernel=conformer_dec_kernel_size, ) + elif decoder_type == 'cnndecoder': + self.decoder = CNNDecoder( + emb_dim=adim, + odim=odim, + kernel_size=cnn_postnet_kernel_size, + dropout_rate=cnn_dec_dropout_rate, + resblock_kernel_sizes=cnn_postnet_resblock_kernel_sizes) + else: + raise ValueError(f"{decoder_type} is not supported.") + + # define final projection + self.feat_out = nn.Linear(adim, odim * reduction_factor) + + # define postnet + if decoder_type == 'cnndecoder': + self.postnet = CNNPostnet( + odim=odim, + kernel_size=cnn_postnet_kernel_size, + dropout_rate=cnn_postnet_dropout_rate, + resblock_kernel_sizes=cnn_postnet_resblock_kernel_sizes) + else: + self.postnet = (None if postnet_layers == 0 else Postnet( + idim=idim, + odim=odim, + n_layers=postnet_layers, + n_chans=postnet_chans, + n_filts=postnet_filts, + use_batch_norm=use_batch_norm, + dropout_rate=postnet_dropout_rate, )) + + nn.initializer.set_global_initializer(None) + + self._reset_parameters( + init_enc_alpha=init_enc_alpha, + init_dec_alpha=init_dec_alpha, ) + + def forward( + self, + text: paddle.Tensor, + text_lengths: paddle.Tensor, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + durations: paddle.Tensor, + pitch: paddle.Tensor, + energy: paddle.Tensor, + tone_id: paddle.Tensor=None, + spk_emb: paddle.Tensor=None, + spk_id: paddle.Tensor=None + ) -> Tuple[paddle.Tensor, Dict[str, paddle.Tensor], paddle.Tensor]: + """Calculate forward propagation. + + Args: + text(Tensor(int64)): Batch of padded token ids (B, Tmax). + text_lengths(Tensor(int64)): Batch of lengths of each input (B,). + speech(Tensor): Batch of padded target features (B, Lmax, odim). + speech_lengths(Tensor(int64)): Batch of the lengths of each target (B,). + durations(Tensor(int64)): Batch of padded durations (B, Tmax). + pitch(Tensor): Batch of padded token-averaged pitch (B, Tmax, 1). + energy(Tensor): Batch of padded token-averaged energy (B, Tmax, 1). + tone_id(Tensor, optional(int64)): Batch of padded tone ids (B, Tmax). + spk_emb(Tensor, optional): Batch of speaker embeddings (B, spk_embed_dim). + spk_id(Tnesor, optional(int64)): Batch of speaker ids (B,) + + Returns: + + + """ + + # input of embedding must be int64 + xs = paddle.cast(text, 'int64') + ilens = paddle.cast(text_lengths, 'int64') + ds = paddle.cast(durations, 'int64') + olens = paddle.cast(speech_lengths, 'int64') + ys = speech + ps = pitch + es = energy + if spk_id is not None: + spk_id = paddle.cast(spk_id, 'int64') + if tone_id is not None: + tone_id = paddle.cast(tone_id, 'int64') + # forward propagation + before_outs, after_outs, d_outs, p_outs, e_outs = self._forward( + xs, + ilens, + olens, + ds, + ps, + es, + is_inference=False, + spk_emb=spk_emb, + spk_id=spk_id, + tone_id=tone_id) + # modify mod part of groundtruth + if self.reduction_factor > 1: + olens = olens - olens % self.reduction_factor + max_olen = max(olens) + ys = ys[:, :max_olen] + + return before_outs, after_outs, d_outs, p_outs, e_outs, ys, olens + + def _forward(self, + xs: paddle.Tensor, + ilens: paddle.Tensor, + olens: paddle.Tensor=None, + ds: paddle.Tensor=None, + ps: paddle.Tensor=None, + es: paddle.Tensor=None, + is_inference: bool=False, + return_after_enc=False, + alpha: float=1.0, + spk_emb=None, + spk_id=None, + tone_id=None) -> Sequence[paddle.Tensor]: + # forward encoder + x_masks = self._source_mask(ilens) + # (B, Tmax, adim) + hs, _ = self.encoder(xs, x_masks) + + # integrate speaker embedding + if self.spk_embed_dim is not None: + # spk_emb has a higher priority than spk_id + if spk_emb is not None: + hs = self._integrate_with_spk_embed(hs, spk_emb) + elif spk_id is not None: + spk_emb = self.spk_embedding_table(spk_id) + hs = self._integrate_with_spk_embed(hs, spk_emb) + + # integrate tone embedding + if self.tone_embed_dim is not None: + if tone_id is not None: + tone_embs = self.tone_embedding_table(tone_id) + hs = self._integrate_with_tone_embed(hs, tone_embs) + # forward duration predictor and variance predictors + d_masks = make_pad_mask(ilens) + + if self.stop_gradient_from_pitch_predictor: + p_outs = self.pitch_predictor(hs.detach(), d_masks.unsqueeze(-1)) + else: + p_outs = self.pitch_predictor(hs, d_masks.unsqueeze(-1)) + if self.stop_gradient_from_energy_predictor: + e_outs = self.energy_predictor(hs.detach(), d_masks.unsqueeze(-1)) + else: + e_outs = self.energy_predictor(hs, d_masks.unsqueeze(-1)) + + if is_inference: + # (B, Tmax) + if ds is not None: + d_outs = ds + else: + d_outs = self.duration_predictor.inference(hs, d_masks) + if ps is not None: + p_outs = ps + if es is not None: + e_outs = es + + # use prediction in inference + # (B, Tmax, 1) + + p_embs = self.pitch_embed(p_outs.transpose((0, 2, 1))).transpose( + (0, 2, 1)) + e_embs = self.energy_embed(e_outs.transpose((0, 2, 1))).transpose( + (0, 2, 1)) + hs = hs + e_embs + p_embs + + # (B, Lmax, adim) + hs = self.length_regulator(hs, d_outs, alpha, is_inference=True) + else: + d_outs = self.duration_predictor(hs, d_masks) + # use groundtruth in training + p_embs = self.pitch_embed(ps.transpose((0, 2, 1))).transpose( + (0, 2, 1)) + e_embs = self.energy_embed(es.transpose((0, 2, 1))).transpose( + (0, 2, 1)) + hs = hs + e_embs + p_embs + + # (B, Lmax, adim) + hs = self.length_regulator(hs, ds, is_inference=False) + + # forward decoder + if olens is not None and not is_inference: + if self.reduction_factor > 1: + olens_in = paddle.to_tensor( + [olen // self.reduction_factor for olen in olens.numpy()]) + else: + olens_in = olens + # (B, 1, T) + h_masks = self._source_mask(olens_in) + else: + h_masks = None + + if return_after_enc: + return hs, h_masks + # (B, Lmax, adim) + zs, _ = self.decoder(hs, h_masks) + # (B, Lmax, odim) + if self.decoder_type == 'cnndecoder': + before_outs = zs + else: + before_outs = self.feat_out(zs).reshape( + (paddle.shape(zs)[0], -1, self.odim)) + + # postnet -> (B, Lmax//r * r, odim) + if self.postnet is None: + after_outs = before_outs + else: + after_outs = before_outs + self.postnet( + before_outs.transpose((0, 2, 1))).transpose((0, 2, 1)) + + return before_outs, after_outs, d_outs, p_outs, e_outs + + def encoder_infer( + self, + text: paddle.Tensor, + alpha: float=1.0, + spk_emb=None, + spk_id=None, + tone_id=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + # input of embedding must be int64 + x = paddle.cast(text, 'int64') + # setup batch axis + ilens = paddle.shape(x)[0] + + xs = x.unsqueeze(0) + + if spk_emb is not None: + spk_emb = spk_emb.unsqueeze(0) + + if tone_id is not None: + tone_id = tone_id.unsqueeze(0) + + # (1, L, odim) + hs, h_masks = self._forward( + xs, + ilens, + is_inference=True, + return_after_enc=True, + alpha=alpha, + spk_emb=spk_emb, + spk_id=spk_id, + tone_id=tone_id) + return hs, h_masks + + def inference( + self, + text: paddle.Tensor, + durations: paddle.Tensor=None, + pitch: paddle.Tensor=None, + energy: paddle.Tensor=None, + alpha: float=1.0, + use_teacher_forcing: bool=False, + spk_emb=None, + spk_id=None, + tone_id=None, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Generate the sequence of features given the sequences of characters. + + Args: + text(Tensor(int64)): Input sequence of characters (T,). + durations(Tensor, optional (int64)): Groundtruth of duration (T,). + pitch(Tensor, optional): Groundtruth of token-averaged pitch (T, 1). + energy(Tensor, optional): Groundtruth of token-averaged energy (T, 1). + alpha(float, optional): Alpha to control the speed. + use_teacher_forcing(bool, optional): Whether to use teacher forcing. + If true, groundtruth of duration, pitch and energy will be used. + spk_emb(Tensor, optional, optional): peaker embedding vector (spk_embed_dim,). (Default value = None) + spk_id(Tensor, optional(int64), optional): Batch of padded spk ids (1,). (Default value = None) + tone_id(Tensor, optional(int64), optional): Batch of padded tone ids (T,). (Default value = None) + + Returns: + + + """ + # input of embedding must be int64 + x = paddle.cast(text, 'int64') + d, p, e = durations, pitch, energy + # setup batch axis + ilens = paddle.shape(x)[0] + + xs = x.unsqueeze(0) + + if spk_emb is not None: + spk_emb = spk_emb.unsqueeze(0) + + if tone_id is not None: + tone_id = tone_id.unsqueeze(0) + + if use_teacher_forcing: + # use groundtruth of duration, pitch, and energy + ds = d.unsqueeze(0) if d is not None else None + ps = p.unsqueeze(0) if p is not None else None + es = e.unsqueeze(0) if e is not None else None + + # (1, L, odim) + _, outs, d_outs, p_outs, e_outs = self._forward( + xs, + ilens, + ds=ds, + ps=ps, + es=es, + spk_emb=spk_emb, + spk_id=spk_id, + tone_id=tone_id, + is_inference=True) + else: + # (1, L, odim) + _, outs, d_outs, p_outs, e_outs = self._forward( + xs, + ilens, + is_inference=True, + alpha=alpha, + spk_emb=spk_emb, + spk_id=spk_id, + tone_id=tone_id) + return outs[0], d_outs[0], p_outs[0], e_outs[0] + + def _integrate_with_spk_embed(self, hs, spk_emb): + """Integrate speaker embedding with hidden states. + + Args: + hs(Tensor): Batch of hidden state sequences (B, Tmax, adim). + spk_emb(Tensor): Batch of speaker embeddings (B, spk_embed_dim). + + Returns: + + + """ + if self.spk_embed_integration_type == "add": + # apply projection and then add to hidden states + spk_emb = self.spk_projection(F.normalize(spk_emb)) + hs = hs + spk_emb.unsqueeze(1) + elif self.spk_embed_integration_type == "concat": + # concat hidden states with spk embeds and then apply projection + spk_emb = F.normalize(spk_emb).unsqueeze(1).expand( + shape=[-1, paddle.shape(hs)[1], -1]) + hs = self.spk_projection(paddle.concat([hs, spk_emb], axis=-1)) + else: + raise NotImplementedError("support only add or concat.") + + return hs + + def _integrate_with_tone_embed(self, hs, tone_embs): + """Integrate speaker embedding with hidden states. + + Args: + hs(Tensor): Batch of hidden state sequences (B, Tmax, adim). + tone_embs(Tensor): Batch of speaker embeddings (B, Tmax, tone_embed_dim). + + Returns: + + + """ + if self.tone_embed_integration_type == "add": + # apply projection and then add to hidden states + tone_embs = self.tone_projection(F.normalize(tone_embs)) + hs = hs + tone_embs + + elif self.tone_embed_integration_type == "concat": + # concat hidden states with tone embeds and then apply projection + tone_embs = F.normalize(tone_embs).expand( + shape=[-1, hs.shape[1], -1]) + hs = self.tone_projection(paddle.concat([hs, tone_embs], axis=-1)) + else: + raise NotImplementedError("support only add or concat.") + return hs + + def _source_mask(self, ilens: paddle.Tensor) -> paddle.Tensor: + """Make masks for self-attention. + + Args: + ilens(Tensor): Batch of lengths (B,). + + Returns: + Tensor: Mask tensor for self-attention. dtype=paddle.bool + + Examples: + >>> ilens = [5, 3] + >>> self._source_mask(ilens) + tensor([[[1, 1, 1, 1, 1], + [1, 1, 1, 0, 0]]]) bool + """ + x_masks = make_non_pad_mask(ilens) + return x_masks.unsqueeze(-2) + + def _reset_parameters(self, init_enc_alpha: float, init_dec_alpha: float): + + # initialize alpha in scaled positional encoding + if self.encoder_type == "transformer" and self.use_scaled_pos_enc: + init_enc_alpha = paddle.to_tensor(init_enc_alpha) + self.encoder.embed[-1].alpha = paddle.create_parameter( + shape=init_enc_alpha.shape, + dtype=str(init_enc_alpha.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign( + init_enc_alpha)) + if self.decoder_type == "transformer" and self.use_scaled_pos_enc: + init_dec_alpha = paddle.to_tensor(init_dec_alpha) + self.decoder.embed[-1].alpha = paddle.create_parameter( + shape=init_dec_alpha.shape, + dtype=str(init_dec_alpha.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign( + init_dec_alpha)) + + +class FastSpeech2Inference(nn.Layer): + def __init__(self, normalizer, model): + super().__init__() + self.normalizer = normalizer + self.acoustic_model = model + + def forward(self, text, spk_id=None, spk_emb=None): + normalized_mel, d_outs, p_outs, e_outs = self.acoustic_model.inference( + text, spk_id=spk_id, spk_emb=spk_emb) + logmel = self.normalizer.inverse(normalized_mel) + return logmel + + +class StyleFastSpeech2Inference(FastSpeech2Inference): + def __init__(self, + normalizer, + model, + pitch_stats_path=None, + energy_stats_path=None): + super().__init__(normalizer, model) + if pitch_stats_path: + pitch_mean, pitch_std = np.load(pitch_stats_path) + self.pitch_mean = paddle.to_tensor(pitch_mean) + self.pitch_std = paddle.to_tensor(pitch_std) + if energy_stats_path: + energy_mean, energy_std = np.load(energy_stats_path) + self.energy_mean = paddle.to_tensor(energy_mean) + self.energy_std = paddle.to_tensor(energy_std) + + def denorm(self, data, mean, std): + return data * std + mean + + def norm(self, data, mean, std): + return (data - mean) / std + + def forward(self, + text: paddle.Tensor, + durations: Union[paddle.Tensor, np.ndarray]=None, + durations_scale: Union[int, float]=None, + durations_bias: Union[int, float]=None, + pitch: Union[paddle.Tensor, np.ndarray]=None, + pitch_scale: Union[int, float]=None, + pitch_bias: Union[int, float]=None, + energy: Union[paddle.Tensor, np.ndarray]=None, + energy_scale: Union[int, float]=None, + energy_bias: Union[int, float]=None, + robot: bool=False, + spk_emb=None, + spk_id=None): + """ + + Args: + text(Tensor(int64)): Input sequence of characters (T,). + durations(paddle.Tensor/np.ndarray, optional (int64)): Groundtruth of duration (T,), this will overwrite the set of durations_scale and durations_bias + durations_scale(int/float, optional): + durations_bias(int/float, optional): + pitch(paddle.Tensor/np.ndarray, optional): Groundtruth of token-averaged pitch (T, 1), this will overwrite the set of pitch_scale and pitch_bias + pitch_scale(int/float, optional): In denormed HZ domain. + pitch_bias(int/float, optional): In denormed HZ domain. + energy(paddle.Tensor/np.ndarray, optional): Groundtruth of token-averaged energy (T, 1), this will overwrite the set of energy_scale and energy_bias + energy_scale(int/float, optional): In denormed domain. + energy_bias(int/float, optional): In denormed domain. + robot: bool: (Default value = False) + spk_emb: (Default value = None) + spk_id: (Default value = None) + + Returns: + Tensor: logmel + + """ + normalized_mel, d_outs, p_outs, e_outs = self.acoustic_model.inference( + text, + durations=None, + pitch=None, + energy=None, + spk_emb=spk_emb, + spk_id=spk_id) + # priority: groundtruth > scale/bias > previous output + # set durations + if isinstance(durations, np.ndarray): + durations = paddle.to_tensor(durations) + elif isinstance(durations, paddle.Tensor): + durations = durations + elif durations_scale or durations_bias: + durations_scale = durations_scale if durations_scale is not None else 1 + durations_bias = durations_bias if durations_bias is not None else 0 + durations = durations_scale * d_outs + durations_bias + else: + durations = d_outs + + if robot: + # set normed pitch to zeros have the same effect with set denormd ones to mean + pitch = paddle.zeros(p_outs.shape) + + # set pitch, can overwrite robot set + if isinstance(pitch, np.ndarray): + pitch = paddle.to_tensor(pitch) + elif isinstance(pitch, paddle.Tensor): + pitch = pitch + elif pitch_scale or pitch_bias: + pitch_scale = pitch_scale if pitch_scale is not None else 1 + pitch_bias = pitch_bias if pitch_bias is not None else 0 + p_Hz = paddle.exp( + self.denorm(p_outs, self.pitch_mean, self.pitch_std)) + p_HZ = pitch_scale * p_Hz + pitch_bias + pitch = self.norm(paddle.log(p_HZ), self.pitch_mean, self.pitch_std) + else: + pitch = p_outs + + # set energy + if isinstance(energy, np.ndarray): + energy = paddle.to_tensor(energy) + elif isinstance(energy, paddle.Tensor): + energy = energy + elif energy_scale or energy_bias: + energy_scale = energy_scale if energy_scale is not None else 1 + energy_bias = energy_bias if energy_bias is not None else 0 + e_dnorm = self.denorm(e_outs, self.energy_mean, self.energy_std) + e_dnorm = energy_scale * e_dnorm + energy_bias + energy = self.norm(e_dnorm, self.energy_mean, self.energy_std) + else: + energy = e_outs + + normalized_mel, d_outs, p_outs, e_outs = self.acoustic_model.inference( + text, + durations=durations, + pitch=pitch, + energy=energy, + use_teacher_forcing=True, + spk_emb=spk_emb, + spk_id=spk_id) + + logmel = self.normalizer.inverse(normalized_mel) + return logmel + + +class FastSpeech2Loss(nn.Layer): + """Loss function module for FastSpeech2.""" + + def __init__(self, use_masking: bool=True, + use_weighted_masking: bool=False): + """Initialize feed-forward Transformer loss module. + Args: + use_masking (bool): Whether to apply masking for padded part in loss calculation. + use_weighted_masking (bool): Whether to weighted masking in loss calculation. + """ + assert check_argument_types() + super().__init__() + + assert (use_masking != use_weighted_masking) or not use_masking + self.use_masking = use_masking + self.use_weighted_masking = use_weighted_masking + + # define criterions + reduction = "none" if self.use_weighted_masking else "mean" + self.l1_criterion = nn.L1Loss(reduction=reduction) + self.mse_criterion = nn.MSELoss(reduction=reduction) + self.duration_criterion = DurationPredictorLoss(reduction=reduction) + + def forward( + self, + after_outs: paddle.Tensor, + before_outs: paddle.Tensor, + d_outs: paddle.Tensor, + p_outs: paddle.Tensor, + e_outs: paddle.Tensor, + ys: paddle.Tensor, + ds: paddle.Tensor, + ps: paddle.Tensor, + es: paddle.Tensor, + ilens: paddle.Tensor, + olens: paddle.Tensor, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Calculate forward propagation. + + Args: + after_outs(Tensor): Batch of outputs after postnets (B, Lmax, odim). + before_outs(Tensor): Batch of outputs before postnets (B, Lmax, odim). + d_outs(Tensor): Batch of outputs of duration predictor (B, Tmax). + p_outs(Tensor): Batch of outputs of pitch predictor (B, Tmax, 1). + e_outs(Tensor): Batch of outputs of energy predictor (B, Tmax, 1). + ys(Tensor): Batch of target features (B, Lmax, odim). + ds(Tensor): Batch of durations (B, Tmax). + ps(Tensor): Batch of target token-averaged pitch (B, Tmax, 1). + es(Tensor): Batch of target token-averaged energy (B, Tmax, 1). + ilens(Tensor): Batch of the lengths of each input (B,). + olens(Tensor): Batch of the lengths of each target (B,). + + Returns: + + + """ + # apply mask to remove padded part + if self.use_masking: + out_masks = make_non_pad_mask(olens).unsqueeze(-1) + before_outs = before_outs.masked_select( + out_masks.broadcast_to(before_outs.shape)) + if after_outs is not None: + after_outs = after_outs.masked_select( + out_masks.broadcast_to(after_outs.shape)) + ys = ys.masked_select(out_masks.broadcast_to(ys.shape)) + duration_masks = make_non_pad_mask(ilens) + d_outs = d_outs.masked_select( + duration_masks.broadcast_to(d_outs.shape)) + ds = ds.masked_select(duration_masks.broadcast_to(ds.shape)) + pitch_masks = make_non_pad_mask(ilens).unsqueeze(-1) + p_outs = p_outs.masked_select( + pitch_masks.broadcast_to(p_outs.shape)) + e_outs = e_outs.masked_select( + pitch_masks.broadcast_to(e_outs.shape)) + ps = ps.masked_select(pitch_masks.broadcast_to(ps.shape)) + es = es.masked_select(pitch_masks.broadcast_to(es.shape)) + + # calculate loss + l1_loss = self.l1_criterion(before_outs, ys) + if after_outs is not None: + l1_loss += self.l1_criterion(after_outs, ys) + duration_loss = self.duration_criterion(d_outs, ds) + pitch_loss = self.mse_criterion(p_outs, ps) + energy_loss = self.mse_criterion(e_outs, es) + + # make weighted mask and apply it + if self.use_weighted_masking: + out_masks = make_non_pad_mask(olens).unsqueeze(-1) + out_weights = out_masks.cast(dtype=paddle.float32) / out_masks.cast( + dtype=paddle.float32).sum( + axis=1, keepdim=True) + out_weights /= ys.shape[0] * ys.shape[2] + duration_masks = make_non_pad_mask(ilens) + duration_weights = (duration_masks.cast(dtype=paddle.float32) / + duration_masks.cast(dtype=paddle.float32).sum( + axis=1, keepdim=True)) + duration_weights /= ds.shape[0] + + # apply weight + + l1_loss = l1_loss.multiply(out_weights) + l1_loss = l1_loss.masked_select( + out_masks.broadcast_to(l1_loss.shape)).sum() + duration_loss = (duration_loss.multiply(duration_weights) + .masked_select(duration_masks).sum()) + pitch_masks = duration_masks.unsqueeze(-1) + pitch_weights = duration_weights.unsqueeze(-1) + pitch_loss = pitch_loss.multiply(pitch_weights) + pitch_loss = pitch_loss.masked_select( + pitch_masks.broadcast_to(pitch_loss.shape)).sum() + energy_loss = energy_loss.multiply(pitch_weights) + energy_loss = energy_loss.masked_select( + pitch_masks.broadcast_to(energy_loss.shape)).sum() + + return l1_loss, duration_loss, pitch_loss, energy_loss diff --git a/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2_updater.py b/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2_updater.py new file mode 100644 index 0000000..92aa9df --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/fastspeech2/fastspeech2_updater.py @@ -0,0 +1,174 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path + +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer + +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2Loss +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class FastSpeech2Updater(StandardUpdater): + def __init__(self, + model: Layer, + optimizer: Optimizer, + dataloader: DataLoader, + init_state=None, + use_masking: bool=False, + use_weighted_masking: bool=False, + output_dir: Path=None): + super().__init__(model, optimizer, dataloader, init_state=None) + + self.criterion = FastSpeech2Loss( + use_masking=use_masking, use_weighted_masking=use_weighted_masking) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # spk_id!=None in multiple spk fastspeech2 + spk_id = batch["spk_id"] if "spk_id" in batch else None + spk_emb = batch["spk_emb"] if "spk_emb" in batch else None + # No explicit speaker identifier labels are used during voice cloning training. + if spk_emb is not None: + spk_id = None + + before_outs, after_outs, d_outs, p_outs, e_outs, ys, olens = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"], + durations=batch["durations"], + pitch=batch["pitch"], + energy=batch["energy"], + spk_id=spk_id, + spk_emb=spk_emb) + + l1_loss, duration_loss, pitch_loss, energy_loss = self.criterion( + after_outs=after_outs, + before_outs=before_outs, + d_outs=d_outs, + p_outs=p_outs, + e_outs=e_outs, + ys=ys, + ds=batch["durations"], + ps=batch["pitch"], + es=batch["energy"], + ilens=batch["text_lengths"], + olens=olens) + + loss = l1_loss + duration_loss + pitch_loss + energy_loss + + optimizer = self.optimizer + optimizer.clear_grad() + loss.backward() + optimizer.step() + + report("train/loss", float(loss)) + report("train/l1_loss", float(l1_loss)) + report("train/duration_loss", float(duration_loss)) + report("train/pitch_loss", float(pitch_loss)) + report("train/energy_loss", float(energy_loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["duration_loss"] = float(duration_loss) + losses_dict["pitch_loss"] = float(pitch_loss) + losses_dict["energy_loss"] = float(energy_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class FastSpeech2Evaluator(StandardEvaluator): + def __init__(self, + model: Layer, + dataloader: DataLoader, + use_masking: bool=False, + use_weighted_masking: bool=False, + output_dir: Path=None): + super().__init__(model, dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + self.criterion = FastSpeech2Loss( + use_masking=use_masking, use_weighted_masking=use_weighted_masking) + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + # spk_id!=None in multiple spk fastspeech2 + spk_id = batch["spk_id"] if "spk_id" in batch else None + spk_emb = batch["spk_emb"] if "spk_emb" in batch else None + if spk_emb is not None: + spk_id = None + + before_outs, after_outs, d_outs, p_outs, e_outs, ys, olens = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"], + durations=batch["durations"], + pitch=batch["pitch"], + energy=batch["energy"], + spk_id=spk_id, + spk_emb=spk_emb) + + l1_loss, duration_loss, pitch_loss, energy_loss = self.criterion( + after_outs=after_outs, + before_outs=before_outs, + d_outs=d_outs, + p_outs=p_outs, + e_outs=e_outs, + ys=ys, + ds=batch["durations"], + ps=batch["pitch"], + es=batch["energy"], + ilens=batch["text_lengths"], + olens=olens, ) + loss = l1_loss + duration_loss + pitch_loss + energy_loss + + report("eval/loss", float(loss)) + report("eval/l1_loss", float(l1_loss)) + report("eval/duration_loss", float(duration_loss)) + report("eval/pitch_loss", float(pitch_loss)) + report("eval/energy_loss", float(energy_loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["duration_loss"] = float(duration_loss) + losses_dict["pitch_loss"] = float(pitch_loss) + losses_dict["energy_loss"] = float(energy_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/hifigan/__init__.py b/ernie-sat/paddlespeech/t2s/models/hifigan/__init__.py new file mode 100644 index 0000000..7aa5e9d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/hifigan/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .hifigan import * +from .hifigan_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan.py b/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan.py new file mode 100644 index 0000000..ac5ff20 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan.py @@ -0,0 +1,716 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# This code is based on https://github.com/jik876/hifi-gan. +import copy +from typing import Any +from typing import Dict +from typing import List + +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.residual_block import HiFiGANResidualBlock as ResidualBlock + + +class HiFiGANGenerator(nn.Layer): + """HiFiGAN generator module.""" + + def __init__( + self, + in_channels: int=80, + out_channels: int=1, + channels: int=512, + kernel_size: int=7, + upsample_scales: List[int]=(8, 8, 2, 2), + upsample_kernel_sizes: List[int]=(16, 16, 4, 4), + resblock_kernel_sizes: List[int]=(3, 7, 11), + resblock_dilations: List[List[int]]=[(1, 3, 5), (1, 3, 5), + (1, 3, 5)], + use_additional_convs: bool=True, + bias: bool=True, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.1}, + use_weight_norm: bool=True, + init_type: str="xavier_uniform", ): + """Initialize HiFiGANGenerator module. + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + channels (int): Number of hidden representation channels. + kernel_size (int): Kernel size of initial and final conv layer. + upsample_scales (list): List of upsampling scales. + upsample_kernel_sizes (list): List of kernel sizes for upsampling layers. + resblock_kernel_sizes (list): List of kernel sizes for residual blocks. + resblock_dilations (list): List of dilation list for residual blocks. + use_additional_convs (bool): Whether to use additional conv layers in residual blocks. + bias (bool): Whether to add bias parameter in convolution layers. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + use_weight_norm (bool): Whether to use weight norm. + If set to true, it will be applied to all of the conv layers. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # check hyperparameters are valid + assert kernel_size % 2 == 1, "Kernel size must be odd number." + assert len(upsample_scales) == len(upsample_kernel_sizes) + assert len(resblock_dilations) == len(resblock_kernel_sizes) + + # define modules + self.num_upsamples = len(upsample_kernel_sizes) + self.num_blocks = len(resblock_kernel_sizes) + self.input_conv = nn.Conv1D( + in_channels, + channels, + kernel_size, + 1, + padding=(kernel_size - 1) // 2, ) + self.upsamples = nn.LayerList() + self.blocks = nn.LayerList() + for i in range(len(upsample_kernel_sizes)): + assert upsample_kernel_sizes[i] == 2 * upsample_scales[i] + self.upsamples.append( + nn.Sequential( + get_activation(nonlinear_activation, ** + nonlinear_activation_params), + nn.Conv1DTranspose( + channels // (2**i), + channels // (2**(i + 1)), + upsample_kernel_sizes[i], + upsample_scales[i], + padding=upsample_scales[i] // 2 + upsample_scales[i] % + 2, + output_padding=upsample_scales[i] % 2, ), )) + for j in range(len(resblock_kernel_sizes)): + self.blocks.append( + ResidualBlock( + kernel_size=resblock_kernel_sizes[j], + channels=channels // (2**(i + 1)), + dilations=resblock_dilations[j], + bias=bias, + use_additional_convs=use_additional_convs, + nonlinear_activation=nonlinear_activation, + nonlinear_activation_params=nonlinear_activation_params, + )) + self.output_conv = nn.Sequential( + nn.LeakyReLU(), + nn.Conv1D( + channels // (2**(i + 1)), + out_channels, + kernel_size, + 1, + padding=(kernel_size - 1) // 2, ), + nn.Tanh(), ) + + nn.initializer.set_global_initializer(None) + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # reset parameters + self.reset_parameters() + + def forward(self, c): + """Calculate forward propagation. + + Args: + c (Tensor): Input tensor (B, in_channels, T). + Returns: + Tensor: Output tensor (B, out_channels, T). + """ + c = self.input_conv(c) + for i in range(self.num_upsamples): + c = self.upsamples[i](c) + # initialize + cs = 0.0 + for j in range(self.num_blocks): + cs += self.blocks[i * self.num_blocks + j](c) + c = cs / self.num_blocks + c = self.output_conv(c) + + return c + + def reset_parameters(self): + """Reset parameters. + This initialization follows official implementation manner. + https://github.com/jik876/hifi-gan/blob/master/models.py + """ + # 定义参数为float的正态分布。 + dist = paddle.distribution.Normal(loc=0.0, scale=0.01) + + def _reset_parameters(m): + if isinstance(m, nn.Conv1D) or isinstance(m, nn.Conv1DTranspose): + w = dist.sample(m.weight.shape) + m.weight.set_value(w) + + self.apply(_reset_parameters) + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + def inference(self, c): + """Perform inference. + Args: + c (Tensor): Input tensor (T, in_channels). + normalize_before (bool): Whether to perform normalization. + Returns: + Tensor: + Output tensor (T ** prod(upsample_scales), out_channels). + """ + c = self.forward(c.transpose([1, 0]).unsqueeze(0)) + return c.squeeze(0).transpose([1, 0]) + + +class HiFiGANPeriodDiscriminator(nn.Layer): + """HiFiGAN period discriminator module.""" + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + period: int=3, + kernel_sizes: List[int]=[5, 3], + channels: int=32, + downsample_scales: List[int]=[3, 3, 3, 3, 1], + max_downsample_channels: int=1024, + bias: bool=True, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.1}, + use_weight_norm: bool=True, + use_spectral_norm: bool=False, + init_type: str="xavier_uniform", ): + """Initialize HiFiGANPeriodDiscriminator module. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + period (int): Period. + kernel_sizes (list): Kernel sizes of initial conv layers and the final conv layer. + channels (int): Number of initial channels. + downsample_scales (list): List of downsampling scales. + max_downsample_channels (int): Number of maximum downsampling channels. + use_additional_convs (bool): Whether to use additional conv layers in residual blocks. + bias (bool): Whether to add bias parameter in convolution layers. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + use_weight_norm (bool): Whether to use weight norm. + If set to true, it will be applied to all of the conv layers. + use_spectral_norm (bool): Whether to use spectral norm. + If set to true, it will be applied to all of the conv layers. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + assert len(kernel_sizes) == 2 + assert kernel_sizes[0] % 2 == 1, "Kernel size must be odd number." + assert kernel_sizes[1] % 2 == 1, "Kernel size must be odd number." + + self.period = period + self.convs = nn.LayerList() + in_chs = in_channels + out_chs = channels + for downsample_scale in downsample_scales: + self.convs.append( + nn.Sequential( + nn.Conv2D( + in_chs, + out_chs, + (kernel_sizes[0], 1), + (downsample_scale, 1), + padding=((kernel_sizes[0] - 1) // 2, 0), ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + in_chs = out_chs + # NOTE: Use downsample_scale + 1? + out_chs = min(out_chs * 4, max_downsample_channels) + self.output_conv = nn.Conv2D( + out_chs, + out_channels, + (kernel_sizes[1] - 1, 1), + 1, + padding=((kernel_sizes[1] - 1) // 2, 0), ) + + if use_weight_norm and use_spectral_norm: + raise ValueError("Either use use_weight_norm or use_spectral_norm.") + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # apply spectral norm + if use_spectral_norm: + self.apply_spectral_norm() + + def forward(self, x): + """Calculate forward propagation. + + Args: + c (Tensor): Input tensor (B, in_channels, T). + Returns: + list: List of each layer's tensors. + """ + # transform 1d to 2d -> (B, C, T/P, P) + b, c, t = paddle.shape(x) + if t % self.period != 0: + n_pad = self.period - (t % self.period) + x = F.pad(x, (0, n_pad), "reflect", data_format="NCL") + t += n_pad + x = x.reshape([b, c, t // self.period, self.period]) + + # forward conv + outs = [] + for layer in self.convs: + x = layer(x) + outs += [x] + x = self.output_conv(x) + x = paddle.flatten(x, 1, -1) + outs += [x] + + return outs + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def apply_spectral_norm(self): + """Apply spectral normalization module from all of the layers.""" + + def _apply_spectral_norm(m): + if isinstance(m, nn.Conv2D): + nn.utils.spectral_norm(m) + + self.apply(_apply_spectral_norm) + + +class HiFiGANMultiPeriodDiscriminator(nn.Layer): + """HiFiGAN multi-period discriminator module.""" + + def __init__( + self, + periods: List[int]=[2, 3, 5, 7, 11], + discriminator_params: Dict[str, Any]={ + "in_channels": 1, + "out_channels": 1, + "kernel_sizes": [5, 3], + "channels": 32, + "downsample_scales": [3, 3, 3, 3, 1], + "max_downsample_channels": 1024, + "bias": True, + "nonlinear_activation": "leakyrelu", + "nonlinear_activation_params": { + "negative_slope": 0.1 + }, + "use_weight_norm": True, + "use_spectral_norm": False, + }, + init_type: str="xavier_uniform", ): + """Initialize HiFiGANMultiPeriodDiscriminator module. + + Args: + periods (list): List of periods. + discriminator_params (dict): Parameters for hifi-gan period discriminator module. + The period parameter will be overwritten. + """ + super().__init__() + # initialize parameters + initialize(self, init_type) + + self.discriminators = nn.LayerList() + for period in periods: + params = copy.deepcopy(discriminator_params) + params["period"] = period + self.discriminators.append(HiFiGANPeriodDiscriminator(**params)) + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: List of list of each discriminator outputs, which consists of each layer output tensors. + """ + outs = [] + for f in self.discriminators: + outs += [f(x)] + + return outs + + +class HiFiGANScaleDiscriminator(nn.Layer): + """HiFi-GAN scale discriminator module.""" + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + kernel_sizes: List[int]=[15, 41, 5, 3], + channels: int=128, + max_downsample_channels: int=1024, + max_groups: int=16, + bias: bool=True, + downsample_scales: List[int]=[2, 2, 4, 4, 1], + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.1}, + use_weight_norm: bool=True, + use_spectral_norm: bool=False, + init_type: str="xavier_uniform", ): + """Initilize HiFiGAN scale discriminator module. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + kernel_sizes (list): List of four kernel sizes. The first will be used for the first conv layer, + and the second is for downsampling part, and the remaining two are for output layers. + channels (int): Initial number of channels for conv layer. + max_downsample_channels (int): Maximum number of channels for downsampling layers. + bias (bool): Whether to add bias parameter in convolution layers. + downsample_scales (list): List of downsampling scales. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + use_weight_norm (bool): Whether to use weight norm. + If set to true, it will be applied to all of the conv layers. + use_spectral_norm (bool): Whether to use spectral norm. + If set to true, it will be applied to all of the conv layers. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + self.layers = nn.LayerList() + + # check kernel size is valid + assert len(kernel_sizes) == 4 + for ks in kernel_sizes: + assert ks % 2 == 1 + + # add first layer + self.layers.append( + nn.Sequential( + nn.Conv1D( + in_channels, + channels, + # NOTE: Use always the same kernel size + kernel_sizes[0], + bias_attr=bias, + padding=(kernel_sizes[0] - 1) // 2, ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + + # add downsample layers + in_chs = channels + out_chs = channels + # NOTE(kan-bayashi): Remove hard coding? + groups = 4 + for downsample_scale in downsample_scales: + self.layers.append( + nn.Sequential( + nn.Conv1D( + in_chs, + out_chs, + kernel_size=kernel_sizes[1], + stride=downsample_scale, + padding=(kernel_sizes[1] - 1) // 2, + groups=groups, + bias_attr=bias, ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + in_chs = out_chs + # NOTE: Remove hard coding? + out_chs = min(in_chs * 2, max_downsample_channels) + # NOTE: Remove hard coding? + groups = min(groups * 4, max_groups) + + # add final layers + out_chs = min(in_chs * 2, max_downsample_channels) + self.layers.append( + nn.Sequential( + nn.Conv1D( + in_chs, + out_chs, + kernel_size=kernel_sizes[2], + stride=1, + padding=(kernel_sizes[2] - 1) // 2, + bias_attr=bias, ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + self.layers.append( + nn.Conv1D( + out_chs, + out_channels, + kernel_size=kernel_sizes[3], + stride=1, + padding=(kernel_sizes[3] - 1) // 2, + bias_attr=bias, ), ) + + if use_weight_norm and use_spectral_norm: + raise ValueError("Either use use_weight_norm or use_spectral_norm.") + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # apply spectral norm + if use_spectral_norm: + self.apply_spectral_norm() + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: List of output tensors of each layer. + """ + outs = [] + for f in self.layers: + x = f(x) + outs += [x] + + return outs + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def apply_spectral_norm(self): + """Apply spectral normalization module from all of the layers.""" + + def _apply_spectral_norm(m): + if isinstance(m, nn.Conv2D): + nn.utils.spectral_norm(m) + + self.apply(_apply_spectral_norm) + + +class HiFiGANMultiScaleDiscriminator(nn.Layer): + """HiFi-GAN multi-scale discriminator module.""" + + def __init__( + self, + scales: int=3, + downsample_pooling: str="AvgPool1D", + # follow the official implementation setting + downsample_pooling_params: Dict[str, Any]={ + "kernel_size": 4, + "stride": 2, + "padding": 2, + }, + discriminator_params: Dict[str, Any]={ + "in_channels": 1, + "out_channels": 1, + "kernel_sizes": [15, 41, 5, 3], + "channels": 128, + "max_downsample_channels": 1024, + "max_groups": 16, + "bias": True, + "downsample_scales": [2, 2, 4, 4, 1], + "nonlinear_activation": "leakyrelu", + "nonlinear_activation_params": { + "negative_slope": 0.1 + }, + }, + follow_official_norm: bool=False, + init_type: str="xavier_uniform", ): + """Initilize HiFiGAN multi-scale discriminator module. + + Args: + scales (int): Number of multi-scales. + downsample_pooling (str): Pooling module name for downsampling of the inputs. + downsample_pooling_params (dict): Parameters for the above pooling module. + discriminator_params (dict): Parameters for hifi-gan scale discriminator module. + follow_official_norm (bool): Whether to follow the norm setting of the official + implementaion. The first discriminator uses spectral norm and the other discriminators use weight norm. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + self.discriminators = nn.LayerList() + + # add discriminators + for i in range(scales): + params = copy.deepcopy(discriminator_params) + if follow_official_norm: + if i == 0: + params["use_weight_norm"] = False + params["use_spectral_norm"] = True + else: + params["use_weight_norm"] = True + params["use_spectral_norm"] = False + self.discriminators.append(HiFiGANScaleDiscriminator(**params)) + self.pooling = getattr(nn, downsample_pooling)( + **downsample_pooling_params) + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: List of list of each discriminator outputs, which consists of each layer output tensors. + """ + outs = [] + for f in self.discriminators: + outs += [f(x)] + x = self.pooling(x) + + return outs + + +class HiFiGANMultiScaleMultiPeriodDiscriminator(nn.Layer): + """HiFi-GAN multi-scale + multi-period discriminator module.""" + + def __init__( + self, + # Multi-scale discriminator related + scales: int=3, + scale_downsample_pooling: str="AvgPool1D", + scale_downsample_pooling_params: Dict[str, Any]={ + "kernel_size": 4, + "stride": 2, + "padding": 2, + }, + scale_discriminator_params: Dict[str, Any]={ + "in_channels": 1, + "out_channels": 1, + "kernel_sizes": [15, 41, 5, 3], + "channels": 128, + "max_downsample_channels": 1024, + "max_groups": 16, + "bias": True, + "downsample_scales": [2, 2, 4, 4, 1], + "nonlinear_activation": "leakyrelu", + "nonlinear_activation_params": { + "negative_slope": 0.1 + }, + }, + follow_official_norm: bool=True, + # Multi-period discriminator related + periods: List[int]=[2, 3, 5, 7, 11], + period_discriminator_params: Dict[str, Any]={ + "in_channels": 1, + "out_channels": 1, + "kernel_sizes": [5, 3], + "channels": 32, + "downsample_scales": [3, 3, 3, 3, 1], + "max_downsample_channels": 1024, + "bias": True, + "nonlinear_activation": "leakyrelu", + "nonlinear_activation_params": { + "negative_slope": 0.1 + }, + "use_weight_norm": True, + "use_spectral_norm": False, + }, + init_type: str="xavier_uniform", ): + """Initilize HiFiGAN multi-scale + multi-period discriminator module. + + Args: + scales (int): Number of multi-scales. + scale_downsample_pooling (str): Pooling module name for downsampling of the inputs. + scale_downsample_pooling_params (dict): Parameters for the above pooling module. + scale_discriminator_params (dict): Parameters for hifi-gan scale discriminator module. + follow_official_norm (bool): Whether to follow the norm setting of the official implementaion. + The first discriminator uses spectral norm and the other discriminators use weight norm. + periods (list): List of periods. + period_discriminator_params (dict): Parameters for hifi-gan period discriminator module. + The period parameter will be overwritten. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + self.msd = HiFiGANMultiScaleDiscriminator( + scales=scales, + downsample_pooling=scale_downsample_pooling, + downsample_pooling_params=scale_downsample_pooling_params, + discriminator_params=scale_discriminator_params, + follow_official_norm=follow_official_norm, ) + self.mpd = HiFiGANMultiPeriodDiscriminator( + periods=periods, + discriminator_params=period_discriminator_params, ) + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: + List of list of each discriminator outputs, + which consists of each layer output tensors. + Multi scale and multi period ones are concatenated. + """ + msd_outs = self.msd(x) + mpd_outs = self.mpd(x) + return msd_outs + mpd_outs + + +class HiFiGANInference(nn.Layer): + def __init__(self, normalizer, hifigan_generator): + super().__init__() + self.normalizer = normalizer + self.hifigan_generator = hifigan_generator + + def forward(self, logmel): + normalized_mel = self.normalizer(logmel) + wav = self.hifigan_generator.inference(normalized_mel) + return wav diff --git a/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan_updater.py b/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan_updater.py new file mode 100644 index 0000000..f12c666 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/hifigan/hifigan_updater.py @@ -0,0 +1,247 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import Dict + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +from paddlespeech.t2s.training.updaters.standard_updater import UpdaterState +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class HiFiGANUpdater(StandardUpdater): + def __init__(self, + models: Dict[str, Layer], + optimizers: Dict[str, Optimizer], + criterions: Dict[str, Layer], + schedulers: Dict[str, LRScheduler], + dataloader: DataLoader, + generator_train_start_steps: int=0, + discriminator_train_start_steps: int=100000, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + lambda_feat_match: float=1.0, + output_dir=None): + self.models = models + self.generator: Layer = models['generator'] + self.discriminator: Layer = models['discriminator'] + + self.optimizers = optimizers + self.optimizer_g: Optimizer = optimizers['generator'] + self.optimizer_d: Optimizer = optimizers['discriminator'] + + self.criterions = criterions + self.criterion_feat_match = criterions['feat_match'] + self.criterion_mel = criterions['mel'] + + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.schedulers = schedulers + self.scheduler_g = schedulers['generator'] + self.scheduler_d = schedulers['discriminator'] + + self.dataloader = dataloader + + self.generator_train_start_steps = generator_train_start_steps + self.discriminator_train_start_steps = discriminator_train_start_steps + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + self.lambda_feat_match = lambda_feat_match + + self.state = UpdaterState(iteration=0, epoch=0) + self.train_iterator = iter(self.dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # parse batch + wav, mel = batch + + # Generator + if self.state.iteration > self.generator_train_start_steps: + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # mel spectrogram loss + mel_loss = self.criterion_mel(wav_, wav) + aux_loss += mel_loss + report("train/mel_loss", float(mel_loss)) + losses_dict["mel_loss"] = float(mel_loss) + + gen_loss += aux_loss * self.lambda_aux + + # adversarial loss + if self.state.iteration > self.discriminator_train_start_steps: + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("train/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + # feature matching loss + # no need to track gradients + with paddle.no_grad(): + p = self.discriminator(wav) + fm_loss = self.criterion_feat_match(p_, p) + report("train/feature_matching_loss", float(fm_loss)) + losses_dict["feature_matching_loss"] = float(fm_loss) + + adv_loss += self.lambda_feat_match * fm_loss + + gen_loss += self.lambda_adv * adv_loss + + report("train/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + self.optimizer_g.clear_grad() + gen_loss.backward() + + self.optimizer_g.step() + self.scheduler_g.step() + + # Disctiminator + if self.state.iteration > self.discriminator_train_start_steps: + # re-compute wav_ which leads better quality + with paddle.no_grad(): + wav_ = self.generator(mel) + + p = self.discriminator(wav) + p_ = self.discriminator(wav_.detach()) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("train/real_loss", float(real_loss)) + report("train/fake_loss", float(fake_loss)) + report("train/discriminator_loss", float(dis_loss)) + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.optimizer_d.clear_grad() + dis_loss.backward() + + self.optimizer_d.step() + self.scheduler_d.step() + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class HiFiGANEvaluator(StandardEvaluator): + def __init__(self, + models: Dict[str, Layer], + criterions: Dict[str, Layer], + dataloader: DataLoader, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + lambda_feat_match: float=1.0, + output_dir=None): + self.models = models + self.generator = models['generator'] + self.discriminator = models['discriminator'] + + self.criterions = criterions + self.criterion_feat_match = criterions['feat_match'] + self.criterion_mel = criterions['mel'] + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.dataloader = dataloader + + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + self.lambda_feat_match = lambda_feat_match + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + # logging.debug("Evaluate: ") + self.msg = "Evaluate: " + losses_dict = {} + wav, mel = batch + + # Generator + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + ## Adversarial loss + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("eval/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + # feature matching loss + p = self.discriminator(wav) + fm_loss = self.criterion_feat_match(p_, p) + report("eval/feature_matching_loss", float(fm_loss)) + losses_dict["feature_matching_loss"] = float(fm_loss) + adv_loss += self.lambda_feat_match * fm_loss + + gen_loss += self.lambda_adv * adv_loss + + # mel spectrogram loss + mel_loss = self.criterion_mel(wav_, wav) + aux_loss += mel_loss + report("eval/mel_loss", float(mel_loss)) + losses_dict["mel_loss"] = float(mel_loss) + + gen_loss += aux_loss * self.lambda_aux + + report("eval/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + # Disctiminator + p = self.discriminator(wav) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("eval/real_loss", float(real_loss)) + report("eval/fake_loss", float(fake_loss)) + report("eval/discriminator_loss", float(dis_loss)) + + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/melgan/__init__.py b/ernie-sat/paddlespeech/t2s/models/melgan/__init__.py new file mode 100644 index 0000000..df8ccd9 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/melgan/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .melgan import * +from .multi_band_melgan_updater import * +from .style_melgan import * +from .style_melgan_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/melgan/melgan.py b/ernie-sat/paddlespeech/t2s/models/melgan/melgan.py new file mode 100644 index 0000000..22d8fd9 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/melgan/melgan.py @@ -0,0 +1,528 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""MelGAN Modules.""" +from typing import Any +from typing import Dict +from typing import List + +import numpy as np +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.causal_conv import CausalConv1D +from paddlespeech.t2s.modules.causal_conv import CausalConv1DTranspose +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.pqmf import PQMF +from paddlespeech.t2s.modules.residual_stack import ResidualStack + + +class MelGANGenerator(nn.Layer): + """MelGAN generator module.""" + + def __init__( + self, + in_channels: int=80, + out_channels: int=1, + kernel_size: int=7, + channels: int=512, + bias: bool=True, + upsample_scales: List[int]=[8, 8, 2, 2], + stack_kernel_size: int=3, + stacks: int=3, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + pad: str="Pad1D", + pad_params: Dict[str, Any]={"mode": "reflect"}, + use_final_nonlinear_activation: bool=True, + use_weight_norm: bool=True, + use_causal_conv: bool=False, + init_type: str="xavier_uniform", ): + """Initialize MelGANGenerator module. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels, + the number of sub-band is out_channels in multi-band melgan. + kernel_size (int): Kernel size of initial and final conv layer. + channels (int): Initial number of channels for conv layer. + bias (bool): Whether to add bias parameter in convolution layers. + upsample_scales (List[int]): List of upsampling scales. + stack_kernel_size (int): Kernel size of dilated conv layers in residual stack. + stacks (int): Number of stacks in a single residual stack. + nonlinear_activation (Optional[str], optional): Non linear activation in upsample network, by default None + nonlinear_activation_params (Dict[str, Any], optional): Parameters passed to the linear activation in the upsample network, + by default {} + pad (str): Padding function module name before dilated convolution layer. + pad_params (dict): Hyperparameters for padding function. + use_final_nonlinear_activation (nn.Layer): Activation function for the final layer. + use_weight_norm (bool): Whether to use weight norm. + If set to true, it will be applied to all of the conv layers. + use_causal_conv (bool): Whether to use causal convolution. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + # check hyper parameters is valid + assert channels >= np.prod(upsample_scales) + assert channels % (2**len(upsample_scales)) == 0 + if not use_causal_conv: + assert (kernel_size - 1 + ) % 2 == 0, "Not support even number kernel size." + + layers = [] + if not use_causal_conv: + layers += [ + getattr(paddle.nn, pad)((kernel_size - 1) // 2, **pad_params), + nn.Conv1D(in_channels, channels, kernel_size, bias_attr=bias), + ] + else: + layers += [ + CausalConv1D( + in_channels, + channels, + kernel_size, + bias=bias, + pad=pad, + pad_params=pad_params, ), + ] + + for i, upsample_scale in enumerate(upsample_scales): + # add upsampling layer + layers += [ + get_activation(nonlinear_activation, + **nonlinear_activation_params) + ] + if not use_causal_conv: + layers += [ + nn.Conv1DTranspose( + channels // (2**i), + channels // (2**(i + 1)), + upsample_scale * 2, + stride=upsample_scale, + padding=upsample_scale // 2 + upsample_scale % 2, + output_padding=upsample_scale % 2, + bias_attr=bias, ) + ] + else: + layers += [ + CausalConv1DTranspose( + channels // (2**i), + channels // (2**(i + 1)), + upsample_scale * 2, + stride=upsample_scale, + bias=bias, ) + ] + + # add residual stack + for j in range(stacks): + layers += [ + ResidualStack( + kernel_size=stack_kernel_size, + channels=channels // (2**(i + 1)), + dilation=stack_kernel_size**j, + bias=bias, + nonlinear_activation=nonlinear_activation, + nonlinear_activation_params=nonlinear_activation_params, + pad=pad, + pad_params=pad_params, + use_causal_conv=use_causal_conv, ) + ] + + # add final layer + layers += [ + get_activation(nonlinear_activation, **nonlinear_activation_params) + ] + if not use_causal_conv: + layers += [ + getattr(nn, pad)((kernel_size - 1) // 2, **pad_params), + nn.Conv1D( + channels // (2**(i + 1)), + out_channels, + kernel_size, + bias_attr=bias), + ] + else: + layers += [ + CausalConv1D( + channels // (2**(i + 1)), + out_channels, + kernel_size, + bias=bias, + pad=pad, + pad_params=pad_params, ), + ] + if use_final_nonlinear_activation: + layers += [nn.Tanh()] + + # define the model as a single function + self.melgan = nn.Sequential(*layers) + nn.initializer.set_global_initializer(None) + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # reset parameters + self.reset_parameters() + + # initialize pqmf for multi-band melgan inference + if out_channels > 1: + self.pqmf = PQMF(subbands=out_channels) + else: + self.pqmf = None + + def forward(self, c): + """Calculate forward propagation. + + Args: + c (Tensor): Input tensor (B, in_channels, T). + Returns: + Tensor: Output tensor (B, out_channels, T ** prod(upsample_scales)). + """ + out = self.melgan(c) + return out + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + def reset_parameters(self): + """Reset parameters. + This initialization follows official implementation manner. + https://github.com/descriptinc/melgan-neurips/blob/master/mel2wav/modules.py + """ + # 定义参数为float的正态分布。 + dist = paddle.distribution.Normal(loc=0.0, scale=0.02) + + def _reset_parameters(m): + if isinstance(m, nn.Conv1D) or isinstance(m, nn.Conv1DTranspose): + w = dist.sample(m.weight.shape) + m.weight.set_value(w) + + self.apply(_reset_parameters) + + def inference(self, c): + """Perform inference. + + Args: + c (Union[Tensor, ndarray]): Input tensor (T, in_channels). + Returns: + Tensor: Output tensor (out_channels*T ** prod(upsample_scales), 1). + """ + # pseudo batch + c = c.transpose([1, 0]).unsqueeze(0) + # (B, out_channels, T ** prod(upsample_scales) + out = self.melgan(c) + if self.pqmf is not None: + # (B, 1, out_channels * T ** prod(upsample_scales) + out = self.pqmf(out) + out = out.squeeze(0).transpose([1, 0]) + return out + + +class MelGANDiscriminator(nn.Layer): + """MelGAN discriminator module.""" + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + kernel_sizes: List[int]=[5, 3], + channels: int=16, + max_downsample_channels: int=1024, + bias: bool=True, + downsample_scales: List[int]=[4, 4, 4, 4], + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + pad: str="Pad1D", + pad_params: Dict[str, Any]={"mode": "reflect"}, + init_type: str="xavier_uniform", ): + """Initilize MelGAN discriminator module. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + kernel_sizes (List[int]): List of two kernel sizes. The prod will be used for the first conv layer, + and the first and the second kernel sizes will be used for the last two layers. + For example if kernel_sizes = [5, 3], the first layer kernel size will be 5 * 3 = 15, + the last two layers' kernel size will be 5 and 3, respectively. + channels (int): Initial number of channels for conv layer. + max_downsample_channels (int): Maximum number of channels for downsampling layers. + bias (bool): Whether to add bias parameter in convolution layers. + downsample_scales (List[int]): List of downsampling scales. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + pad (str): Padding function module name before dilated convolution layer. + pad_params (dict): Hyperparameters for padding function. + """ + super().__init__() + + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + # initialize parameters + initialize(self, init_type) + + self.layers = nn.LayerList() + + # check kernel size is valid + assert len(kernel_sizes) == 2 + assert kernel_sizes[0] % 2 == 1 + assert kernel_sizes[1] % 2 == 1 + + # add first layer + self.layers.append( + nn.Sequential( + getattr(nn, pad)((np.prod(kernel_sizes) - 1) // 2, ** + pad_params), + nn.Conv1D( + in_channels, + channels, + int(np.prod(kernel_sizes)), + bias_attr=bias), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + + # add downsample layers + in_chs = channels + for downsample_scale in downsample_scales: + out_chs = min(in_chs * downsample_scale, max_downsample_channels) + self.layers.append( + nn.Sequential( + nn.Conv1D( + in_chs, + out_chs, + kernel_size=downsample_scale * 10 + 1, + stride=downsample_scale, + padding=downsample_scale * 5, + groups=in_chs // 4, + bias_attr=bias, ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + in_chs = out_chs + + # add final layers + out_chs = min(in_chs * 2, max_downsample_channels) + self.layers.append( + nn.Sequential( + nn.Conv1D( + in_chs, + out_chs, + kernel_sizes[0], + padding=(kernel_sizes[0] - 1) // 2, + bias_attr=bias, ), + get_activation(nonlinear_activation, ** + nonlinear_activation_params), )) + self.layers.append( + nn.Conv1D( + out_chs, + out_channels, + kernel_sizes[1], + padding=(kernel_sizes[1] - 1) // 2, + bias_attr=bias, ), ) + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: List of output tensors of each layer (for feat_match_loss). + """ + outs = [] + for f in self.layers: + x = f(x) + outs += [x] + + return outs + + +class MelGANMultiScaleDiscriminator(nn.Layer): + """MelGAN multi-scale discriminator module.""" + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + scales: int=3, + downsample_pooling: str="AvgPool1D", + # follow the official implementation setting + downsample_pooling_params: Dict[str, Any]={ + "kernel_size": 4, + "stride": 2, + "padding": 1, + "exclusive": True, + }, + kernel_sizes: List[int]=[5, 3], + channels: int=16, + max_downsample_channels: int=1024, + bias: bool=True, + downsample_scales: List[int]=[4, 4, 4, 4], + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + pad: str="Pad1D", + pad_params: Dict[str, Any]={"mode": "reflect"}, + use_weight_norm: bool=True, + init_type: str="xavier_uniform", ): + """Initilize MelGAN multi-scale discriminator module. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + scales (int): Number of multi-scales. + downsample_pooling (str): Pooling module name for downsampling of the inputs. + downsample_pooling_params (dict): Parameters for the above pooling module. + kernel_sizes (List[int]): List of two kernel sizes. The sum will be used for the first conv layer, + and the first and the second kernel sizes will be used for the last two layers. + channels (int): Initial number of channels for conv layer. + max_downsample_channels (int): Maximum number of channels for downsampling layers. + bias (bool): Whether to add bias parameter in convolution layers. + downsample_scales (List[int]): List of downsampling scales. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + pad (str): Padding function module name before dilated convolution layer. + pad_params (dict): Hyperparameters for padding function. + use_causal_conv (bool): Whether to use causal convolution. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # for + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + self.discriminators = nn.LayerList() + + # add discriminators + for _ in range(scales): + self.discriminators.append( + MelGANDiscriminator( + in_channels=in_channels, + out_channels=out_channels, + kernel_sizes=kernel_sizes, + channels=channels, + max_downsample_channels=max_downsample_channels, + bias=bias, + downsample_scales=downsample_scales, + nonlinear_activation=nonlinear_activation, + nonlinear_activation_params=nonlinear_activation_params, + pad=pad, + pad_params=pad_params, )) + self.pooling = getattr(nn, downsample_pooling)( + **downsample_pooling_params) + + nn.initializer.set_global_initializer(None) + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # reset parameters + self.reset_parameters() + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input noise signal (B, 1, T). + Returns: + List: List of list of each discriminator outputs, which consists of each layer output tensors. + """ + outs = [] + for f in self.discriminators: + outs += [f(x)] + x = self.pooling(x) + + return outs + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + def reset_parameters(self): + """Reset parameters. + This initialization follows official implementation manner. + https://github.com/descriptinc/melgan-neurips/blob/master/mel2wav/modules.py + """ + + # 定义参数为float的正态分布。 + dist = paddle.distribution.Normal(loc=0.0, scale=0.02) + + def _reset_parameters(m): + if isinstance(m, nn.Conv1D) or isinstance(m, nn.Conv1DTranspose): + w = dist.sample(m.weight.shape) + m.weight.set_value(w) + + self.apply(_reset_parameters) + + +class MelGANInference(nn.Layer): + def __init__(self, normalizer, melgan_generator): + super().__init__() + self.normalizer = normalizer + self.melgan_generator = melgan_generator + + def forward(self, logmel): + normalized_mel = self.normalizer(logmel) + wav = self.melgan_generator.inference(normalized_mel) + return wav diff --git a/ernie-sat/paddlespeech/t2s/models/melgan/multi_band_melgan_updater.py b/ernie-sat/paddlespeech/t2s/models/melgan/multi_band_melgan_updater.py new file mode 100644 index 0000000..1c6c34c --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/melgan/multi_band_melgan_updater.py @@ -0,0 +1,263 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path +from typing import Dict + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +from paddlespeech.t2s.training.updaters.standard_updater import UpdaterState +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class MBMelGANUpdater(StandardUpdater): + def __init__(self, + models: Dict[str, Layer], + optimizers: Dict[str, Optimizer], + criterions: Dict[str, Layer], + schedulers: Dict[str, LRScheduler], + dataloader: DataLoader, + generator_train_start_steps: int=0, + discriminator_train_start_steps: int=100000, + lambda_aux: float=1.0, + lambda_adv: float=1.0, + output_dir: Path=None): + self.models = models + self.generator: Layer = models['generator'] + self.discriminator: Layer = models['discriminator'] + + self.optimizers = optimizers + self.optimizer_g: Optimizer = optimizers['generator'] + self.optimizer_d: Optimizer = optimizers['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_sub_stft = criterions['sub_stft'] + self.criterion_pqmf = criterions['pqmf'] + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.schedulers = schedulers + self.scheduler_g = schedulers['generator'] + self.scheduler_d = schedulers['discriminator'] + + self.dataloader = dataloader + + self.generator_train_start_steps = generator_train_start_steps + self.discriminator_train_start_steps = discriminator_train_start_steps + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + + self.state = UpdaterState(iteration=0, epoch=0) + self.train_iterator = iter(self.dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # parse batch + wav, mel = batch + + # Generator + if self.state.iteration > self.generator_train_start_steps: + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + wav_mb_ = wav_ + # (B, 1, out_channels*T ** prod(upsample_scales) + wav_ = self.criterion_pqmf.synthesis(wav_mb_) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # full band Multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + # for balancing with subband stft loss + # Eq.(9) in paper + aux_loss += 0.5 * (sc_loss + mag_loss) + report("train/spectral_convergence_loss", float(sc_loss)) + report("train/log_stft_magnitude_loss", float(mag_loss)) + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + + # sub band Multi-resolution stft loss + # (B, subbands, T // subbands) + wav_mb = self.criterion_pqmf.analysis(wav) + sub_sc_loss, sub_mag_loss = self.criterion_sub_stft(wav_mb_, wav_mb) + # Eq.(9) in paper + aux_loss += 0.5 * (sub_sc_loss + sub_mag_loss) + report("train/sub_spectral_convergence_loss", float(sub_sc_loss)) + report("train/sub_log_stft_magnitude_loss", float(sub_mag_loss)) + losses_dict["sub_spectral_convergence_loss"] = float(sub_sc_loss) + losses_dict["sub_log_stft_magnitude_loss"] = float(sub_mag_loss) + + gen_loss += aux_loss * self.lambda_aux + + # adversarial loss + if self.state.iteration > self.discriminator_train_start_steps: + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("train/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + report("train/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + self.optimizer_g.clear_grad() + gen_loss.backward() + + self.optimizer_g.step() + self.scheduler_g.step() + + # Disctiminator + if self.state.iteration > self.discriminator_train_start_steps: + # re-compute wav_ which leads better quality + with paddle.no_grad(): + wav_ = self.generator(mel) + wav_ = self.criterion_pqmf.synthesis(wav_) + p = self.discriminator(wav) + p_ = self.discriminator(wav_.detach()) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("train/real_loss", float(real_loss)) + report("train/fake_loss", float(fake_loss)) + report("train/discriminator_loss", float(dis_loss)) + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.optimizer_d.clear_grad() + dis_loss.backward() + + self.optimizer_d.step() + self.scheduler_d.step() + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class MBMelGANEvaluator(StandardEvaluator): + def __init__(self, + models: Dict[str, Layer], + criterions: Dict[str, Layer], + dataloader: DataLoader, + lambda_aux: float=1.0, + lambda_adv: float=1.0, + output_dir: Path=None): + self.models = models + self.generator = models['generator'] + self.discriminator = models['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_sub_stft = criterions['sub_stft'] + self.criterion_pqmf = criterions['pqmf'] + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.dataloader = dataloader + + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + # logging.debug("Evaluate: ") + self.msg = "Evaluate: " + losses_dict = {} + wav, mel = batch + + # Generator + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + wav_mb_ = wav_ + # (B, 1, out_channels*T ** prod(upsample_scales) + wav_ = self.criterion_pqmf.synthesis(wav_mb_) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # adversarial loss + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("eval/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + # Multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + # Eq.(9) in paper + aux_loss += 0.5 * (sc_loss + mag_loss) + report("eval/spectral_convergence_loss", float(sc_loss)) + report("eval/log_stft_magnitude_loss", float(mag_loss)) + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + + # sub band Multi-resolution stft loss + # (B, subbands, T // subbands) + wav_mb = self.criterion_pqmf.analysis(wav) + sub_sc_loss, sub_mag_loss = self.criterion_sub_stft(wav_mb_, wav_mb) + # Eq.(9) in paper + aux_loss += 0.5 * (sub_sc_loss + sub_mag_loss) + report("eval/sub_spectral_convergence_loss", float(sub_sc_loss)) + report("eval/sub_log_stft_magnitude_loss", float(sub_mag_loss)) + losses_dict["sub_spectral_convergence_loss"] = float(sub_sc_loss) + losses_dict["sub_log_stft_magnitude_loss"] = float(sub_mag_loss) + + gen_loss += aux_loss * self.lambda_aux + + report("eval/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + # Disctiminator + p = self.discriminator(wav) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("eval/real_loss", float(real_loss)) + report("eval/fake_loss", float(fake_loss)) + report("eval/discriminator_loss", float(dis_loss)) + + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan.py b/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan.py new file mode 100644 index 0000000..40a2f10 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan.py @@ -0,0 +1,375 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""StyleMelGAN Modules.""" +import copy +from typing import Any +from typing import Dict +from typing import List + +import numpy as np +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.models.melgan import MelGANDiscriminator as BaseDiscriminator +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.pqmf import PQMF +from paddlespeech.t2s.modules.tade_res_block import TADEResBlock + + +class StyleMelGANGenerator(nn.Layer): + """Style MelGAN generator module.""" + + def __init__( + self, + in_channels: int=128, + aux_channels: int=80, + channels: int=64, + out_channels: int=1, + kernel_size: int=9, + dilation: int=2, + bias: bool=True, + noise_upsample_scales: List[int]=[11, 2, 2, 2], + noise_upsample_activation: str="leakyrelu", + noise_upsample_activation_params: Dict[str, + Any]={"negative_slope": 0.2}, + upsample_scales: List[int]=[2, 2, 2, 2, 2, 2, 2, 2, 1], + upsample_mode: str="linear", + gated_function: str="softmax", + use_weight_norm: bool=True, + init_type: str="xavier_uniform", ): + """Initilize Style MelGAN generator. + + Args: + in_channels (int): Number of input noise channels. + aux_channels (int): Number of auxiliary input channels. + channels (int): Number of channels for conv layer. + out_channels (int): Number of output channels. + kernel_size (int): Kernel size of conv layers. + dilation (int): Dilation factor for conv layers. + bias (bool): Whether to add bias parameter in convolution layers. + noise_upsample_scales (list): List of noise upsampling scales. + noise_upsample_activation (str): Activation function module name for noise upsampling. + noise_upsample_activation_params (dict): Hyperparameters for the above activation function. + upsample_scales (list): List of upsampling scales. + upsample_mode (str): Upsampling mode in TADE layer. + gated_function (str): Gated function in TADEResBlock ("softmax" or "sigmoid"). + use_weight_norm (bool): Whether to use weight norm. + If set to true, it will be applied to all of the conv layers. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + self.in_channels = in_channels + noise_upsample = [] + in_chs = in_channels + for noise_upsample_scale in noise_upsample_scales: + noise_upsample.append( + nn.Conv1DTranspose( + in_chs, + channels, + noise_upsample_scale * 2, + stride=noise_upsample_scale, + padding=noise_upsample_scale // 2 + noise_upsample_scale % + 2, + output_padding=noise_upsample_scale % 2, + bias_attr=bias, )) + noise_upsample.append( + get_activation(noise_upsample_activation, ** + noise_upsample_activation_params)) + in_chs = channels + self.noise_upsample = nn.Sequential(*noise_upsample) + self.noise_upsample_factor = np.prod(noise_upsample_scales) + + self.blocks = nn.LayerList() + aux_chs = aux_channels + for upsample_scale in upsample_scales: + self.blocks.append( + TADEResBlock( + in_channels=channels, + aux_channels=aux_chs, + kernel_size=kernel_size, + dilation=dilation, + bias=bias, + upsample_factor=upsample_scale, + upsample_mode=upsample_mode, + gated_function=gated_function, ), ) + aux_chs = channels + self.upsample_factor = np.prod(upsample_scales) + + self.output_conv = nn.Sequential( + nn.Conv1D( + channels, + out_channels, + kernel_size, + 1, + bias_attr=bias, + padding=(kernel_size - 1) // 2, ), + nn.Tanh(), ) + + nn.initializer.set_global_initializer(None) + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # reset parameters + self.reset_parameters() + + def forward(self, c, z=None): + """Calculate forward propagation. + + Args: + c (Tensor): Auxiliary input tensor (B, channels, T). + z (Tensor): Input noise tensor (B, in_channels, 1). + Returns: + Tensor: Output tensor (B, out_channels, T ** prod(upsample_scales)). + """ + # batch_max_steps(24000) == noise_upsample_factor(80) * upsample_factor(300) + if z is None: + z = paddle.randn([paddle.shape(c)[0], self.in_channels, 1]) + # (B, in_channels, noise_upsample_factor). + x = self.noise_upsample(z) + for block in self.blocks: + x, c = block(x, c) + x = self.output_conv(x) + return x + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + if layer: + nn.utils.remove_weight_norm(layer) + # add AttributeError to bypass https://github.com/PaddlePaddle/Paddle/issues/38532 temporarily + except (ValueError, AttributeError): + pass + + self.apply(_remove_weight_norm) + + def reset_parameters(self): + """Reset parameters. + This initialization follows official implementation manner. + https://github.com/descriptinc/melgan-neurips/blob/master/mel2wav/modules.py + """ + # 定义参数为float的正态分布。 + dist = paddle.distribution.Normal(loc=0.0, scale=0.02) + + def _reset_parameters(m): + if isinstance(m, nn.Conv1D) or isinstance(m, nn.Conv1DTranspose): + w = dist.sample(m.weight.shape) + m.weight.set_value(w) + + self.apply(_reset_parameters) + + def inference(self, c): + """Perform inference. + Args: + c (Tensor): Input tensor (T, in_channels). + Returns: + Tensor: Output tensor (T ** prod(upsample_scales), out_channels). + """ + # (1, in_channels, T) + c = c.transpose([1, 0]).unsqueeze(0) + c_shape = paddle.shape(c) + # prepare noise input + # there is a bug in Paddle int division, we must convert a int tensor to int here + noise_T = paddle.cast( + paddle.ceil(c_shape[2] / int(self.noise_upsample_factor)), + dtype='int64') + noise_size = (1, self.in_channels, noise_T) + # (1, in_channels, T/noise_upsample_factor) + noise = paddle.randn(noise_size) + # (1, in_channels, T) + x = self.noise_upsample(noise) + x_shape = paddle.shape(x) + total_length = c_shape[2] * self.upsample_factor + # Dygraph to Static Graph bug here, 2021.12.15 + c = F.pad( + c, (0, x_shape[2] - c_shape[2]), "replicate", data_format="NCL") + # c.shape[2] == x.shape[2] here + # (1, in_channels, T*prod(upsample_scales)) + for block in self.blocks: + x, c = block(x, c) + x = self.output_conv(x)[..., :total_length] + return x.squeeze(0).transpose([1, 0]) + + +class StyleMelGANDiscriminator(nn.Layer): + """Style MelGAN disciminator module.""" + + def __init__( + self, + repeats: int=2, + window_sizes: List[int]=[512, 1024, 2048, 4096], + pqmf_params: List[List[int]]=[ + [1, None, None, None], + [2, 62, 0.26700, 9.0], + [4, 62, 0.14200, 9.0], + [8, 62, 0.07949, 9.0], + ], + discriminator_params: Dict[str, Any]={ + "out_channels": 1, + "kernel_sizes": [5, 3], + "channels": 16, + "max_downsample_channels": 512, + "bias": True, + "downsample_scales": [4, 4, 4, 1], + "nonlinear_activation": "leakyrelu", + "nonlinear_activation_params": { + "negative_slope": 0.2 + }, + "pad": "Pad1D", + "pad_params": { + "mode": "reflect" + }, + }, + use_weight_norm: bool=True, + init_type: str="xavier_uniform", ): + """Initilize Style MelGAN discriminator. + + Args: + repeats (int): Number of repititons to apply RWD. + window_sizes (list): List of random window sizes. + pqmf_params (list): List of list of Parameters for PQMF modules + discriminator_params (dict): Parameters for base discriminator module. + use_weight_nom (bool): Whether to apply weight normalization. + """ + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # window size check + assert len(window_sizes) == len(pqmf_params) + sizes = [ws // p[0] for ws, p in zip(window_sizes, pqmf_params)] + assert len(window_sizes) == sum([sizes[0] == size for size in sizes]) + + self.repeats = repeats + self.window_sizes = window_sizes + self.pqmfs = nn.LayerList() + self.discriminators = nn.LayerList() + for pqmf_param in pqmf_params: + d_params = copy.deepcopy(discriminator_params) + d_params["in_channels"] = pqmf_param[0] + if pqmf_param[0] == 1: + self.pqmfs.append(nn.Identity()) + else: + self.pqmfs.append(PQMF(*pqmf_param)) + self.discriminators.append(BaseDiscriminator(**d_params)) + + nn.initializer.set_global_initializer(None) + + # apply weight norm + if use_weight_norm: + self.apply_weight_norm() + + # reset parameters + self.reset_parameters() + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input tensor (B, 1, T). + Returns: + List: List of discriminator outputs, #items in the list will be + equal to repeats * #discriminators. + """ + outs = [] + for _ in range(self.repeats): + outs += self._forward(x) + return outs + + def _forward(self, x): + outs = [] + for idx, (ws, pqmf, disc) in enumerate( + zip(self.window_sizes, self.pqmfs, self.discriminators)): + start_idx = int(np.random.randint(paddle.shape(x)[-1] - ws)) + x_ = x[:, :, start_idx:start_idx + ws] + if idx == 0: + # nn.Identity() + x_ = pqmf(x_) + else: + x_ = pqmf.analysis(x_) + outs += [disc(x_)] + return outs + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv1DTranspose)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + def reset_parameters(self): + """Reset parameters. + This initialization follows official implementation manner. + https://github.com/descriptinc/melgan-neurips/blob/master/mel2wav/modules.py + """ + # 定义参数为float的正态分布。 + dist = paddle.distribution.Normal(loc=0.0, scale=0.02) + + def _reset_parameters(m): + if isinstance(m, nn.Conv1D) or isinstance(m, nn.Conv1DTranspose): + w = dist.sample(m.weight.shape) + m.weight.set_value(w) + + self.apply(_reset_parameters) + + +class StyleMelGANInference(nn.Layer): + def __init__(self, normalizer, style_melgan_generator): + super().__init__() + self.normalizer = normalizer + self.style_melgan_generator = style_melgan_generator + + def forward(self, logmel): + normalized_mel = self.normalizer(logmel) + wav = self.style_melgan_generator.inference(normalized_mel) + return wav diff --git a/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan_updater.py b/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan_updater.py new file mode 100644 index 0000000..b0cb4ed --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/melgan/style_melgan_updater.py @@ -0,0 +1,227 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path +from typing import Dict + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +from paddlespeech.t2s.training.updaters.standard_updater import UpdaterState +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class StyleMelGANUpdater(StandardUpdater): + def __init__(self, + models: Dict[str, Layer], + optimizers: Dict[str, Optimizer], + criterions: Dict[str, Layer], + schedulers: Dict[str, LRScheduler], + dataloader: DataLoader, + generator_train_start_steps: int=0, + discriminator_train_start_steps: int=100000, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + output_dir: Path=None): + self.models = models + self.generator: Layer = models['generator'] + self.discriminator: Layer = models['discriminator'] + + self.optimizers = optimizers + self.optimizer_g: Optimizer = optimizers['generator'] + self.optimizer_d: Optimizer = optimizers['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.schedulers = schedulers + self.scheduler_g = schedulers['generator'] + self.scheduler_d = schedulers['discriminator'] + + self.dataloader = dataloader + + self.generator_train_start_steps = generator_train_start_steps + self.discriminator_train_start_steps = discriminator_train_start_steps + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + + self.state = UpdaterState(iteration=0, epoch=0) + self.train_iterator = iter(self.dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # parse batch + wav, mel = batch + + # Generator + if self.state.iteration > self.generator_train_start_steps: + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # full band multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + aux_loss += sc_loss + mag_loss + report("train/spectral_convergence_loss", float(sc_loss)) + report("train/log_stft_magnitude_loss", float(mag_loss)) + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + + gen_loss += aux_loss * self.lambda_aux + + # adversarial loss + if self.state.iteration > self.discriminator_train_start_steps: + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("train/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + report("train/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + self.optimizer_g.clear_grad() + gen_loss.backward() + + self.optimizer_g.step() + self.scheduler_g.step() + + # Disctiminator + if self.state.iteration > self.discriminator_train_start_steps: + # re-compute wav_ which leads better quality + with paddle.no_grad(): + wav_ = self.generator(mel) + + p = self.discriminator(wav) + p_ = self.discriminator(wav_.detach()) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("train/real_loss", float(real_loss)) + report("train/fake_loss", float(fake_loss)) + report("train/discriminator_loss", float(dis_loss)) + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.optimizer_d.clear_grad() + dis_loss.backward() + + self.optimizer_d.step() + self.scheduler_d.step() + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class StyleMelGANEvaluator(StandardEvaluator): + def __init__(self, + models: Dict[str, Layer], + criterions: Dict[str, Layer], + dataloader: DataLoader, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + output_dir: Path=None): + self.models = models + self.generator = models['generator'] + self.discriminator = models['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_gen_adv = criterions["gen_adv"] + self.criterion_dis_adv = criterions["dis_adv"] + + self.dataloader = dataloader + + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + wav, mel = batch + + # Generator + # (B, out_channels, T ** prod(upsample_scales) + wav_ = self.generator(mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # adversarial loss + p_ = self.discriminator(wav_) + adv_loss = self.criterion_gen_adv(p_) + report("eval/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + # multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + aux_loss += sc_loss + mag_loss + report("eval/spectral_convergence_loss", float(sc_loss)) + report("eval/log_stft_magnitude_loss", float(mag_loss)) + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + + gen_loss += aux_loss * self.lambda_aux + + report("eval/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + # Disctiminator + p = self.discriminator(wav) + real_loss, fake_loss = self.criterion_dis_adv(p_, p) + dis_loss = real_loss + fake_loss + report("eval/real_loss", float(real_loss)) + report("eval/fake_loss", float(fake_loss)) + report("eval/discriminator_loss", float(dis_loss)) + + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/__init__.py b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/__init__.py new file mode 100644 index 0000000..7232273 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .parallel_wavegan import * +from .parallel_wavegan_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan.py b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan.py new file mode 100644 index 0000000..cc8460e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan.py @@ -0,0 +1,450 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import math +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +import numpy as np +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.residual_block import WaveNetResidualBlock as ResidualBlock +from paddlespeech.t2s.modules.upsample import ConvInUpsampleNet + + +class PWGGenerator(nn.Layer): + """Wave Generator for Parallel WaveGAN + + Args: + in_channels (int, optional): Number of channels of the input waveform, by default 1 + out_channels (int, optional): Number of channels of the output waveform, by default 1 + kernel_size (int, optional): Kernel size of the residual blocks inside, by default 3 + layers (int, optional): Number of residual blocks inside, by default 30 + stacks (int, optional): The number of groups to split the residual blocks into, by default 3 + Within each group, the dilation of the residual block grows exponentially. + residual_channels (int, optional): Residual channel of the residual blocks, by default 64 + gate_channels (int, optional): Gate channel of the residual blocks, by default 128 + skip_channels (int, optional): Skip channel of the residual blocks, by default 64 + aux_channels (int, optional): Auxiliary channel of the residual blocks, by default 80 + aux_context_window (int, optional): The context window size of the first convolution applied to the + auxiliary input, by default 2 + dropout (float, optional): Dropout of the residual blocks, by default 0. + bias (bool, optional): Whether to use bias in residual blocks, by default True + use_weight_norm (bool, optional): Whether to use weight norm in all convolutions, by default True + use_causal_conv (bool, optional): Whether to use causal padding in the upsample network and residual + blocks, by default False + upsample_scales (List[int], optional): Upsample scales of the upsample network, by default [4, 4, 4, 4] + nonlinear_activation (Optional[str], optional): Non linear activation in upsample network, by default None + nonlinear_activation_params (Dict[str, Any], optional): Parameters passed to the linear activation in the upsample network, + by default {} + interpolate_mode (str, optional): Interpolation mode of the upsample network, by default "nearest" + freq_axis_kernel_size (int, optional): Kernel size along the frequency axis of the upsample network, by default 1 + """ + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + kernel_size: int=3, + layers: int=30, + stacks: int=3, + residual_channels: int=64, + gate_channels: int=128, + skip_channels: int=64, + aux_channels: int=80, + aux_context_window: int=2, + dropout: float=0., + bias: bool=True, + use_weight_norm: bool=True, + use_causal_conv: bool=False, + upsample_scales: List[int]=[4, 4, 4, 4], + nonlinear_activation: Optional[str]=None, + nonlinear_activation_params: Dict[str, Any]={}, + interpolate_mode: str="nearest", + freq_axis_kernel_size: int=1, + init_type: str="xavier_uniform", ): + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + self.in_channels = in_channels + self.out_channels = out_channels + self.aux_channels = aux_channels + self.aux_context_window = aux_context_window + self.layers = layers + self.stacks = stacks + self.kernel_size = kernel_size + + assert layers % stacks == 0 + layers_per_stack = layers // stacks + + self.first_conv = nn.Conv1D( + in_channels, residual_channels, 1, bias_attr=True) + self.upsample_net = ConvInUpsampleNet( + upsample_scales=upsample_scales, + nonlinear_activation=nonlinear_activation, + nonlinear_activation_params=nonlinear_activation_params, + interpolate_mode=interpolate_mode, + freq_axis_kernel_size=freq_axis_kernel_size, + aux_channels=aux_channels, + aux_context_window=aux_context_window, + use_causal_conv=use_causal_conv) + self.upsample_factor = np.prod(upsample_scales) + + self.conv_layers = nn.LayerList() + for layer in range(layers): + dilation = 2**(layer % layers_per_stack) + conv = ResidualBlock( + kernel_size=kernel_size, + residual_channels=residual_channels, + gate_channels=gate_channels, + skip_channels=skip_channels, + aux_channels=aux_channels, + dilation=dilation, + dropout=dropout, + bias=bias, + use_causal_conv=use_causal_conv) + self.conv_layers.append(conv) + + self.last_conv_layers = nn.Sequential(nn.ReLU(), + nn.Conv1D( + skip_channels, + skip_channels, + 1, + bias_attr=True), + nn.ReLU(), + nn.Conv1D( + skip_channels, + out_channels, + 1, + bias_attr=True)) + + if use_weight_norm: + self.apply_weight_norm() + + def forward(self, x, c): + """Generate waveform. + + Args: + x(Tensor): Shape (N, C_in, T), The input waveform. + c(Tensor): Shape (N, C_aux, T'). The auxiliary input (e.g. spectrogram). It + is upsampled to match the time resolution of the input. + + Returns: + Tensor: Shape (N, C_out, T), the generated waveform. + """ + c = self.upsample_net(c) + assert c.shape[-1] == x.shape[-1] + + x = self.first_conv(x) + skips = 0 + for f in self.conv_layers: + x, s = f(x, c) + skips += s + skips *= math.sqrt(1.0 / len(self.conv_layers)) + + x = self.last_conv_layers(skips) + return x + + def apply_weight_norm(self): + """Recursively apply weight normalization to all the Convolution layers + in the sublayers. + """ + + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + """Recursively remove weight normalization from all the Convolution + layers in the sublayers. + """ + + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + def inference(self, c=None): + """Waveform generation. This function is used for single instance inference. + + Args: + c(Tensor, optional, optional): Shape (T', C_aux), the auxiliary input, by default None + x(Tensor, optional): Shape (T, C_in), the noise waveform, by default None + + Returns: + Tensor: Shape (T, C_out), the generated waveform + """ + # when to static, can not input x, see https://github.com/PaddlePaddle/Parakeet/pull/132/files + x = paddle.randn( + [1, self.in_channels, paddle.shape(c)[0] * self.upsample_factor]) + c = paddle.transpose(c, [1, 0]).unsqueeze(0) # pseudo batch + c = nn.Pad1D(self.aux_context_window, mode='replicate')(c) + out = self(x, c).squeeze(0).transpose([1, 0]) + return out + + +class PWGDiscriminator(nn.Layer): + """A convolutional discriminator for audio. + + Args: + in_channels (int, optional): Number of channels of the input audio, by default 1 + out_channels (int, optional): Output feature size, by default 1 + kernel_size (int, optional): Kernel size of convolutional sublayers, by default 3 + layers (int, optional): Number of layers, by default 10 + conv_channels (int, optional): Feature size of the convolutional sublayers, by default 64 + dilation_factor (int, optional): The factor with which dilation of each convolutional sublayers grows + exponentially if it is greater than 1, else the dilation of each convolutional sublayers grows linearly, + by default 1 + nonlinear_activation (str, optional): The activation after each convolutional sublayer, by default "leakyrelu" + nonlinear_activation_params (Dict[str, Any], optional): The parameters passed to the activation's initializer, by default + {"negative_slope": 0.2} + bias (bool, optional): Whether to use bias in convolutional sublayers, by default True + use_weight_norm (bool, optional): Whether to use weight normalization at all convolutional sublayers, + by default True + """ + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + kernel_size: int=3, + layers: int=10, + conv_channels: int=64, + dilation_factor: int=1, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + bias: bool=True, + use_weight_norm: bool=True, + init_type: str="xavier_uniform", ): + super().__init__() + + # initialize parameters + initialize(self, init_type) + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + assert kernel_size % 2 == 1 + assert dilation_factor > 0 + conv_layers = [] + conv_in_channels = in_channels + for i in range(layers - 1): + if i == 0: + dilation = 1 + else: + dilation = i if dilation_factor == 1 else dilation_factor**i + conv_in_channels = conv_channels + padding = (kernel_size - 1) // 2 * dilation + conv_layer = nn.Conv1D( + conv_in_channels, + conv_channels, + kernel_size, + padding=padding, + dilation=dilation, + bias_attr=bias) + nonlinear = get_activation(nonlinear_activation, + **nonlinear_activation_params) + conv_layers.append(conv_layer) + conv_layers.append(nonlinear) + padding = (kernel_size - 1) // 2 + last_conv = nn.Conv1D( + conv_in_channels, + out_channels, + kernel_size, + padding=padding, + bias_attr=bias) + conv_layers.append(last_conv) + self.conv_layers = nn.Sequential(*conv_layers) + + if use_weight_norm: + self.apply_weight_norm() + + def forward(self, x): + """ + + Args: + x (Tensor): Shape (N, in_channels, num_samples), the input audio. + + Returns: + Tensor: Shape (N, out_channels, num_samples), the predicted logits. + """ + return self.conv_layers(x) + + def apply_weight_norm(self): + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + +class ResidualPWGDiscriminator(nn.Layer): + """A wavenet-style discriminator for audio. + + Args: + in_channels (int, optional): Number of channels of the input audio, by default 1 + out_channels (int, optional): Output feature size, by default 1 + kernel_size (int, optional): Kernel size of residual blocks, by default 3 + layers (int, optional): Number of residual blocks, by default 30 + stacks (int, optional): Number of groups of residual blocks, within which the dilation + of each residual blocks grows exponentially, by default 3 + residual_channels (int, optional): Residual channels of residual blocks, by default 64 + gate_channels (int, optional): Gate channels of residual blocks, by default 128 + skip_channels (int, optional): Skip channels of residual blocks, by default 64 + dropout (float, optional): Dropout probability of residual blocks, by default 0. + bias (bool, optional): Whether to use bias in residual blocks, by default True + use_weight_norm (bool, optional): Whether to use weight normalization in all convolutional layers, + by default True + use_causal_conv (bool, optional): Whether to use causal convolution in residual blocks, by default False + nonlinear_activation (str, optional): Activation after convolutions other than those in residual blocks, + by default "leakyrelu" + nonlinear_activation_params (Dict[str, Any], optional): Parameters to pass to the activation, + by default {"negative_slope": 0.2} + """ + + def __init__( + self, + in_channels: int=1, + out_channels: int=1, + kernel_size: int=3, + layers: int=30, + stacks: int=3, + residual_channels: int=64, + gate_channels: int=128, + skip_channels: int=64, + dropout: float=0., + bias: bool=True, + use_weight_norm: bool=True, + use_causal_conv: bool=False, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + init_type: str="xavier_uniform", ): + super().__init__() + + # initialize parameters + initialize(self, init_type) + + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + assert kernel_size % 2 == 1 + self.in_channels = in_channels + self.out_channels = out_channels + self.layers = layers + self.stacks = stacks + self.kernel_size = kernel_size + + assert layers % stacks == 0 + layers_per_stack = layers // stacks + + self.first_conv = nn.Sequential( + nn.Conv1D(in_channels, residual_channels, 1, bias_attr=True), + get_activation(nonlinear_activation, **nonlinear_activation_params)) + + self.conv_layers = nn.LayerList() + for layer in range(layers): + dilation = 2**(layer % layers_per_stack) + conv = ResidualBlock( + kernel_size=kernel_size, + residual_channels=residual_channels, + gate_channels=gate_channels, + skip_channels=skip_channels, + aux_channels=None, # no auxiliary input + dropout=dropout, + dilation=dilation, + bias=bias, + use_causal_conv=use_causal_conv) + self.conv_layers.append(conv) + + self.last_conv_layers = nn.Sequential( + get_activation(nonlinear_activation, **nonlinear_activation_params), + nn.Conv1D(skip_channels, skip_channels, 1, bias_attr=True), + get_activation(nonlinear_activation, **nonlinear_activation_params), + nn.Conv1D(skip_channels, out_channels, 1, bias_attr=True)) + + if use_weight_norm: + self.apply_weight_norm() + + def forward(self, x): + """ + Args: + x(Tensor): Shape (N, in_channels, num_samples), the input audio.↩ + + Returns: + Tensor: Shape (N, out_channels, num_samples), the predicted logits. + """ + x = self.first_conv(x) + skip = 0 + for f in self.conv_layers: + x, h = f(x, None) + skip += h + skip *= math.sqrt(1 / len(self.conv_layers)) + + x = skip + x = self.last_conv_layers(x) + return x + + def apply_weight_norm(self): + def _apply_weight_norm(layer): + if isinstance(layer, (nn.Conv1D, nn.Conv2D)): + nn.utils.weight_norm(layer) + + self.apply(_apply_weight_norm) + + def remove_weight_norm(self): + def _remove_weight_norm(layer): + try: + nn.utils.remove_weight_norm(layer) + except ValueError: + pass + + self.apply(_remove_weight_norm) + + +class PWGInference(nn.Layer): + def __init__(self, normalizer, pwg_generator): + super().__init__() + self.normalizer = normalizer + self.pwg_generator = pwg_generator + + def forward(self, logmel): + normalized_mel = self.normalizer(logmel) + wav = self.pwg_generator.inference(normalized_mel) + return wav diff --git a/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan_updater.py b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan_updater.py new file mode 100644 index 0000000..40cfff5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/parallel_wavegan/parallel_wavegan_updater.py @@ -0,0 +1,228 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path +from typing import Dict + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +from paddlespeech.t2s.training.updaters.standard_updater import UpdaterState + +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class PWGUpdater(StandardUpdater): + def __init__(self, + models: Dict[str, Layer], + optimizers: Dict[str, Optimizer], + criterions: Dict[str, Layer], + schedulers: Dict[str, LRScheduler], + dataloader: DataLoader, + generator_train_start_steps: int=0, + discriminator_train_start_steps: int=100000, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + output_dir: Path=None): + self.models = models + self.generator: Layer = models['generator'] + self.discriminator: Layer = models['discriminator'] + + self.optimizers = optimizers + self.optimizer_g: Optimizer = optimizers['generator'] + self.optimizer_d: Optimizer = optimizers['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_mse = criterions['mse'] + + self.schedulers = schedulers + self.scheduler_g = schedulers['generator'] + self.scheduler_d = schedulers['discriminator'] + + self.dataloader = dataloader + + self.generator_train_start_steps = generator_train_start_steps + self.discriminator_train_start_steps = discriminator_train_start_steps + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + self.state = UpdaterState(iteration=0, epoch=0) + + self.train_iterator = iter(self.dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # parse batch + wav, mel = batch + + # Generator + if self.state.iteration > self.generator_train_start_steps: + noise = paddle.randn(wav.shape) + wav_ = self.generator(noise, mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + aux_loss += sc_loss + mag_loss + report("train/spectral_convergence_loss", float(sc_loss)) + report("train/log_stft_magnitude_loss", float(mag_loss)) + + gen_loss += aux_loss * self.lambda_aux + + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + + # adversarial loss + if self.state.iteration > self.discriminator_train_start_steps: + p_ = self.discriminator(wav_) + adv_loss = self.criterion_mse(p_, paddle.ones_like(p_)) + report("train/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + report("train/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + self.optimizer_g.clear_grad() + gen_loss.backward() + + self.optimizer_g.step() + self.scheduler_g.step() + + # Disctiminator + if self.state.iteration > self.discriminator_train_start_steps: + with paddle.no_grad(): + wav_ = self.generator(noise, mel) + p = self.discriminator(wav) + p_ = self.discriminator(wav_.detach()) + real_loss = self.criterion_mse(p, paddle.ones_like(p)) + fake_loss = self.criterion_mse(p_, paddle.zeros_like(p_)) + dis_loss = real_loss + fake_loss + report("train/real_loss", float(real_loss)) + report("train/fake_loss", float(fake_loss)) + report("train/discriminator_loss", float(dis_loss)) + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.optimizer_d.clear_grad() + dis_loss.backward() + + self.optimizer_d.step() + self.scheduler_d.step() + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class PWGEvaluator(StandardEvaluator): + def __init__(self, + models: Dict[str, Layer], + criterions: Dict[str, Layer], + dataloader: DataLoader, + lambda_adv: float=1.0, + lambda_aux: float=1.0, + output_dir: Path=None): + self.models = models + self.generator = models['generator'] + self.discriminator = models['discriminator'] + + self.criterions = criterions + self.criterion_stft = criterions['stft'] + self.criterion_mse = criterions['mse'] + + self.dataloader = dataloader + + self.lambda_adv = lambda_adv + self.lambda_aux = lambda_aux + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + # logging.debug("Evaluate: ") + self.msg = "Evaluate: " + losses_dict = {} + wav, mel = batch + noise = paddle.randn(wav.shape) + + # Generator + wav_ = self.generator(noise, mel) + + # initialize + gen_loss = 0.0 + aux_loss = 0.0 + + # adversarial loss + p_ = self.discriminator(wav_) + adv_loss = self.criterion_mse(p_, paddle.ones_like(p_)) + report("eval/adversarial_loss", float(adv_loss)) + losses_dict["adversarial_loss"] = float(adv_loss) + + gen_loss += self.lambda_adv * adv_loss + + # multi-resolution stft loss + sc_loss, mag_loss = self.criterion_stft(wav_, wav) + report("eval/spectral_convergence_loss", float(sc_loss)) + report("eval/log_stft_magnitude_loss", float(mag_loss)) + losses_dict["spectral_convergence_loss"] = float(sc_loss) + losses_dict["log_stft_magnitude_loss"] = float(mag_loss) + aux_loss += sc_loss + mag_loss + + gen_loss += aux_loss * self.lambda_aux + + report("eval/generator_loss", float(gen_loss)) + losses_dict["generator_loss"] = float(gen_loss) + + # Disctiminator + p = self.discriminator(wav) + real_loss = self.criterion_mse(p, paddle.ones_like(p)) + fake_loss = self.criterion_mse(p_, paddle.zeros_like(p_)) + dis_loss = real_loss + fake_loss + report("eval/real_loss", float(real_loss)) + report("eval/fake_loss", float(fake_loss)) + report("eval/discriminator_loss", float(dis_loss)) + + losses_dict["real_loss"] = float(real_loss) + losses_dict["fake_loss"] = float(fake_loss) + losses_dict["discriminator_loss"] = float(dis_loss) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/speedyspeech/__init__.py b/ernie-sat/paddlespeech/t2s/models/speedyspeech/__init__.py new file mode 100644 index 0000000..abdac8d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/speedyspeech/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .speedyspeech import * +from .speedyspeech_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech.py b/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech.py new file mode 100644 index 0000000..44ccfc6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech.py @@ -0,0 +1,254 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.positional_encoding import sinusoid_position_encoding +from paddlespeech.t2s.modules.predictor.length_regulator import LengthRegulator + + +class ResidualBlock(nn.Layer): + def __init__(self, channels, kernel_size, dilation, n=2): + super().__init__() + blocks = [ + nn.Sequential( + nn.Conv1D( + channels, + channels, + kernel_size, + dilation=dilation, + padding="same", + data_format="NLC"), + nn.ReLU(), + nn.BatchNorm1D(channels, data_format="NLC"), ) for _ in range(n) + ] + self.blocks = nn.Sequential(*blocks) + + def forward(self, x): + return x + self.blocks(x) + + +class TextEmbedding(nn.Layer): + def __init__(self, + vocab_size: int, + embedding_size: int, + tone_vocab_size: int=None, + tone_embedding_size: int=None, + padding_idx: int=None, + tone_padding_idx: int=None, + concat: bool=False): + super().__init__() + self.text_embedding = nn.Embedding(vocab_size, embedding_size, + padding_idx) + if tone_vocab_size: + tone_embedding_size = tone_embedding_size or embedding_size + if tone_embedding_size != embedding_size and not concat: + raise ValueError( + "embedding size != tone_embedding size, only conat is avaiable." + ) + self.tone_embedding = nn.Embedding( + tone_vocab_size, tone_embedding_size, tone_padding_idx) + self.concat = concat + + def forward(self, text, tone=None): + text_embed = self.text_embedding(text) + if tone is None: + return text_embed + tone_embed = self.tone_embedding(tone) + if self.concat: + embed = paddle.concat([text_embed, tone_embed], -1) + else: + embed = text_embed + tone_embed + return embed + + +class SpeedySpeechEncoder(nn.Layer): + def __init__(self, + vocab_size, + tone_size, + hidden_size, + kernel_size, + dilations, + spk_num=None): + super().__init__() + self.embedding = TextEmbedding( + vocab_size, + hidden_size, + tone_size, + padding_idx=0, + tone_padding_idx=0) + + if spk_num: + self.spk_emb = nn.Embedding( + num_embeddings=spk_num, + embedding_dim=hidden_size, + padding_idx=0) + else: + self.spk_emb = None + + self.prenet = nn.Sequential( + nn.Linear(hidden_size, hidden_size), + nn.ReLU(), ) + res_blocks = [ + ResidualBlock(hidden_size, kernel_size, d, n=2) for d in dilations + ] + self.res_blocks = nn.Sequential(*res_blocks) + + self.postnet1 = nn.Sequential(nn.Linear(hidden_size, hidden_size)) + self.postnet2 = nn.Sequential( + nn.ReLU(), + nn.BatchNorm1D(hidden_size, data_format="NLC"), + nn.Linear(hidden_size, hidden_size), ) + + def forward(self, text, tones, spk_id=None): + embedding = self.embedding(text, tones) + if self.spk_emb: + embedding += self.spk_emb(spk_id).unsqueeze(1) + embedding = self.prenet(embedding) + x = self.res_blocks(embedding) + x = embedding + self.postnet1(x) + x = self.postnet2(x) + return x + + +class DurationPredictor(nn.Layer): + def __init__(self, hidden_size): + super().__init__() + self.layers = nn.Sequential( + ResidualBlock(hidden_size, 4, 1, n=1), + ResidualBlock(hidden_size, 3, 1, n=1), + ResidualBlock(hidden_size, 1, 1, n=1), nn.Linear(hidden_size, 1)) + + def forward(self, x): + return paddle.squeeze(self.layers(x), -1) + + +class SpeedySpeechDecoder(nn.Layer): + def __init__(self, hidden_size, output_size, kernel_size, dilations): + super().__init__() + res_blocks = [ + ResidualBlock(hidden_size, kernel_size, d, n=2) for d in dilations + ] + self.res_blocks = nn.Sequential(*res_blocks) + + self.postnet1 = nn.Sequential(nn.Linear(hidden_size, hidden_size)) + self.postnet2 = nn.Sequential( + ResidualBlock(hidden_size, kernel_size, 1, n=2), + nn.Linear(hidden_size, output_size)) + + def forward(self, x): + xx = self.res_blocks(x) + x = x + self.postnet1(xx) + x = self.postnet2(x) + return x + + +class SpeedySpeech(nn.Layer): + def __init__( + self, + vocab_size, + encoder_hidden_size, + encoder_kernel_size, + encoder_dilations, + duration_predictor_hidden_size, + decoder_hidden_size, + decoder_output_size, + decoder_kernel_size, + decoder_dilations, + tone_size=None, + spk_num=None, + init_type: str="xavier_uniform", ): + super().__init__() + + # initialize parameters + initialize(self, init_type) + + encoder = SpeedySpeechEncoder(vocab_size, tone_size, + encoder_hidden_size, encoder_kernel_size, + encoder_dilations, spk_num) + duration_predictor = DurationPredictor(duration_predictor_hidden_size) + decoder = SpeedySpeechDecoder(decoder_hidden_size, decoder_output_size, + decoder_kernel_size, decoder_dilations) + + self.encoder = encoder + self.duration_predictor = duration_predictor + self.decoder = decoder + # define length regulator + self.length_regulator = LengthRegulator() + + nn.initializer.set_global_initializer(None) + + def forward(self, text, tones, durations, spk_id: paddle.Tensor=None): + # input of embedding must be int64 + text = paddle.cast(text, 'int64') + tones = paddle.cast(tones, 'int64') + if spk_id is not None: + spk_id = paddle.cast(spk_id, 'int64') + durations = paddle.cast(durations, 'int64') + encodings = self.encoder(text, tones, spk_id) + + pred_durations = self.duration_predictor(encodings.detach()) + + # expand encodings + durations_to_expand = durations + encodings = self.length_regulator(encodings, durations_to_expand) + + # decode + # remove positional encoding here + _, t_dec, feature_size = encodings.shape + encodings += sinusoid_position_encoding(t_dec, feature_size) + decoded = self.decoder(encodings) + return decoded, pred_durations + + def inference(self, text, tones=None, durations=None, spk_id=None): + # text: [T] + # tones: [T] + # input of embedding must be int64 + text = paddle.cast(text, 'int64') + text = text.unsqueeze(0) + if tones is not None: + tones = paddle.cast(tones, 'int64') + tones = tones.unsqueeze(0) + + encodings = self.encoder(text, tones, spk_id) + + if durations is None: + # (1, T) + pred_durations = self.duration_predictor(encodings) + durations_to_expand = paddle.round(pred_durations.exp()) + durations_to_expand = durations_to_expand.astype(paddle.int64) + else: + durations_to_expand = durations + encodings = self.length_regulator( + encodings, durations_to_expand, is_inference=True) + + shape = paddle.shape(encodings) + t_dec, feature_size = shape[1], shape[2] + encodings += sinusoid_position_encoding(t_dec, feature_size) + decoded = self.decoder(encodings) + return decoded[0] + + +class SpeedySpeechInference(nn.Layer): + def __init__(self, normalizer, speedyspeech_model): + super().__init__() + self.normalizer = normalizer + self.acoustic_model = speedyspeech_model + + def forward(self, phones, tones, spk_id=None, durations=None): + normalized_mel = self.acoustic_model.inference( + phones, tones, durations=durations, spk_id=spk_id) + logmel = self.normalizer.inverse(normalized_mel) + return logmel diff --git a/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech_updater.py b/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech_updater.py new file mode 100644 index 0000000..e30a3fe --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/speedyspeech/speedyspeech_updater.py @@ -0,0 +1,172 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path + +import paddle +from paddle import distributed as dist +from paddle.fluid.layers import huber_loss +from paddle.io import DataLoader +from paddle.nn import functional as F +from paddle.nn import Layer +from paddle.optimizer import Optimizer + +from paddlespeech.t2s.modules.losses import masked_l1_loss +from paddlespeech.t2s.modules.losses import ssim +from paddlespeech.t2s.modules.losses import weighted_mean +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class SpeedySpeechUpdater(StandardUpdater): + def __init__(self, + model: Layer, + optimizer: Optimizer, + dataloader: DataLoader, + init_state=None, + output_dir: Path=None): + super().__init__(model, optimizer, dataloader, init_state=None) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + + # spk_id!=None in multiple spk speedyspeech + spk_id = batch["spk_id"] if "spk_id" in batch else None + + decoded, predicted_durations = self.model( + text=batch["phones"], + tones=batch["tones"], + durations=batch["durations"], + spk_id=spk_id) + + target_mel = batch["feats"] + spec_mask = F.sequence_mask( + batch["num_frames"], dtype=target_mel.dtype).unsqueeze(-1) + text_mask = F.sequence_mask( + batch["num_phones"], dtype=predicted_durations.dtype) + + # spec loss + l1_loss = masked_l1_loss(decoded, target_mel, spec_mask) + + # duration loss + target_durations = batch["durations"] + target_durations = paddle.maximum( + target_durations.astype(predicted_durations.dtype), + paddle.to_tensor([1.0])) + duration_loss = weighted_mean( + huber_loss( + predicted_durations, paddle.log(target_durations), delta=1.0), + text_mask, ) + + # ssim loss + ssim_loss = 1.0 - ssim((decoded * spec_mask).unsqueeze(1), + (target_mel * spec_mask).unsqueeze(1)) + + loss = l1_loss + ssim_loss + duration_loss + + optimizer = self.optimizer + optimizer.clear_grad() + loss.backward() + optimizer.step() + + report("train/loss", float(loss)) + report("train/l1_loss", float(l1_loss)) + report("train/duration_loss", float(duration_loss)) + report("train/ssim_loss", float(ssim_loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["duration_loss"] = float(duration_loss) + losses_dict["ssim_loss"] = float(ssim_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class SpeedySpeechEvaluator(StandardEvaluator): + def __init__(self, + model: Layer, + dataloader: DataLoader, + output_dir: Path=None): + super().__init__(model, dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + + spk_id = batch["spk_id"] if "spk_id" in batch else None + + decoded, predicted_durations = self.model( + text=batch["phones"], + tones=batch["tones"], + durations=batch["durations"], + spk_id=spk_id) + + target_mel = batch["feats"] + spec_mask = F.sequence_mask( + batch["num_frames"], dtype=target_mel.dtype).unsqueeze(-1) + text_mask = F.sequence_mask( + batch["num_phones"], dtype=predicted_durations.dtype) + + # spec loss + l1_loss = masked_l1_loss(decoded, target_mel, spec_mask) + + # duration loss + target_durations = batch["durations"] + target_durations = paddle.maximum( + target_durations.astype(predicted_durations.dtype), + paddle.to_tensor([1.0])) + duration_loss = weighted_mean( + huber_loss( + predicted_durations, paddle.log(target_durations), delta=1.0), + text_mask, ) + + # ssim loss + ssim_loss = 1.0 - ssim((decoded * spec_mask).unsqueeze(1), + (target_mel * spec_mask).unsqueeze(1)) + + loss = l1_loss + ssim_loss + duration_loss + + # import pdb; pdb.set_trace() + + report("eval/loss", float(loss)) + report("eval/l1_loss", float(l1_loss)) + report("eval/duration_loss", float(duration_loss)) + report("eval/ssim_loss", float(ssim_loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["duration_loss"] = float(duration_loss) + losses_dict["ssim_loss"] = float(ssim_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/tacotron2/__init__.py b/ernie-sat/paddlespeech/t2s/models/tacotron2/__init__.py new file mode 100644 index 0000000..ea63257 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/tacotron2/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .tacotron2 import * +from .tacotron2_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2.py b/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2.py new file mode 100644 index 0000000..7b306e4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2.py @@ -0,0 +1,441 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Tacotron 2 related modules for paddle""" +import logging +from typing import Dict +from typing import Optional +from typing import Tuple + +import paddle +import paddle.nn.functional as F +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.nets_utils import make_pad_mask +from paddlespeech.t2s.modules.tacotron2.attentions import AttForward +from paddlespeech.t2s.modules.tacotron2.attentions import AttForwardTA +from paddlespeech.t2s.modules.tacotron2.attentions import AttLoc +from paddlespeech.t2s.modules.tacotron2.decoder import Decoder +from paddlespeech.t2s.modules.tacotron2.encoder import Encoder + + +class Tacotron2(nn.Layer): + """Tacotron2 module for end-to-end text-to-speech. + + This is a module of Spectrogram prediction network in Tacotron2 described + in `Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`_, + which converts the sequence of characters into the sequence of Mel-filterbanks. + + .. _`Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`: + https://arxiv.org/abs/1712.05884 + + """ + + def __init__( + self, + # network structure related + idim: int, + odim: int, + embed_dim: int=512, + elayers: int=1, + eunits: int=512, + econv_layers: int=3, + econv_chans: int=512, + econv_filts: int=5, + atype: str="location", + adim: int=512, + aconv_chans: int=32, + aconv_filts: int=15, + cumulate_att_w: bool=True, + dlayers: int=2, + dunits: int=1024, + prenet_layers: int=2, + prenet_units: int=256, + postnet_layers: int=5, + postnet_chans: int=512, + postnet_filts: int=5, + output_activation: str=None, + use_batch_norm: bool=True, + use_concate: bool=True, + use_residual: bool=False, + reduction_factor: int=1, + # extra embedding related + spk_num: Optional[int]=None, + lang_num: Optional[int]=None, + spk_embed_dim: Optional[int]=None, + spk_embed_integration_type: str="concat", + dropout_rate: float=0.5, + zoneout_rate: float=0.1, + # training related + init_type: str="xavier_uniform", ): + """Initialize Tacotron2 module. + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + embed_dim (int): Dimension of the token embedding. + elayers (int): Number of encoder blstm layers. + eunits (int): Number of encoder blstm units. + econv_layers (int): Number of encoder conv layers. + econv_filts (int): Number of encoder conv filter size. + econv_chans (int): Number of encoder conv filter channels. + dlayers (int): Number of decoder lstm layers. + dunits (int): Number of decoder lstm units. + prenet_layers (int): Number of prenet layers. + prenet_units (int): Number of prenet units. + postnet_layers (int): Number of postnet layers. + postnet_filts (int): Number of postnet filter size. + postnet_chans (int): Number of postnet filter channels. + output_activation (str): Name of activation function for outputs. + adim (int): Number of dimension of mlp in attention. + aconv_chans (int): Number of attention conv filter channels. + aconv_filts (int): Number of attention conv filter size. + cumulate_att_w (bool): Whether to cumulate previous attention weight. + use_batch_norm (bool): Whether to use batch normalization. + use_concate (bool): Whether to concat enc outputs w/ dec lstm outputs. + reduction_factor (int): Reduction factor. + spk_num (Optional[int]): Number of speakers. If set to > 1, assume that the + sids will be provided as the input and use sid embedding layer. + lang_num (Optional[int]): Number of languages. If set to > 1, assume that the + lids will be provided as the input and use sid embedding layer. + spk_embed_dim (Optional[int]): Speaker embedding dimension. If set to > 0, + assume that spk_emb will be provided as the input. + spk_embed_integration_type (str): How to integrate speaker embedding. + dropout_rate (float): Dropout rate. + zoneout_rate (float): Zoneout rate. + """ + assert check_argument_types() + super().__init__() + + # store hyperparameters + self.idim = idim + self.odim = odim + self.eos = idim - 1 + self.cumulate_att_w = cumulate_att_w + self.reduction_factor = reduction_factor + + # define activation function for the final output + if output_activation is None: + self.output_activation_fn = None + elif hasattr(F, output_activation): + self.output_activation_fn = getattr(F, output_activation) + else: + raise ValueError(f"there is no such an activation function. " + f"({output_activation})") + + # set padding idx + padding_idx = 0 + self.padding_idx = padding_idx + + # initialize parameters + initialize(self, init_type) + + # define network modules + self.enc = Encoder( + idim=idim, + embed_dim=embed_dim, + elayers=elayers, + eunits=eunits, + econv_layers=econv_layers, + econv_chans=econv_chans, + econv_filts=econv_filts, + use_batch_norm=use_batch_norm, + use_residual=use_residual, + dropout_rate=dropout_rate, + padding_idx=padding_idx, ) + + self.spk_num = None + if spk_num is not None and spk_num > 1: + self.spk_num = spk_num + self.sid_emb = nn.Embedding(spk_num, eunits) + self.lang_num = None + if lang_num is not None and lang_num > 1: + self.lang_num = lang_num + self.lid_emb = nn.Embedding(lang_num, eunits) + + self.spk_embed_dim = None + if spk_embed_dim is not None and spk_embed_dim > 0: + self.spk_embed_dim = spk_embed_dim + self.spk_embed_integration_type = spk_embed_integration_type + if self.spk_embed_dim is None: + dec_idim = eunits + elif self.spk_embed_integration_type == "concat": + dec_idim = eunits + spk_embed_dim + elif self.spk_embed_integration_type == "add": + dec_idim = eunits + self.projection = nn.Linear(self.spk_embed_dim, eunits) + else: + raise ValueError(f"{spk_embed_integration_type} is not supported.") + + if atype == "location": + att = AttLoc(dec_idim, dunits, adim, aconv_chans, aconv_filts) + elif atype == "forward": + att = AttForward(dec_idim, dunits, adim, aconv_chans, aconv_filts) + if self.cumulate_att_w: + logging.warning("cumulation of attention weights is disabled " + "in forward attention.") + self.cumulate_att_w = False + elif atype == "forward_ta": + att = AttForwardTA(dec_idim, dunits, adim, aconv_chans, aconv_filts, + odim) + if self.cumulate_att_w: + logging.warning("cumulation of attention weights is disabled " + "in forward attention.") + self.cumulate_att_w = False + else: + raise NotImplementedError("Support only location or forward") + self.dec = Decoder( + idim=dec_idim, + odim=odim, + att=att, + dlayers=dlayers, + dunits=dunits, + prenet_layers=prenet_layers, + prenet_units=prenet_units, + postnet_layers=postnet_layers, + postnet_chans=postnet_chans, + postnet_filts=postnet_filts, + output_activation_fn=self.output_activation_fn, + cumulate_att_w=self.cumulate_att_w, + use_batch_norm=use_batch_norm, + use_concate=use_concate, + dropout_rate=dropout_rate, + zoneout_rate=zoneout_rate, + reduction_factor=reduction_factor, ) + + nn.initializer.set_global_initializer(None) + + def forward( + self, + text: paddle.Tensor, + text_lengths: paddle.Tensor, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + spk_emb: Optional[paddle.Tensor]=None, + spk_id: Optional[paddle.Tensor]=None, + lang_id: Optional[paddle.Tensor]=None + ) -> Tuple[paddle.Tensor, Dict[str, paddle.Tensor], paddle.Tensor]: + """Calculate forward propagation. + + Args: + text (Tensor(int64)): Batch of padded character ids (B, T_text). + text_lengths (Tensor(int64)): Batch of lengths of each input batch (B,). + speech (Tensor): Batch of padded target features (B, T_feats, odim). + speech_lengths (Tensor(int64)): Batch of the lengths of each target (B,). + spk_emb (Optional[Tensor]): Batch of speaker embeddings (B, spk_embed_dim). + spk_id (Optional[Tensor]): Batch of speaker IDs (B, 1). + lang_id (Optional[Tensor]): Batch of language IDs (B, 1). + + Returns: + Tensor: Loss scalar value. + Dict: Statistics to be monitored. + Tensor: Weight value if not joint training else model outputs. + + """ + text = text[:, :text_lengths.max()] + speech = speech[:, :speech_lengths.max()] + + batch_size = paddle.shape(text)[0] + + # Add eos at the last of sequence + xs = F.pad(text, [0, 0, 0, 1], "constant", self.padding_idx) + for i, l in enumerate(text_lengths): + xs[i, l] = self.eos + ilens = text_lengths + 1 + + ys = speech + olens = speech_lengths + + # make labels for stop prediction + stop_labels = make_pad_mask(olens - 1) + # bool 类型无法切片 + stop_labels = paddle.cast(stop_labels, dtype='float32') + stop_labels = F.pad(stop_labels, [0, 0, 0, 1], "constant", 1.0) + + # calculate tacotron2 outputs + after_outs, before_outs, logits, att_ws = self._forward( + xs=xs, + ilens=ilens, + ys=ys, + olens=olens, + spk_emb=spk_emb, + spk_id=spk_id, + lang_id=lang_id, ) + + # modify mod part of groundtruth + if self.reduction_factor > 1: + assert olens.ge(self.reduction_factor).all( + ), "Output length must be greater than or equal to reduction factor." + olens = olens - olens % self.reduction_factor + max_out = max(olens) + ys = ys[:, :max_out] + stop_labels = stop_labels[:, :max_out] + stop_labels = paddle.scatter(stop_labels, 1, + (olens - 1).unsqueeze(1), 1.0) + olens_in = olens // self.reduction_factor + else: + olens_in = olens + return after_outs, before_outs, logits, ys, stop_labels, olens, att_ws, olens_in + + def _forward( + self, + xs: paddle.Tensor, + ilens: paddle.Tensor, + ys: paddle.Tensor, + olens: paddle.Tensor, + spk_emb: paddle.Tensor, + spk_id: paddle.Tensor, + lang_id: paddle.Tensor, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + + hs, hlens = self.enc(xs, ilens) + if self.spk_num is not None: + sid_embs = self.sid_emb(spk_id.reshape([-1])) + hs = hs + sid_embs.unsqueeze(1) + if self.lang_num is not None: + lid_embs = self.lid_emb(lang_id.reshape([-1])) + hs = hs + lid_embs.unsqueeze(1) + if self.spk_embed_dim is not None: + hs = self._integrate_with_spk_embed(hs, spk_emb) + + return self.dec(hs, hlens, ys) + + def inference( + self, + text: paddle.Tensor, + speech: Optional[paddle.Tensor]=None, + spk_emb: Optional[paddle.Tensor]=None, + spk_id: Optional[paddle.Tensor]=None, + lang_id: Optional[paddle.Tensor]=None, + threshold: float=0.5, + minlenratio: float=0.0, + maxlenratio: float=10.0, + use_att_constraint: bool=False, + backward_window: int=1, + forward_window: int=3, + use_teacher_forcing: bool=False, ) -> Dict[str, paddle.Tensor]: + """Generate the sequence of features given the sequences of characters. + + Args: + text (Tensor(int64)): Input sequence of characters (T_text,). + speech (Optional[Tensor]): Feature sequence to extract style (N, idim). + spk_emb (ptional[Tensor]): Speaker embedding (spk_embed_dim,). + spk_id (Optional[Tensor]): Speaker ID (1,). + lang_id (Optional[Tensor]): Language ID (1,). + threshold (float): Threshold in inference. + minlenratio (float): Minimum length ratio in inference. + maxlenratio (float): Maximum length ratio in inference. + use_att_constraint (bool): Whether to apply attention constraint. + backward_window (int): Backward window in attention constraint. + forward_window (int): Forward window in attention constraint. + use_teacher_forcing (bool): Whether to use teacher forcing. + + Returns: + Dict[str, Tensor] + Output dict including the following items: + * feat_gen (Tensor): Output sequence of features (T_feats, odim). + * prob (Tensor): Output sequence of stop probabilities (T_feats,). + * att_w (Tensor): Attention weights (T_feats, T). + + """ + x = text + y = speech + + # add eos at the last of sequence + x = F.pad(x, [0, 1], "constant", self.eos) + + # inference with teacher forcing + if use_teacher_forcing: + assert speech is not None, "speech must be provided with teacher forcing." + + xs, ys = x.unsqueeze(0), y.unsqueeze(0) + spk_emb = None if spk_emb is None else spk_emb.unsqueeze(0) + ilens = paddle.shape(xs)[1] + olens = paddle.shape(ys)[1] + outs, _, _, att_ws = self._forward( + xs=xs, + ilens=ilens, + ys=ys, + olens=olens, + spk_emb=spk_emb, + spk_id=spk_id, + lang_id=lang_id, ) + + return dict(feat_gen=outs[0], att_w=att_ws[0]) + + # inference + h = self.enc.inference(x) + + if self.spk_num is not None: + sid_emb = self.sid_emb(spk_id.reshape([-1])) + h = h + sid_emb + if self.lang_num is not None: + lid_emb = self.lid_emb(lang_id.reshape([-1])) + h = h + lid_emb + if self.spk_embed_dim is not None: + hs, spk_emb = h.unsqueeze(0), spk_emb.unsqueeze(0) + h = self._integrate_with_spk_embed(hs, spk_emb)[0] + out, prob, att_w = self.dec.inference( + h, + threshold=threshold, + minlenratio=minlenratio, + maxlenratio=maxlenratio, + use_att_constraint=use_att_constraint, + backward_window=backward_window, + forward_window=forward_window, ) + + return dict(feat_gen=out, prob=prob, att_w=att_w) + + def _integrate_with_spk_embed(self, + hs: paddle.Tensor, + spk_emb: paddle.Tensor) -> paddle.Tensor: + """Integrate speaker embedding with hidden states. + + Args: + hs (Tensor): Batch of hidden state sequences (B, Tmax, eunits). + spk_emb (Tensor): Batch of speaker embeddings (B, spk_embed_dim). + + Returns: + Tensor: Batch of integrated hidden state sequences (B, Tmax, eunits) if + integration_type is "add" else (B, Tmax, eunits + spk_embed_dim). + + """ + if self.spk_embed_integration_type == "add": + # apply projection and then add to hidden states + spk_emb = self.projection(F.normalize(spk_emb)) + hs = hs + spk_emb.unsqueeze(1) + elif self.spk_embed_integration_type == "concat": + # concat hidden states with spk embeds + spk_emb = F.normalize(spk_emb).unsqueeze(1).expand( + shape=[-1, paddle.shape(hs)[1], -1]) + hs = paddle.concat([hs, spk_emb], axis=-1) + else: + raise NotImplementedError("support only add or concat.") + + return hs + + +class Tacotron2Inference(nn.Layer): + def __init__(self, normalizer, model): + super().__init__() + self.normalizer = normalizer + self.acoustic_model = model + + def forward(self, text, spk_id=None, spk_emb=None): + out = self.acoustic_model.inference( + text, spk_id=spk_id, spk_emb=spk_emb) + normalized_mel = out["feat_gen"] + logmel = self.normalizer.inverse(normalized_mel) + return logmel diff --git a/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2_updater.py b/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2_updater.py new file mode 100644 index 0000000..09e6827 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/tacotron2/tacotron2_updater.py @@ -0,0 +1,219 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path + +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer + +from paddlespeech.t2s.modules.losses import GuidedAttentionLoss +from paddlespeech.t2s.modules.losses import Tacotron2Loss +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class Tacotron2Updater(StandardUpdater): + def __init__(self, + model: Layer, + optimizer: Optimizer, + dataloader: DataLoader, + init_state=None, + use_masking: bool=True, + use_weighted_masking: bool=False, + bce_pos_weight: float=5.0, + loss_type: str="L1+L2", + use_guided_attn_loss: bool=True, + guided_attn_loss_sigma: float=0.4, + guided_attn_loss_lambda: float=1.0, + output_dir: Path=None): + super().__init__(model, optimizer, dataloader, init_state=None) + + self.loss_type = loss_type + self.use_guided_attn_loss = use_guided_attn_loss + + self.taco2_loss = Tacotron2Loss( + use_masking=use_masking, + use_weighted_masking=use_weighted_masking, + bce_pos_weight=bce_pos_weight, ) + if self.use_guided_attn_loss: + self.attn_loss = GuidedAttentionLoss( + sigma=guided_attn_loss_sigma, + alpha=guided_attn_loss_lambda, ) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # spk_id!=None in multiple spk fastspeech2 + spk_id = batch["spk_id"] if "spk_id" in batch else None + spk_emb = batch["spk_emb"] if "spk_emb" in batch else None + if spk_emb is not None: + spk_id = None + + after_outs, before_outs, logits, ys, stop_labels, olens, att_ws, olens_in = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"], + spk_id=spk_id, + spk_emb=spk_emb) + + # calculate taco2 loss + l1_loss, mse_loss, bce_loss = self.taco2_loss( + after_outs=after_outs, + before_outs=before_outs, + logits=logits, + ys=ys, + stop_labels=stop_labels, + olens=olens) + + if self.loss_type == "L1+L2": + loss = l1_loss + mse_loss + bce_loss + elif self.loss_type == "L1": + loss = l1_loss + bce_loss + elif self.loss_type == "L2": + loss = mse_loss + bce_loss + else: + raise ValueError(f"unknown --loss-type {self.loss_type}") + + # calculate attention loss + if self.use_guided_attn_loss: + # NOTE: length of output for auto-regressive + # input will be changed when r > 1 + attn_loss = self.attn_loss( + att_ws=att_ws, ilens=batch["text_lengths"] + 1, olens=olens_in) + loss = loss + attn_loss + + optimizer = self.optimizer + optimizer.clear_grad() + loss.backward() + optimizer.step() + + report("train/l1_loss", float(l1_loss)) + report("train/mse_loss", float(mse_loss)) + report("train/bce_loss", float(bce_loss)) + report("train/attn_loss", float(attn_loss)) + report("train/loss", float(loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["mse_loss"] = float(mse_loss) + losses_dict["bce_loss"] = float(bce_loss) + losses_dict["attn_loss"] = float(attn_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class Tacotron2Evaluator(StandardEvaluator): + def __init__(self, + model: Layer, + dataloader: DataLoader, + use_masking: bool=True, + use_weighted_masking: bool=False, + bce_pos_weight: float=5.0, + loss_type: str="L1+L2", + use_guided_attn_loss: bool=True, + guided_attn_loss_sigma: float=0.4, + guided_attn_loss_lambda: float=1.0, + output_dir=None): + super().__init__(model, dataloader) + + self.loss_type = loss_type + self.use_guided_attn_loss = use_guided_attn_loss + + self.taco2_loss = Tacotron2Loss( + use_masking=use_masking, + use_weighted_masking=use_weighted_masking, + bce_pos_weight=bce_pos_weight, ) + if self.use_guided_attn_loss: + self.attn_loss = GuidedAttentionLoss( + sigma=guided_attn_loss_sigma, + alpha=guided_attn_loss_lambda, ) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + # spk_id!=None in multiple spk fastspeech2 + spk_id = batch["spk_id"] if "spk_id" in batch else None + spk_emb = batch["spk_emb"] if "spk_emb" in batch else None + if spk_emb is not None: + spk_id = None + + after_outs, before_outs, logits, ys, stop_labels, olens, att_ws, olens_in = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"], + spk_id=spk_id, + spk_emb=spk_emb) + + # calculate taco2 loss + l1_loss, mse_loss, bce_loss = self.taco2_loss( + after_outs=after_outs, + before_outs=before_outs, + logits=logits, + ys=ys, + stop_labels=stop_labels, + olens=olens) + + if self.loss_type == "L1+L2": + loss = l1_loss + mse_loss + bce_loss + elif self.loss_type == "L1": + loss = l1_loss + bce_loss + elif self.loss_type == "L2": + loss = mse_loss + bce_loss + else: + raise ValueError(f"unknown --loss-type {self.loss_type}") + + # calculate attention loss + if self.use_guided_attn_loss: + # NOTE: length of output for auto-regressive + # input will be changed when r > 1 + attn_loss = self.attn_loss( + att_ws=att_ws, ilens=batch["text_lengths"] + 1, olens=olens_in) + loss = loss + attn_loss + + report("eval/l1_loss", float(l1_loss)) + report("eval/mse_loss", float(mse_loss)) + report("eval/bce_loss", float(bce_loss)) + report("eval/attn_loss", float(attn_loss)) + report("eval/loss", float(loss)) + + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["mse_loss"] = float(mse_loss) + losses_dict["bce_loss"] = float(bce_loss) + losses_dict["attn_loss"] = float(attn_loss) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/transformer_tts/__init__.py b/ernie-sat/paddlespeech/t2s/models/transformer_tts/__init__.py new file mode 100644 index 0000000..80a151e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/transformer_tts/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .transformer_tts import * +from .transformer_tts_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts.py b/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts.py new file mode 100644 index 0000000..92754c3 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts.py @@ -0,0 +1,674 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Fastspeech2 related modules for paddle""" +from typing import Dict +from typing import Sequence +from typing import Tuple + +import numpy +import paddle +import paddle.nn.functional as F +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.nets_utils import make_non_pad_mask +from paddlespeech.t2s.modules.nets_utils import make_pad_mask +from paddlespeech.t2s.modules.style_encoder import StyleEncoder +from paddlespeech.t2s.modules.tacotron2.decoder import Postnet +from paddlespeech.t2s.modules.tacotron2.decoder import Prenet as DecoderPrenet +from paddlespeech.t2s.modules.tacotron2.encoder import Encoder as EncoderPrenet +from paddlespeech.t2s.modules.transformer.attention import MultiHeadedAttention +from paddlespeech.t2s.modules.transformer.decoder import Decoder +from paddlespeech.t2s.modules.transformer.embedding import PositionalEncoding +from paddlespeech.t2s.modules.transformer.embedding import ScaledPositionalEncoding +from paddlespeech.t2s.modules.transformer.encoder import TransformerEncoder +from paddlespeech.t2s.modules.transformer.mask import subsequent_mask + + +class TransformerTTS(nn.Layer): + """TTS-Transformer module. + + This is a module of text-to-speech Transformer described in `Neural Speech Synthesis + with Transformer Network`_, which convert the sequence of tokens into the sequence + of Mel-filterbanks. + + .. _`Neural Speech Synthesis with Transformer Network`: + https://arxiv.org/pdf/1809.08895.pdf + + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + embed_dim (int, optional): Dimension of character embedding. + eprenet_conv_layers (int, optional): Number of encoder prenet convolution layers. + eprenet_conv_chans (int, optional): Number of encoder prenet convolution channels. + eprenet_conv_filts (int, optional): Filter size of encoder prenet convolution. + dprenet_layers (int, optional): Number of decoder prenet layers. + dprenet_units (int, optional): Number of decoder prenet hidden units. + elayers (int, optional): Number of encoder layers. + eunits (int, optional): Number of encoder hidden units. + adim (int, optional): Number of attention transformation dimensions. + aheads (int, optional): Number of heads for multi head attention. + dlayers (int, optional): Number of decoder layers. + dunits (int, optional): Number of decoder hidden units. + postnet_layers (int, optional): Number of postnet layers. + postnet_chans (int, optional): Number of postnet channels. + postnet_filts (int, optional): Filter size of postnet. + use_scaled_pos_enc (pool, optional): Whether to use trainable scaled positional encoding. + use_batch_norm (bool, optional): Whether to use batch normalization in encoder prenet. + encoder_normalize_before (bool, optional): Whether to perform layer normalization before encoder block. + decoder_normalize_before (bool, optional): Whether to perform layer normalization before decoder block. + encoder_concat_after (bool, optional): Whether to concatenate attention layer's input and output in encoder. + decoder_concat_after (bool, optional): Whether to concatenate attention layer's input and output in decoder. + positionwise_layer_type (str, optional): Position-wise operation type. + positionwise_conv_kernel_size (int, optional): Kernel size in position wise conv 1d. + reduction_factor (int, optional): Reduction factor. + spk_embed_dim (int, optional): Number of speaker embedding dimenstions. + spk_embed_integration_type (str, optional): How to integrate speaker embedding. + use_gst (str, optional): Whether to use global style token. + gst_tokens (int, optional): The number of GST embeddings. + gst_heads (int, optional): The number of heads in GST multihead attention. + gst_conv_layers (int, optional): The number of conv layers in GST. + gst_conv_chans_list (Sequence[int], optional): List of the number of channels of conv layers in GST. + gst_conv_kernel_size (int, optional): Kernal size of conv layers in GST. + gst_conv_stride (int, optional): Stride size of conv layers in GST. + gst_gru_layers (int, optional): The number of GRU layers in GST. + gst_gru_units (int, optional): The number of GRU units in GST. + transformer_lr (float, optional): Initial value of learning rate. + transformer_warmup_steps (int, optional): Optimizer warmup steps. + transformer_enc_dropout_rate (float, optional): Dropout rate in encoder except attention and positional encoding. + transformer_enc_positional_dropout_rate (float, optional): Dropout rate after encoder positional encoding. + transformer_enc_attn_dropout_rate (float, optional): Dropout rate in encoder self-attention module. + transformer_dec_dropout_rate (float, optional): Dropout rate in decoder except attention & positional encoding. + transformer_dec_positional_dropout_rate (float, optional): Dropout rate after decoder positional encoding. + transformer_dec_attn_dropout_rate (float, optional): Dropout rate in deocoder self-attention module. + transformer_enc_dec_attn_dropout_rate (float, optional): Dropout rate in encoder-deocoder attention module. + init_type (str, optional): How to initialize transformer parameters. + init_enc_alpha (float, optional): Initial value of alpha in scaled pos encoding of the encoder. + init_dec_alpha (float, optional): Initial value of alpha in scaled pos encoding of the decoder. + eprenet_dropout_rate (float, optional): Dropout rate in encoder prenet. + dprenet_dropout_rate (float, optional): Dropout rate in decoder prenet. + postnet_dropout_rate (float, optional): Dropout rate in postnet. + use_masking (bool, optional): Whether to apply masking for padded part in loss calculation. + use_weighted_masking (bool, optional): Whether to apply weighted masking in loss calculation. + bce_pos_weight (float, optional): Positive sample weight in bce calculation (only for use_masking=true). + loss_type (str, optional): How to calculate loss. + use_guided_attn_loss (bool, optional): Whether to use guided attention loss. + num_heads_applied_guided_attn (int, optional): Number of heads in each layer to apply guided attention loss. + num_layers_applied_guided_attn (int, optional): Number of layers to apply guided attention loss. + List of module names to apply guided attention loss. + """ + + def __init__( + self, + # network structure related + idim: int, + odim: int, + embed_dim: int=512, + eprenet_conv_layers: int=3, + eprenet_conv_chans: int=256, + eprenet_conv_filts: int=5, + dprenet_layers: int=2, + dprenet_units: int=256, + elayers: int=6, + eunits: int=1024, + adim: int=512, + aheads: int=4, + dlayers: int=6, + dunits: int=1024, + postnet_layers: int=5, + postnet_chans: int=256, + postnet_filts: int=5, + positionwise_layer_type: str="conv1d", + positionwise_conv_kernel_size: int=1, + use_scaled_pos_enc: bool=True, + use_batch_norm: bool=True, + encoder_normalize_before: bool=True, + decoder_normalize_before: bool=True, + encoder_concat_after: bool=False, + decoder_concat_after: bool=False, + reduction_factor: int=1, + spk_embed_dim: int=None, + spk_embed_integration_type: str="add", + use_gst: bool=False, + gst_tokens: int=10, + gst_heads: int=4, + gst_conv_layers: int=6, + gst_conv_chans_list: Sequence[int]=(32, 32, 64, 64, 128, 128), + gst_conv_kernel_size: int=3, + gst_conv_stride: int=2, + gst_gru_layers: int=1, + gst_gru_units: int=128, + # training related + transformer_enc_dropout_rate: float=0.1, + transformer_enc_positional_dropout_rate: float=0.1, + transformer_enc_attn_dropout_rate: float=0.1, + transformer_dec_dropout_rate: float=0.1, + transformer_dec_positional_dropout_rate: float=0.1, + transformer_dec_attn_dropout_rate: float=0.1, + transformer_enc_dec_attn_dropout_rate: float=0.1, + eprenet_dropout_rate: float=0.5, + dprenet_dropout_rate: float=0.5, + postnet_dropout_rate: float=0.5, + init_type: str="xavier_uniform", + init_enc_alpha: float=1.0, + init_dec_alpha: float=1.0, + use_guided_attn_loss: bool=True, + num_heads_applied_guided_attn: int=2, + num_layers_applied_guided_attn: int=2, ): + """Initialize Transformer module.""" + assert check_argument_types() + super().__init__() + + # store hyperparameters + self.idim = idim + self.odim = odim + self.eos = idim - 1 + self.spk_embed_dim = spk_embed_dim + self.reduction_factor = reduction_factor + self.use_gst = use_gst + self.use_scaled_pos_enc = use_scaled_pos_enc + self.use_guided_attn_loss = use_guided_attn_loss + if self.use_guided_attn_loss: + if num_layers_applied_guided_attn == -1: + self.num_layers_applied_guided_attn = elayers + else: + self.num_layers_applied_guided_attn = num_layers_applied_guided_attn + if num_heads_applied_guided_attn == -1: + self.num_heads_applied_guided_attn = aheads + else: + self.num_heads_applied_guided_attn = num_heads_applied_guided_attn + if self.spk_embed_dim is not None: + self.spk_embed_integration_type = spk_embed_integration_type + + # use idx 0 as padding idx + self.padding_idx = 0 + # set_global_initializer 会影响后面的全局,包括 create_parameter + initialize(self, init_type) + + # get positional encoding layer type + transformer_pos_enc_layer_type = "scaled_abs_pos" if self.use_scaled_pos_enc else "abs_pos" + + # define transformer encoder + if eprenet_conv_layers != 0: + # encoder prenet + encoder_input_layer = nn.Sequential( + EncoderPrenet( + idim=idim, + embed_dim=embed_dim, + elayers=0, + econv_layers=eprenet_conv_layers, + econv_chans=eprenet_conv_chans, + econv_filts=eprenet_conv_filts, + use_batch_norm=use_batch_norm, + dropout_rate=eprenet_dropout_rate, + padding_idx=self.padding_idx, ), + nn.Linear(eprenet_conv_chans, adim), ) + else: + encoder_input_layer = nn.Embedding( + num_embeddings=idim, + embedding_dim=adim, + padding_idx=self.padding_idx) + self.encoder = TransformerEncoder( + idim=idim, + attention_dim=adim, + attention_heads=aheads, + linear_units=eunits, + num_blocks=elayers, + input_layer=encoder_input_layer, + dropout_rate=transformer_enc_dropout_rate, + positional_dropout_rate=transformer_enc_positional_dropout_rate, + attention_dropout_rate=transformer_enc_attn_dropout_rate, + pos_enc_layer_type=transformer_pos_enc_layer_type, + normalize_before=encoder_normalize_before, + concat_after=encoder_concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, ) + + # define GST + if self.use_gst: + self.gst = StyleEncoder( + idim=odim, # the input is mel-spectrogram + gst_tokens=gst_tokens, + gst_token_dim=adim, + gst_heads=gst_heads, + conv_layers=gst_conv_layers, + conv_chans_list=gst_conv_chans_list, + conv_kernel_size=gst_conv_kernel_size, + conv_stride=gst_conv_stride, + gru_layers=gst_gru_layers, + gru_units=gst_gru_units, ) + + # define projection layer + if self.spk_embed_dim is not None: + if self.spk_embed_integration_type == "add": + self.projection = nn.Linear(self.spk_embed_dim, adim) + else: + self.projection = nn.Linear(adim + self.spk_embed_dim, adim) + + # define transformer decoder + if dprenet_layers != 0: + # decoder prenet + decoder_input_layer = nn.Sequential( + DecoderPrenet( + idim=odim, + n_layers=dprenet_layers, + n_units=dprenet_units, + dropout_rate=dprenet_dropout_rate, ), + nn.Linear(dprenet_units, adim), ) + else: + decoder_input_layer = "linear" + # get positional encoding class + pos_enc_class = (ScaledPositionalEncoding + if self.use_scaled_pos_enc else PositionalEncoding) + self.decoder = Decoder( + odim=odim, # odim is needed when no prenet is used + attention_dim=adim, + attention_heads=aheads, + linear_units=dunits, + num_blocks=dlayers, + dropout_rate=transformer_dec_dropout_rate, + positional_dropout_rate=transformer_dec_positional_dropout_rate, + self_attention_dropout_rate=transformer_dec_attn_dropout_rate, + src_attention_dropout_rate=transformer_enc_dec_attn_dropout_rate, + input_layer=decoder_input_layer, + use_output_layer=False, + pos_enc_class=pos_enc_class, + normalize_before=decoder_normalize_before, + concat_after=decoder_concat_after, ) + + # define final projection + self.feat_out = nn.Linear(adim, odim * reduction_factor) + self.prob_out = nn.Linear(adim, reduction_factor) + + # define postnet + self.postnet = (None if postnet_layers == 0 else Postnet( + idim=idim, + odim=odim, + n_layers=postnet_layers, + n_chans=postnet_chans, + n_filts=postnet_filts, + use_batch_norm=use_batch_norm, + dropout_rate=postnet_dropout_rate, )) + + # 闭合的 initialize() 中的 set_global_initializer 的作用域,防止其影响到 self._reset_parameters() + nn.initializer.set_global_initializer(None) + + self._reset_parameters( + init_enc_alpha=init_enc_alpha, + init_dec_alpha=init_dec_alpha, ) + + def _reset_parameters(self, init_enc_alpha: float, init_dec_alpha: float): + + # initialize alpha in scaled positional encoding + if self.use_scaled_pos_enc: + init_enc_alpha = paddle.to_tensor(init_enc_alpha) + self.encoder.embed[-1].alpha = paddle.create_parameter( + shape=init_enc_alpha.shape, + dtype=str(init_enc_alpha.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign( + init_enc_alpha)) + + init_dec_alpha = paddle.to_tensor(init_dec_alpha) + self.decoder.embed[-1].alpha = paddle.create_parameter( + shape=init_dec_alpha.shape, + dtype=str(init_dec_alpha.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign( + init_dec_alpha)) + + def forward( + self, + text: paddle.Tensor, + text_lengths: paddle.Tensor, + speech: paddle.Tensor, + speech_lengths: paddle.Tensor, + spk_emb: paddle.Tensor=None, + ) -> Tuple[paddle.Tensor, Dict[str, paddle.Tensor], paddle.Tensor]: + """Calculate forward propagation. + + Args: + text(Tensor(int64)): Batch of padded character ids (B, Tmax). + text_lengths(Tensor(int64)): Batch of lengths of each input batch (B,). + speech(Tensor): Batch of padded target features (B, Lmax, odim). + speech_lengths(Tensor(int64)): Batch of the lengths of each target (B,). + spk_emb(Tensor, optional): Batch of speaker embeddings (B, spk_embed_dim). + + Returns: + Tensor: Loss scalar value. + Dict: Statistics to be monitored. + + """ + # input of embedding must be int64 + text_lengths = paddle.cast(text_lengths, 'int64') + + # Add eos at the last of sequence + text = numpy.pad(text.numpy(), ((0, 0), (0, 1)), 'constant') + xs = paddle.to_tensor(text, dtype='int64') + for i, l in enumerate(text_lengths): + xs[i, l] = self.eos + ilens = text_lengths + 1 + + ys = speech + olens = paddle.cast(speech_lengths, 'int64') + + # make labels for stop prediction + stop_labels = make_pad_mask(olens - 1) + # bool 类型无法切片 + stop_labels = paddle.cast(stop_labels, dtype='float32') + stop_labels = F.pad(stop_labels, [0, 0, 0, 1], "constant", 1.0) + + # calculate transformer outputs + after_outs, before_outs, logits = self._forward(xs, ilens, ys, olens, + spk_emb) + + # modifiy mod part of groundtruth + + if self.reduction_factor > 1: + olens = olens - olens % self.reduction_factor + max_olen = max(olens) + ys = ys[:, :max_olen] + stop_labels = stop_labels[:, :max_olen] + stop_labels[:, -1] = 1.0 # make sure at least one frame has 1 + olens_in = olens // self.reduction_factor + else: + olens_in = olens + + need_dict = {} + need_dict['encoder'] = self.encoder + need_dict['decoder'] = self.decoder + need_dict[ + 'num_heads_applied_guided_attn'] = self.num_heads_applied_guided_attn + need_dict[ + 'num_layers_applied_guided_attn'] = self.num_layers_applied_guided_attn + need_dict['use_scaled_pos_enc'] = self.use_scaled_pos_enc + + return after_outs, before_outs, logits, ys, stop_labels, olens, olens_in, need_dict + + def _forward( + self, + xs: paddle.Tensor, + ilens: paddle.Tensor, + ys: paddle.Tensor, + olens: paddle.Tensor, + spk_emb: paddle.Tensor, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + # forward encoder + x_masks = self._source_mask(ilens) + hs, h_masks = self.encoder(xs, x_masks) + + # integrate with GST + if self.use_gst: + style_embs = self.gst(ys) + hs = hs + style_embs.unsqueeze(1) + + # integrate speaker embedding + if self.spk_embed_dim is not None: + hs = self._integrate_with_spk_embed(hs, spk_emb) + + # thin out frames for reduction factor (B, Lmax, odim) -> (B, Lmax//r, odim) + if self.reduction_factor > 1: + ys_in = ys[:, self.reduction_factor - 1::self.reduction_factor] + olens_in = olens // self.reduction_factor + else: + ys_in, olens_in = ys, olens + + # add first zero frame and remove last frame for auto-regressive + ys_in = self._add_first_frame_and_remove_last_frame(ys_in) + + # forward decoder + y_masks = self._target_mask(olens_in) + zs, _ = self.decoder(ys_in, y_masks, hs, h_masks) + # (B, Lmax//r, odim * r) -> (B, Lmax//r * r, odim) + before_outs = self.feat_out(zs).reshape([zs.shape[0], -1, self.odim]) + # (B, Lmax//r, r) -> (B, Lmax//r * r) + logits = self.prob_out(zs).reshape([zs.shape[0], -1]) + + # postnet -> (B, Lmax//r * r, odim) + if self.postnet is None: + after_outs = before_outs + else: + after_outs = before_outs + self.postnet( + before_outs.transpose([0, 2, 1])).transpose([0, 2, 1]) + + return after_outs, before_outs, logits + + def inference( + self, + text: paddle.Tensor, + speech: paddle.Tensor=None, + spk_emb: paddle.Tensor=None, + threshold: float=0.5, + minlenratio: float=0.0, + maxlenratio: float=10.0, + use_teacher_forcing: bool=False, + ) -> Tuple[paddle.Tensor, paddle.Tensor, paddle.Tensor]: + """Generate the sequence of features given the sequences of characters. + + Args: + text(Tensor(int64)): Input sequence of characters (T,). + speech(Tensor, optional): Feature sequence to extract style (N, idim). + spk_emb(Tensor, optional): Speaker embedding vector (spk_embed_dim,). + threshold(float, optional): Threshold in inference. + minlenratio(float, optional): Minimum length ratio in inference. + maxlenratio(float, optional): Maximum length ratio in inference. + use_teacher_forcing(bool, optional): Whether to use teacher forcing. + + Returns: + Tensor: Output sequence of features (L, odim). + Tensor: Output sequence of stop probabilities (L,). + Tensor: Encoder-decoder (source) attention weights (#layers, #heads, L, T). + + """ + # input of embedding must be int64 + y = speech + + # add eos at the last of sequence + text = numpy.pad( + text.numpy(), (0, 1), 'constant', constant_values=self.eos) + x = paddle.to_tensor(text, dtype='int64') + + # inference with teacher forcing + if use_teacher_forcing: + assert speech is not None, "speech must be provided with teacher forcing." + + # get teacher forcing outputs + xs, ys = x.unsqueeze(0), y.unsqueeze(0) + spk_emb = None if spk_emb is None else spk_emb.unsqueeze(0) + ilens = paddle.to_tensor( + [xs.shape[1]], dtype=paddle.int64, place=xs.place) + olens = paddle.to_tensor( + [ys.shape[1]], dtype=paddle.int64, place=ys.place) + outs, *_ = self._forward(xs, ilens, ys, olens, spk_emb) + + # get attention weights + att_ws = [] + for i in range(len(self.decoder.decoders)): + att_ws += [self.decoder.decoders[i].src_attn.attn] + # (B, L, H, T_out, T_in) + att_ws = paddle.stack(att_ws, axis=1) + + return outs[0], None, att_ws[0] + + # forward encoder + xs = x.unsqueeze(0) + hs, _ = self.encoder(xs, None) + + # integrate GST + if self.use_gst: + style_embs = self.gst(y.unsqueeze(0)) + hs = hs + style_embs.unsqueeze(1) + + # integrate speaker embedding + if spk_emb is not None: + spk_emb = spk_emb.unsqueeze(0) + hs = self._integrate_with_spk_embed(hs, spk_emb) + + # set limits of length + maxlen = int(hs.shape[1] * maxlenratio / self.reduction_factor) + minlen = int(hs.shape[1] * minlenratio / self.reduction_factor) + + # initialize + idx = 0 + ys = paddle.zeros([1, 1, self.odim]) + outs, probs = [], [] + + # forward decoder step-by-step + z_cache = None + while True: + # update index + idx += 1 + + # calculate output and stop prob at idx-th step + y_masks = subsequent_mask(idx).unsqueeze(0) + z, z_cache = self.decoder.forward_one_step( + ys, y_masks, hs, cache=z_cache) # (B, adim) + outs += [ + self.feat_out(z).reshape([self.reduction_factor, self.odim]) + ] # [(r, odim), ...] + probs += [F.sigmoid(self.prob_out(z))[0]] # [(r), ...] + + # update next inputs + ys = paddle.concat( + (ys, outs[-1][-1].reshape([1, 1, self.odim])), + axis=1) # (1, idx + 1, odim) + + # get attention weights + att_ws_ = [] + for name, m in self.named_sublayers(): + if isinstance(m, MultiHeadedAttention) and "src" in name: + # [(#heads, 1, T),...] + att_ws_ += [m.attn[0, :, -1].unsqueeze(1)] + if idx == 1: + att_ws = att_ws_ + else: + # [(#heads, l, T), ...] + att_ws = [ + paddle.concat([att_w, att_w_], axis=1) + for att_w, att_w_ in zip(att_ws, att_ws_) + ] + + # check whether to finish generation + if sum(paddle.cast(probs[-1] >= threshold, + 'int64')) > 0 or idx >= maxlen: + # check mininum length + if idx < minlen: + continue + # (L, odim) -> (1, L, odim) -> (1, odim, L) + outs = (paddle.concat(outs, axis=0).unsqueeze(0).transpose( + [0, 2, 1])) + if self.postnet is not None: + # (1, odim, L) + outs = outs + self.postnet(outs) + # (L, odim) + outs = outs.transpose([0, 2, 1]).squeeze(0) + probs = paddle.concat(probs, axis=0) + break + + # concatenate attention weights -> (#layers, #heads, L, T) + att_ws = paddle.stack(att_ws, axis=0) + + return outs, probs, att_ws + + def _add_first_frame_and_remove_last_frame( + self, ys: paddle.Tensor) -> paddle.Tensor: + ys_in = paddle.concat( + [paddle.zeros((ys.shape[0], 1, ys.shape[2])), ys[:, :-1]], axis=1) + return ys_in + + def _source_mask(self, ilens: paddle.Tensor) -> paddle.Tensor: + """Make masks for self-attention. + + Args: + ilens(Tensor): Batch of lengths (B,). + + Returns: + Tensor: Mask tensor for self-attention. dtype=paddle.bool + + Examples: + >>> ilens = [5, 3] + >>> self._source_mask(ilens) + tensor([[[1, 1, 1, 1, 1], + [1, 1, 1, 0, 0]]]) bool + + """ + x_masks = make_non_pad_mask(ilens) + return x_masks.unsqueeze(-2) + + def _target_mask(self, olens: paddle.Tensor) -> paddle.Tensor: + """Make masks for masked self-attention. + + Args: + olens (Tensor(int64)): Batch of lengths (B,). + + Returns: + Tensor: Mask tensor for masked self-attention. + + Examples: + >>> olens = [5, 3] + >>> self._target_mask(olens) + tensor([[[1, 0, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 1, 0], + [1, 1, 1, 1, 1]], + [[1, 0, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0], + [1, 1, 1, 0, 0]]], dtype=paddle.uint8) + + """ + y_masks = make_non_pad_mask(olens) + s_masks = subsequent_mask(y_masks.shape[-1]).unsqueeze(0) + return paddle.logical_and(y_masks.unsqueeze(-2), s_masks) + + def _integrate_with_spk_embed(self, + hs: paddle.Tensor, + spk_emb: paddle.Tensor) -> paddle.Tensor: + """Integrate speaker embedding with hidden states. + + Args: + hs(Tensor): Batch of hidden state sequences (B, Tmax, adim). + spk_emb(Tensor): Batch of speaker embeddings (B, spk_embed_dim). + + Returns: + Tensor: Batch of integrated hidden state sequences (B, Tmax, adim). + + """ + if self.spk_embed_integration_type == "add": + # apply projection and then add to hidden states + spk_emb = self.projection(F.normalize(spk_emb)) + hs = hs + spk_emb.unsqueeze(1) + elif self.spk_embed_integration_type == "concat": + # concat hidden states with spk embeds and then apply projection + spk_emb = F.normalize(spk_emb).unsqueeze(1).expand(-1, hs.shape[1], + -1) + hs = self.projection(paddle.concat([hs, spk_emb], axis=-1)) + else: + raise NotImplementedError("support only add or concat.") + + return hs + + +class TransformerTTSInference(nn.Layer): + def __init__(self, normalizer, model): + super().__init__() + self.normalizer = normalizer + self.acoustic_model = model + + def forward(self, text, spk_id=None): + normalized_mel = self.acoustic_model.inference(text)[0] + logmel = self.normalizer.inverse(normalized_mel) + return logmel diff --git a/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts_updater.py b/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts_updater.py new file mode 100644 index 0000000..dff908e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/transformer_tts/transformer_tts_updater.py @@ -0,0 +1,333 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path +from typing import Sequence + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer + +from paddlespeech.t2s.modules.losses import GuidedMultiHeadAttentionLoss +from paddlespeech.t2s.modules.losses import Tacotron2Loss as TransformerTTSLoss +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class TransformerTTSUpdater(StandardUpdater): + def __init__( + self, + model: Layer, + optimizer: Optimizer, + dataloader: DataLoader, + init_state=None, + use_masking: bool=False, + use_weighted_masking: bool=False, + output_dir: Path=None, + bce_pos_weight: float=5.0, + loss_type: str="L1", + use_guided_attn_loss: bool=True, + modules_applied_guided_attn: Sequence[str]=("encoder-decoder"), + guided_attn_loss_sigma: float=0.4, + guided_attn_loss_lambda: float=1.0, ): + super().__init__(model, optimizer, dataloader, init_state=None) + + self.loss_type = loss_type + self.use_guided_attn_loss = use_guided_attn_loss + self.modules_applied_guided_attn = modules_applied_guided_attn + + self.criterion = TransformerTTSLoss( + use_masking=use_masking, + use_weighted_masking=use_weighted_masking, + bce_pos_weight=bce_pos_weight) + + if self.use_guided_attn_loss: + self.attn_criterion = GuidedMultiHeadAttentionLoss( + sigma=guided_attn_loss_sigma, + alpha=guided_attn_loss_lambda, ) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + + after_outs, before_outs, logits, ys, stop_labels, olens, olens_in, need_dict = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"], ) + + l1_loss, l2_loss, bce_loss = self.criterion( + after_outs=after_outs, + before_outs=before_outs, + logits=logits, + ys=ys, + stop_labels=stop_labels, + olens=olens) + + report("train/bce_loss", float(bce_loss)) + report("train/l1_loss", float(l1_loss)) + report("train/l2_loss", float(l2_loss)) + losses_dict["bce_loss"] = float(bce_loss) + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["l2_loss"] = float(l2_loss) + # caluculate loss values + if self.loss_type == "L1": + loss = l1_loss + bce_loss + elif self.loss_type == "L2": + loss = l2_loss + bce_loss + elif self.loss_type == "L1+L2": + loss = l1_loss + l2_loss + bce_loss + else: + raise ValueError("unknown --loss-type " + self.loss_type) + + # calculate guided attention loss + if self.use_guided_attn_loss: + # calculate for encoder + if "encoder" in self.modules_applied_guided_attn: + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['encoder'].encoders)))): + att_ws += [ + need_dict['encoder'].encoders[layer_idx].self_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_in, T_in) + att_ws = paddle.concat(att_ws, axis=1) + enc_attn_loss = self.attn_criterion( + att_ws=att_ws, + ilens=batch["text_lengths"] + 1, + olens=batch["text_lengths"] + 1) + loss = loss + enc_attn_loss + report("train/enc_attn_loss", float(enc_attn_loss)) + losses_dict["enc_attn_loss"] = float(enc_attn_loss) + # calculate for decoder + if "decoder" in self.modules_applied_guided_attn: + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['decoder'].decoders)))): + att_ws += [ + need_dict['decoder'].decoders[layer_idx].self_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_out, T_out) + att_ws = paddle.concat(att_ws, axis=1) + dec_attn_loss = self.attn_criterion( + att_ws=att_ws, ilens=olens_in, olens=olens_in) + report("train/dec_attn_loss", float(dec_attn_loss)) + losses_dict["dec_attn_loss"] = float(dec_attn_loss) + loss = loss + dec_attn_loss + # calculate for encoder-decoder + if "encoder-decoder" in self.modules_applied_guided_attn: + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['decoder'].decoders)))): + att_ws += [ + need_dict['decoder'].decoders[layer_idx].src_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_out, T_in) + att_ws = paddle.concat(att_ws, axis=1) + enc_dec_attn_loss = self.attn_criterion( + att_ws=att_ws, + ilens=batch["text_lengths"] + 1, + olens=olens_in) + report("train/enc_dec_attn_loss", float(enc_dec_attn_loss)) + losses_dict["enc_dec_attn_loss"] = float(enc_dec_attn_loss) + loss = loss + enc_dec_attn_loss + if need_dict['use_scaled_pos_enc']: + report("train/encoder_alpha", + float(need_dict['encoder'].embed[-1].alpha)) + report("train/decoder_alpha", + float(need_dict['decoder'].embed[-1].alpha)) + losses_dict["encoder_alpha"] = float( + need_dict['encoder'].embed[-1].alpha) + losses_dict["decoder_alpha"] = float( + need_dict['decoder'].embed[-1].alpha) + + optimizer = self.optimizer + optimizer.clear_grad() + loss.backward() + optimizer.step() + + report("train/loss", float(loss)) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class TransformerTTSEvaluator(StandardEvaluator): + def __init__( + self, + model: Layer, + dataloader: DataLoader, + init_state=None, + use_masking: bool=False, + use_weighted_masking: bool=False, + output_dir: Path=None, + bce_pos_weight: float=5.0, + loss_type: str="L1", + use_guided_attn_loss: bool=True, + modules_applied_guided_attn: Sequence[str]=("encoder-decoder"), + guided_attn_loss_sigma: float=0.4, + guided_attn_loss_lambda: float=1.0, ): + super().__init__(model, dataloader) + + self.loss_type = loss_type + self.use_guided_attn_loss = use_guided_attn_loss + self.modules_applied_guided_attn = modules_applied_guided_attn + + self.criterion = TransformerTTSLoss( + use_masking=use_masking, + use_weighted_masking=use_weighted_masking, + bce_pos_weight=bce_pos_weight) + + if self.use_guided_attn_loss: + self.attn_criterion = GuidedMultiHeadAttentionLoss( + sigma=guided_attn_loss_sigma, + alpha=guided_attn_loss_lambda, ) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + after_outs, before_outs, logits, ys, stop_labels, olens, olens_in, need_dict = self.model( + text=batch["text"], + text_lengths=batch["text_lengths"], + speech=batch["speech"], + speech_lengths=batch["speech_lengths"]) + + l1_loss, l2_loss, bce_loss = self.criterion( + after_outs=after_outs, + before_outs=before_outs, + logits=logits, + ys=ys, + stop_labels=stop_labels, + olens=olens) + + report("eval/bce_loss", float(bce_loss)) + report("eval/l1_loss", float(l1_loss)) + report("eval/l2_loss", float(l2_loss)) + losses_dict["bce_loss"] = float(bce_loss) + losses_dict["l1_loss"] = float(l1_loss) + losses_dict["l2_loss"] = float(l2_loss) + # caluculate loss values + if self.loss_type == "L1": + loss = l1_loss + bce_loss + elif self.loss_type == "L2": + loss = l2_loss + bce_loss + elif self.loss_type == "L1+L2": + loss = l1_loss + l2_loss + bce_loss + else: + raise ValueError("unknown --loss-type " + self.loss_type) + + # calculate guided attention loss + if self.use_guided_attn_loss: + # calculate for encoder + if "encoder" in self.modules_applied_guided_attn: + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['encoder'].encoders)))): + att_ws += [ + need_dict['encoder'].encoders[layer_idx].self_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_in, T_in) + att_ws = paddle.concat(att_ws, axis=1) + enc_attn_loss = self.attn_criterion( + att_ws=att_ws, + ilens=batch["text_lengths"] + 1, + olens=batch["text_lengths"] + 1) + loss = loss + enc_attn_loss + report("train/enc_attn_loss", float(enc_attn_loss)) + losses_dict["enc_attn_loss"] = float(enc_attn_loss) + # calculate for decoder + if "decoder" in self.modules_applied_guided_attn: + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['decoder'].decoders)))): + att_ws += [ + need_dict['decoder'].decoders[layer_idx].self_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_out, T_out) + att_ws = paddle.concat(att_ws, axis=1) + dec_attn_loss = self.attn_criterion( + att_ws=att_ws, ilens=olens_in, olens=olens_in) + report("eval/dec_attn_loss", float(dec_attn_loss)) + losses_dict["dec_attn_loss"] = float(dec_attn_loss) + loss = loss + dec_attn_loss + # calculate for encoder-decoder + if "encoder-decoder" in self.modules_applied_guided_attn: + + att_ws = [] + for idx, layer_idx in enumerate( + reversed(range(len(need_dict['decoder'].decoders)))): + att_ws += [ + need_dict['decoder'].decoders[layer_idx].src_attn. + attn[:, :need_dict['num_heads_applied_guided_attn']] + ] + if idx + 1 == need_dict['num_layers_applied_guided_attn']: + break + # (B, H*L, T_out, T_in) + att_ws = paddle.concat(att_ws, axis=1) + enc_dec_attn_loss = self.attn_criterion( + att_ws=att_ws, + ilens=batch["text_lengths"] + 1, + olens=olens_in) + report("eval/enc_dec_attn_loss", float(enc_dec_attn_loss)) + losses_dict["enc_dec_attn_loss"] = float(enc_dec_attn_loss) + loss = loss + enc_dec_attn_loss + if need_dict['use_scaled_pos_enc']: + report("eval/encoder_alpha", + float(need_dict['encoder'].embed[-1].alpha)) + report("eval/decoder_alpha", + float(need_dict['decoder'].embed[-1].alpha)) + losses_dict["encoder_alpha"] = float( + need_dict['encoder'].embed[-1].alpha) + losses_dict["decoder_alpha"] = float( + need_dict['decoder'].embed[-1].alpha) + report("eval/loss", float(loss)) + losses_dict["loss"] = float(loss) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/t2s/models/waveflow.py b/ernie-sat/paddlespeech/t2s/models/waveflow.py new file mode 100644 index 0000000..52e6005 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/waveflow.py @@ -0,0 +1,736 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math +import time +from typing import List +from typing import Tuple +from typing import Union + +import numpy as np +import paddle +from paddle import nn +from paddle.nn import functional as F +from paddle.nn import initializer as I + +from paddlespeech.t2s.modules import geometry as geo +from paddlespeech.t2s.utils import checkpoint + +__all__ = ["WaveFlow", "ConditionalWaveFlow", "WaveFlowLoss"] + + +def fold(x, n_group): + """Fold audio or spectrogram's temporal dimension in to groups. + + Args: + x(Tensor): The input tensor. shape=(*, time_steps) + n_group(int): The size of a group. + + Returns: + Tensor: Folded tensor. shape=(*, time_steps // n_group, group) + """ + spatial_shape = list(x.shape[:-1]) + time_steps = paddle.shape(x)[-1] + new_shape = spatial_shape + [time_steps // n_group, n_group] + return paddle.reshape(x, new_shape) + + +class UpsampleNet(nn.LayerList): + """Layer to upsample mel spectrogram to the same temporal resolution with + the corresponding waveform. + + It consists of several conv2dtranspose layers which perform deconvolution + on mel and time dimension. + + Args: + upscale_factors(List[int], optional): Time upsampling factors for each Conv2DTranspose Layer. + The ``UpsampleNet`` contains ``len(upscale_factor)`` Conv2DTranspose + Layers. Each upscale_factor is used as the ``stride`` for the + corresponding Conv2DTranspose. Defaults to [16, 16], this the default + upsampling factor is 256. + + Notes: + ``np.prod(upscale_factors)`` should equals the ``hop_length`` of the stft + transformation used to extract spectrogram features from audio. + + For example, ``16 * 16 = 256``, then the spectrogram extracted with a stft + transformation whose ``hop_length`` equals 256 is suitable. + + See Also + + ``librosa.core.stft`` + """ + + def __init__(self, upsample_factors): + super().__init__() + for factor in upsample_factors: + std = math.sqrt(1 / (3 * 2 * factor)) + init = I.Uniform(-std, std) + self.append( + nn.utils.weight_norm( + nn.Conv2DTranspose( + 1, + 1, (3, 2 * factor), + padding=(1, factor // 2), + stride=(1, factor), + weight_attr=init, + bias_attr=init))) + + # upsample factors + self.upsample_factor = np.prod(upsample_factors) + self.upsample_factors = upsample_factors + + def forward(self, x, trim_conv_artifact=False): + """Forward pass of the ``UpsampleNet`` + + Args: + x(Tensor): The input spectrogram. shape=(batch_size, input_channels, time_steps) + trim_conv_artifact(bool, optional, optional): Trim deconvolution artifact at each layer. Defaults to False. + + Returns: + Tensor: The upsampled spectrogram. shape=(batch_size, input_channels, time_steps * upsample_factor) + + Notes: + If trim_conv_artifact is ``True``, the output time steps is less + than ``time_steps * upsample_factors``. + """ + x = paddle.unsqueeze(x, 1) # (B, C, T) -> (B, 1, C, T) + for layer in self: + x = layer(x) + if trim_conv_artifact: + time_cutoff = layer._kernel_size[1] - layer._stride[1] + x = x[:, :, :, :-time_cutoff] + x = F.leaky_relu(x, 0.4) + x = paddle.squeeze(x, 1) # back to (B, C, T) + return x + + +class ResidualBlock(nn.Layer): + """ResidualBlock, the basic unit of ResidualNet used in WaveFlow. + + It has a conv2d layer, which has causal padding in height dimension and + same paddign in width dimension. It also has projection for the condition + and output. + + Args: + channels (int): Feature size of the input. + cond_channels (int): Featuer size of the condition. + kernel_size (Tuple[int]): Kernel size of the Convolution2d applied to the input. + dilations (int): Dilations of the Convolution2d applied to the input. + """ + + def __init__(self, channels, cond_channels, kernel_size, dilations): + super().__init__() + # input conv + std = math.sqrt(1 / channels * np.prod(kernel_size)) + init = I.Uniform(-std, std) + receptive_field = [ + 1 + (k - 1) * d for (k, d) in zip(kernel_size, dilations) + ] + rh, rw = receptive_field + paddings = [rh - 1, 0, rw // 2, (rw - 1) // 2] # causal & same + conv = nn.Conv2D( + channels, + 2 * channels, + kernel_size, + padding=paddings, + dilation=dilations, + weight_attr=init, + bias_attr=init) + self.conv = nn.utils.weight_norm(conv) + self.rh = rh + self.rw = rw + self.dilations = dilations + + # condition projection + std = math.sqrt(1 / cond_channels) + init = I.Uniform(-std, std) + condition_proj = nn.Conv2D( + cond_channels, + 2 * channels, (1, 1), + weight_attr=init, + bias_attr=init) + self.condition_proj = nn.utils.weight_norm(condition_proj) + + # parametric residual & skip connection + std = math.sqrt(1 / channels) + init = I.Uniform(-std, std) + out_proj = nn.Conv2D( + channels, 2 * channels, (1, 1), weight_attr=init, bias_attr=init) + self.out_proj = nn.utils.weight_norm(out_proj) + + def forward(self, x, condition): + """Compute output for a whole folded sequence. + + Args: + x (Tensor): The input. [shape=(batch_size, channel, height, width)] + condition (Tensor [shape=(batch_size, condition_channel, height, width)]): The local condition. + + Returns: + res (Tensor): The residual output. [shape=(batch_size, channel, height, width)] + skip (Tensor): The skip output. [shape=(batch_size, channel, height, width)] + """ + x_in = x + x = self.conv(x) + x += self.condition_proj(condition) + + content, gate = paddle.chunk(x, 2, axis=1) + x = paddle.tanh(content) * F.sigmoid(gate) + + x = self.out_proj(x) + res, skip = paddle.chunk(x, 2, axis=1) + res = x_in + res + return res, skip + + def start_sequence(self): + """Prepare the layer for incremental computation of causal + convolution. Reset the buffer for causal convolution. + + Raises: + ValueError: If not in evaluation mode. + """ + if self.training: + raise ValueError("Only use start sequence at evaluation mode.") + self._conv_buffer = paddle.zeros([1]) + + # NOTE: call self.conv's weight norm hook expliccitly since + # its weight will be visited directly in `add_input` without + # calling its `__call__` method. If we do not trigger the weight + # norm hook, the weight may be outdated. e.g. after loading from + # a saved checkpoint + # see also: https://github.com/pytorch/pytorch/issues/47588 + for hook in self.conv._forward_pre_hooks.values(): + hook(self.conv, None) + + def add_input(self, x_row, condition_row): + """Compute the output for a row and update the buffer. + + Args: + x_row (Tensor): A row of the input. shape=(batch_size, channel, 1, width) + condition_row (Tensor): A row of the condition. shape=(batch_size, condition_channel, 1, width) + + Returns: + res (Tensor): A row of the the residual output. shape=(batch_size, channel, 1, width) + skip (Tensor): A row of the skip output. shape=(batch_size, channel, 1, width) + + """ + x_row_in = x_row + if len(paddle.shape(self._conv_buffer)) == 1: + self._init_buffer(x_row) + self._update_buffer(x_row) + rw = self.rw + x_row = F.conv2d( + self._conv_buffer, + self.conv.weight, + self.conv.bias, + padding=[0, 0, rw // 2, (rw - 1) // 2], + dilation=self.dilations) + x_row += self.condition_proj(condition_row) + content, gate = paddle.chunk(x_row, 2, axis=1) + x_row = paddle.tanh(content) * F.sigmoid(gate) + + x_row = self.out_proj(x_row) + res, skip = paddle.chunk(x_row, 2, axis=1) + res = x_row_in + res + return res, skip + + def _init_buffer(self, input): + batch_size, channels, _, width = input.shape + self._conv_buffer = paddle.zeros( + [batch_size, channels, self.rh, width], dtype=input.dtype) + + def _update_buffer(self, input): + self._conv_buffer = paddle.concat( + [self._conv_buffer[:, :, 1:, :], input], axis=2) + + +class ResidualNet(nn.LayerList): + """A stack of several ResidualBlocks. It merges condition at each layer. + + Args: + n_layer (int): Number of ResidualBlocks in the ResidualNet. + residual_channels (int): Feature size of each ResidualBlocks. + condition_channels (int): Feature size of the condition. + kernel_size (Tuple[int]): Kernel size of each ResidualBlock. + dilations_h (List[int]): Dilation in height dimension of every ResidualBlock. + + Raises: + ValueError: If the length of dilations_h does not equals n_layers. + """ + + def __init__(self, + n_layer: int, + residual_channels: int, + condition_channels: int, + kernel_size: Tuple[int], + dilations_h: List[int]): + if len(dilations_h) != n_layer: + raise ValueError( + "number of dilations_h should equals num of layers") + super().__init__() + for i in range(n_layer): + dilation = (dilations_h[i], 2**i) + layer = ResidualBlock(residual_channels, condition_channels, + kernel_size, dilation) + self.append(layer) + + def forward(self, x, condition): + """Comput the output of given the input and the condition. + + Args: + x (Tensor): The input. shape=(batch_size, channel, height, width) + condition (Tensor): The local condition. shape=(batch_size, condition_channel, height, width) + + Returns: + Tensor : The output, which is an aggregation of all the skip outputs. shape=(batch_size, channel, height, width) + + """ + skip_connections = [] + for layer in self: + x, skip = layer(x, condition) + skip_connections.append(skip) + out = paddle.sum(paddle.stack(skip_connections, 0), 0) + return out + + def start_sequence(self): + """Prepare the layer for incremental computation. + """ + for layer in self: + layer.start_sequence() + + def add_input(self, x_row, condition_row): + """Compute the output for a row and update the buffers. + + Args: + x_row (Tensor): A row of the input. shape=(batch_size, channel, 1, width) + condition_row (Tensor): A row of the condition. shape=(batch_size, condition_channel, 1, width) + + Returns: + res (Tensor): A row of the the residual output. shape=(batch_size, channel, 1, width) + skip (Tensor): A row of the skip output. shape=(batch_size, channel, 1, width) + + """ + skip_connections = [] + for layer in self: + x_row, skip = layer.add_input(x_row, condition_row) + skip_connections.append(skip) + out = paddle.sum(paddle.stack(skip_connections, 0), 0) + return out + + +class Flow(nn.Layer): + """A bijection (Reversable layer) that transform a density of latent + variables p(Z) into a complex data distribution p(X). + + It's an auto regressive flow. The ``forward`` method implements the + probability density estimation. The ``inverse`` method implements the + sampling. + + Args: + n_layers (int): Number of ResidualBlocks in the Flow. + channels (int): Feature size of the ResidualBlocks. + mel_bands (int): Feature size of the mel spectrogram (mel bands). + kernel_size (Tuple[int]): Kernel size of each ResisualBlocks in the Flow. + n_group (int): Number of timesteps to the folded into a group. + """ + dilations_dict = { + 8: [1, 1, 1, 1, 1, 1, 1, 1], + 16: [1, 1, 1, 1, 1, 1, 1, 1], + 32: [1, 2, 4, 1, 2, 4, 1, 2], + 64: [1, 2, 4, 8, 16, 1, 2, 4], + 128: [1, 2, 4, 8, 16, 32, 64, 1] + } + + def __init__(self, n_layers, channels, mel_bands, kernel_size, n_group): + super().__init__() + # input projection + self.input_proj = nn.utils.weight_norm( + nn.Conv2D( + 1, + channels, (1, 1), + weight_attr=I.Uniform(-1., 1.), + bias_attr=I.Uniform(-1., 1.))) + + # residual net + self.resnet = ResidualNet(n_layers, channels, mel_bands, kernel_size, + self.dilations_dict[n_group]) + + # output projection + self.output_proj = nn.Conv2D( + channels, + 2, (1, 1), + weight_attr=I.Constant(0.), + bias_attr=I.Constant(0.)) + + # specs + self.n_group = n_group + + def _predict_parameters(self, x, condition): + x = self.input_proj(x) + x = self.resnet(x, condition) + bijection_params = self.output_proj(x) + logs, b = paddle.chunk(bijection_params, 2, axis=1) + return logs, b + + def _transform(self, x, logs, b): + z_0 = x[:, :, :1, :] # the first row, just copy it + z_out = x[:, :, 1:, :] * paddle.exp(logs) + b + z_out = paddle.concat([z_0, z_out], axis=2) + return z_out + + def forward(self, x, condition): + """Probability density estimation. It is done by inversely transform + a sample from p(X) into a sample from p(Z). + + Args: + x (Tensor): A input sample of the distribution p(X). shape=(batch, 1, height, width) + condition (Tensor): The local condition. shape=(batch, condition_channel, height, width) + + Returns: + z (Tensor): shape(batch, 1, height, width), the transformed sample. + Tuple[Tensor, Tensor]: + The parameter of the transformation. + logs (Tensor): shape(batch, 1, height - 1, width), the log scale of the transformation from x to z. + b (Tensor): shape(batch, 1, height - 1, width), the shift of the transformation from x to z. + """ + # (B, C, H-1, W) + logs, b = self._predict_parameters(x[:, :, :-1, :], + condition[:, :, 1:, :]) + z = self._transform(x, logs, b) + return z, (logs, b) + + def _predict_row_parameters(self, x_row, condition_row): + x_row = self.input_proj(x_row) + x_row = self.resnet.add_input(x_row, condition_row) + bijection_params = self.output_proj(x_row) + logs, b = paddle.chunk(bijection_params, 2, axis=1) + return logs, b + + def _inverse_transform_row(self, z_row, logs, b): + x_row = (z_row - b) * paddle.exp(-logs) + return x_row + + def _inverse_row(self, z_row, x_row, condition_row): + logs, b = self._predict_row_parameters(x_row, condition_row) + x_next_row = self._inverse_transform_row(z_row, logs, b) + return x_next_row, (logs, b) + + def _start_sequence(self): + self.resnet.start_sequence() + + def inverse(self, z, condition): + """Sampling from the the distrition p(X). It is done by sample form + p(Z) and transform the sample. It is a auto regressive transformation. + + Args: + z(Tensor): A sample of the distribution p(Z). shape=(batch, 1, time_steps + condition(Tensor): The local condition. shape=(batch, condition_channel, time_steps) + Returns: + Tensor: + The transformed sample. shape=(batch, 1, height, width) + """ + z_0 = z[:, :, :1, :] + x = paddle.zeros_like(z) + x[:, :, :1, :] = z_0 + + self._start_sequence() + + num_step = paddle.ones([1], dtype='int32') * (self.n_group) + for i in range(1, num_step): + x_row = x[:, :, i - 1:i, :] + z_row = z[:, :, i:i + 1, :] + condition_row = condition[:, :, i:i + 1, :] + x_next_row, (logs, b) = self._inverse_row(z_row, x_row, + condition_row) + x[:, :, i:i + 1, :] = x_next_row + + return x + + +class WaveFlow(nn.LayerList): + """An Deep Reversible layer that is composed of severel auto regressive + flows. + + Args: + n_flows (int): Number of flows in the WaveFlow model. + n_layers (int): Number of ResidualBlocks in each Flow. + n_group (int): Number of timesteps to fold as a group. + channels (int): Feature size of each ResidualBlock. + mel_bands (int): Feature size of mel spectrogram (mel bands). + kernel_size (Union[int, List[int]]): Kernel size of the convolution layer in each ResidualBlock. + """ + + def __init__(self, n_flows, n_layers, n_group, channels, mel_bands, + kernel_size): + if n_group % 2 or n_flows % 2: + raise ValueError( + "number of flows and number of group must be even " + "since a permutation along group among flows is used.") + super().__init__() + for _ in range(n_flows): + self.append( + Flow(n_layers, channels, mel_bands, kernel_size, n_group)) + + # permutations in h + self.perms = self._create_perm(n_group, n_flows) + + # specs + self.n_group = n_group + self.n_flows = n_flows + + def _create_perm(self, n_group, n_flows): + indices = list(range(n_group)) + half = n_group // 2 + perms = [] + for i in range(n_flows): + if i < n_flows // 2: + perm = indices[::-1] + else: + perm = list(reversed(indices[:half])) + list( + reversed(indices[half:])) + perm = paddle.to_tensor(perm) + self.register_buffer(perm.name, perm) + perms.append(perm) + return perms + + def _trim(self, x, condition): + assert condition.shape[-1] >= x.shape[-1] + pruned_len = int(paddle.shape(x)[-1] // self.n_group * self.n_group) + + if x.shape[-1] > pruned_len: + x = x[:, :pruned_len] + if condition.shape[-1] > pruned_len: + condition = condition[:, :, :pruned_len] + return x, condition + + def forward(self, x, condition): + """Probability density estimation of random variable x given the + condition. + + Args: + x (Tensor): The audio. shape=(batch_size, time_steps) + condition (Tensor): The local condition (mel spectrogram here). shape=(batch_size, condition channel, time_steps) + + Returns: + Tensor: The transformed random variable. shape=(batch_size, time_steps) + Tensor: The log determinant of the jacobian of the transformation from x to z. shape=(1,) + """ + # x: (B, T) + # condition: (B, C, T) upsampled condition + x, condition = self._trim(x, condition) + + # to (B, C, h, T//h) layout + x = paddle.unsqueeze( + paddle.transpose(fold(x, self.n_group), [0, 2, 1]), 1) + condition = paddle.transpose( + fold(condition, self.n_group), [0, 1, 3, 2]) + + # flows + logs_list = [] + for i, layer in enumerate(self): + x, (logs, b) = layer(x, condition) + logs_list.append(logs) + # permute paddle has no shuffle dim + x = geo.shuffle_dim(x, 2, perm=self.perms[i]) + condition = geo.shuffle_dim(condition, 2, perm=self.perms[i]) + + z = paddle.squeeze(x, 1) # (B, H, W) + batch_size = z.shape[0] + z = paddle.reshape(paddle.transpose(z, [0, 2, 1]), [batch_size, -1]) + + log_det_jacobian = paddle.sum(paddle.stack(logs_list)) + return z, log_det_jacobian + + def inverse(self, z, condition): + """Sampling from the the distrition p(X). + + It is done by sample a ``z`` form p(Z) and transform it into ``x``. + Each Flow transform .. math:: `z_{i-1}` to .. math:: `z_{i}` in an + autoregressive manner. + + Args: + z (Tensor): A sample of the distribution p(Z). shape=(batch, 1, time_steps + condition (Tensor): The local condition. shape=(batch, condition_channel, time_steps) + + Returns: + Tensor: The transformed sample (audio here). shape=(batch_size, time_steps) + + """ + + z, condition = self._trim(z, condition) + # to (B, C, h, T//h) layout + z = paddle.unsqueeze( + paddle.transpose(fold(z, self.n_group), [0, 2, 1]), 1) + condition = paddle.transpose( + fold(condition, self.n_group), [0, 1, 3, 2]) + + # reverse it flow by flow + for i in reversed(range(self.n_flows)): + z = geo.shuffle_dim(z, 2, perm=self.perms[i]) + condition = geo.shuffle_dim(condition, 2, perm=self.perms[i]) + z = self[i].inverse(z, condition) + + x = paddle.squeeze(z, 1) # (B, H, W) + batch_size = x.shape[0] + x = paddle.reshape(paddle.transpose(x, [0, 2, 1]), [batch_size, -1]) + return x + + +class ConditionalWaveFlow(nn.LayerList): + """ConditionalWaveFlow, a UpsampleNet with a WaveFlow model. + + Args: + upsample_factors (List[int]): Upsample factors for the upsample net. + n_flows (int): Number of flows in the WaveFlow model. + n_layers (int): Number of ResidualBlocks in each Flow. + n_group (int): Number of timesteps to fold as a group. + channels (int): Feature size of each ResidualBlock. + n_mels (int): Feature size of mel spectrogram (mel bands). + kernel_size (Union[int, List[int]]): Kernel size of the convolution layer in each ResidualBlock. + """ + + def __init__(self, + upsample_factors: List[int], + n_flows: int, + n_layers: int, + n_group: int, + channels: int, + n_mels: int, + kernel_size: Union[int, List[int]]): + super().__init__() + self.encoder = UpsampleNet(upsample_factors) + self.decoder = WaveFlow( + n_flows=n_flows, + n_layers=n_layers, + n_group=n_group, + channels=channels, + mel_bands=n_mels, + kernel_size=kernel_size) + + def forward(self, audio, mel): + """Compute the transformed random variable z (x to z) and the log of + the determinant of the jacobian of the transformation from x to z. + + Args: + audio(Tensor): The audio. shape=(B, T) + mel(Tensor): The mel spectrogram. shape=(B, C_mel, T_mel) + + Returns: + Tensor: The inversely transformed random variable z (x to z). shape=(B, T) + Tensor: the log of the determinant of the jacobian of the transformation from x to z. shape=(1,) + """ + condition = self.encoder(mel) + z, log_det_jacobian = self.decoder(audio, condition) + return z, log_det_jacobian + + @paddle.no_grad() + def infer(self, mel): + """Generate raw audio given mel spectrogram. + + Args: + mel(np.ndarray): Mel spectrogram of an utterance(in log-magnitude). shape=(C_mel, T_mel) + + Returns: + Tensor: The synthesized audio, where``T <= T_mel * upsample_factors``. shape=(B, T) + """ + start = time.time() + condition = self.encoder(mel, trim_conv_artifact=True) # (B, C, T) + batch_size, _, time_steps = condition.shape + z = paddle.randn([batch_size, time_steps], dtype=mel.dtype) + x = self.decoder.inverse(z, condition) + end = time.time() + print("time: {}s".format(end - start)) + return x + + @paddle.no_grad() + def predict(self, mel): + """Generate raw audio given mel spectrogram. + + Args: + mel(np.ndarray): Mel spectrogram of an utterance(in log-magnitude). shape=(C_mel, T_mel) + + Returns: + np.ndarray: The synthesized audio. shape=(T,) + """ + mel = paddle.to_tensor(mel) + mel = paddle.unsqueeze(mel, 0) + audio = self.infer(mel) + audio = audio[0].numpy() + return audio + + @classmethod + def from_pretrained(cls, config, checkpoint_path): + """Build a ConditionalWaveFlow model from a pretrained model. + + Args: + config(yacs.config.CfgNode): model configs + checkpoint_path(Path or str): the path of pretrained model checkpoint, without extension name + + Returns: + ConditionalWaveFlow The model built from pretrained result. + """ + model = cls(upsample_factors=config.model.upsample_factors, + n_flows=config.model.n_flows, + n_layers=config.model.n_layers, + n_group=config.model.n_group, + channels=config.model.channels, + n_mels=config.data.n_mels, + kernel_size=config.model.kernel_size) + checkpoint.load_parameters(model, checkpoint_path=checkpoint_path) + return model + + +class WaveFlowLoss(nn.Layer): + """Criterion of a WaveFlow model. + + Args: + sigma (float): The standard deviation of the gaussian noise used in WaveFlow, + by default 1.0. + """ + + def __init__(self, sigma=1.0): + super().__init__() + self.sigma = sigma + self.const = 0.5 * np.log(2 * np.pi) + np.log(self.sigma) + + def forward(self, z, log_det_jacobian): + """Compute the loss given the transformed random variable z and the + log_det_jacobian of transformation from x to z. + + Args: + z(Tensor): The transformed random variable (x to z). shape=(B, T) + log_det_jacobian(Tensor): The log of the determinant of the jacobian matrix of the + transformation from x to z. shape=(1,) + + Returns: + Tensor: The loss. shape=(1,) + """ + loss = paddle.sum(z * z) / (2 * self.sigma * self.sigma + ) - log_det_jacobian + loss = loss / np.prod(z.shape) + return loss + self.const + + +class ConditionalWaveFlow2Infer(ConditionalWaveFlow): + def forward(self, mel): + """Generate raw audio given mel spectrogram. + + Args: + mel (np.ndarray): Mel spectrogram of an utterance(in log-magnitude). shape=(C_mel, T_mel) + + Returns: + np.ndarray: The synthesized audio. shape=(T,) + + """ + audio = self.predict(mel) + return audio diff --git a/ernie-sat/paddlespeech/t2s/models/wavernn/__init__.py b/ernie-sat/paddlespeech/t2s/models/wavernn/__init__.py new file mode 100644 index 0000000..80ffd06 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/wavernn/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .wavernn import * +from .wavernn_updater import * diff --git a/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn.py b/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn.py new file mode 100644 index 0000000..b4b8b48 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn.py @@ -0,0 +1,582 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from https://github.com/fatchord/WaveRNN +import sys +import time +from typing import List + +import numpy as np +import paddle +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.t2s.audio.codec import decode_mu_law +from paddlespeech.t2s.modules.losses import sample_from_discretized_mix_logistic +from paddlespeech.t2s.modules.nets_utils import initialize +from paddlespeech.t2s.modules.upsample import Stretch2D + + +class ResBlock(nn.Layer): + def __init__(self, dims): + super().__init__() + self.conv1 = nn.Conv1D(dims, dims, kernel_size=1, bias_attr=False) + self.conv2 = nn.Conv1D(dims, dims, kernel_size=1, bias_attr=False) + self.batch_norm1 = nn.BatchNorm1D(dims) + self.batch_norm2 = nn.BatchNorm1D(dims) + + def forward(self, x): + ''' + conv -> bn -> relu -> conv -> bn + residual connection + ''' + residual = x + x = self.conv1(x) + x = self.batch_norm1(x) + x = F.relu(x) + x = self.conv2(x) + x = self.batch_norm2(x) + return x + residual + + +class MelResNet(nn.Layer): + def __init__(self, + res_blocks: int=10, + compute_dims: int=128, + res_out_dims: int=128, + aux_channels: int=80, + aux_context_window: int=0): + super().__init__() + k_size = aux_context_window * 2 + 1 + # pay attention here, the dim reduces aux_context_window * 2 + self.conv_in = nn.Conv1D( + aux_channels, compute_dims, kernel_size=k_size, bias_attr=False) + self.batch_norm = nn.BatchNorm1D(compute_dims) + self.layers = nn.LayerList() + for _ in range(res_blocks): + self.layers.append(ResBlock(compute_dims)) + self.conv_out = nn.Conv1D(compute_dims, res_out_dims, kernel_size=1) + + def forward(self, x): + ''' + Args: + x (Tensor): Input tensor (B, in_dims, T). + Returns: + Tensor: Output tensor (B, res_out_dims, T). + ''' + + x = self.conv_in(x) + x = self.batch_norm(x) + x = F.relu(x) + for f in self.layers: + x = f(x) + x = self.conv_out(x) + return x + + +class UpsampleNetwork(nn.Layer): + def __init__(self, + aux_channels: int=80, + upsample_scales: List[int]=[4, 5, 3, 5], + compute_dims: int=128, + res_blocks: int=10, + res_out_dims: int=128, + aux_context_window: int=2): + super().__init__() + # total_scale is the total Up sampling multiple + total_scale = np.prod(upsample_scales) + # TODO pad*total_scale is numpy.int64 + self.indent = int(aux_context_window * total_scale) + self.resnet = MelResNet( + res_blocks=res_blocks, + aux_channels=aux_channels, + compute_dims=compute_dims, + res_out_dims=res_out_dims, + aux_context_window=aux_context_window) + self.resnet_stretch = Stretch2D(total_scale, 1) + self.up_layers = nn.LayerList() + for scale in upsample_scales: + k_size = (1, scale * 2 + 1) + padding = (0, scale) + stretch = Stretch2D(scale, 1) + + conv = nn.Conv2D( + 1, 1, kernel_size=k_size, padding=padding, bias_attr=False) + weight_ = paddle.full_like(conv.weight, 1. / k_size[1]) + conv.weight.set_value(weight_) + self.up_layers.append(stretch) + self.up_layers.append(conv) + + def forward(self, m): + ''' + Args: + c (Tensor): Input tensor (B, C_aux, T). + Returns: + Tensor: Output tensor (B, (T - 2 * pad) * prob(upsample_scales), C_aux). + Tensor: Output tensor (B, (T - 2 * pad) * prob(upsample_scales), res_out_dims). + ''' + # aux: [B, C_aux, T] + # -> [B, res_out_dims, T - 2 * aux_context_window] + # -> [B, 1, res_out_dims, T - 2 * aux_context_window] + aux = self.resnet(m).unsqueeze(1) + # aux: [B, 1, res_out_dims, T - 2 * aux_context_window] + # -> [B, 1, res_out_dims, (T - 2 * pad) * prob(upsample_scales)] + aux = self.resnet_stretch(aux) + # aux: [B, 1, res_out_dims, T * prob(upsample_scales)] + # -> [B, res_out_dims, T * prob(upsample_scales)] + aux = aux.squeeze(1) + # m: [B, C_aux, T] -> [B, 1, C_aux, T] + m = m.unsqueeze(1) + for f in self.up_layers: + m = f(m) + # m: [B, 1, C_aux, T*prob(upsample_scales)] + # -> [B, C_aux, T * prob(upsample_scales)] + # -> [B, C_aux, (T - 2 * pad) * prob(upsample_scales)] + m = m.squeeze(1)[:, :, self.indent:-self.indent] + # m: [B, (T - 2 * pad) * prob(upsample_scales), C_aux] + # aux: [B, (T - 2 * pad) * prob(upsample_scales), res_out_dims] + return m.transpose([0, 2, 1]), aux.transpose([0, 2, 1]) + + +class WaveRNN(nn.Layer): + def __init__( + self, + rnn_dims: int=512, + fc_dims: int=512, + bits: int=9, + aux_context_window: int=2, + upsample_scales: List[int]=[4, 5, 3, 5], + aux_channels: int=80, + compute_dims: int=128, + res_out_dims: int=128, + res_blocks: int=10, + hop_length: int=300, + sample_rate: int=24000, + mode='RAW', + init_type: str="xavier_uniform", ): + ''' + Args: + rnn_dims (int, optional): Hidden dims of RNN Layers. + fc_dims (int, optional): Dims of FC Layers. + bits (int, optional): bit depth of signal. + aux_context_window (int, optional): The context window size of the first convolution applied to the + auxiliary input, by default 2 + upsample_scales (List[int], optional): Upsample scales of the upsample network. + aux_channels (int, optional): Auxiliary channel of the residual blocks. + compute_dims (int, optional): Dims of Conv1D in MelResNet. + res_out_dims (int, optional): Dims of output in MelResNet. + res_blocks (int, optional): Number of residual blocks. + mode (str, optional): Output mode of the WaveRNN vocoder. + `MOL` for Mixture of Logistic Distribution, and `RAW` for quantized bits as the model's output. + init_type (str): How to initialize parameters. + ''' + super().__init__() + self.mode = mode + self.aux_context_window = aux_context_window + if self.mode == 'RAW': + self.n_classes = 2**bits + elif self.mode == 'MOL': + self.n_classes = 10 * 3 + else: + RuntimeError('Unknown model mode value - ', self.mode) + + # List of rnns to call 'flatten_parameters()' on + self._to_flatten = [] + + self.rnn_dims = rnn_dims + self.aux_dims = res_out_dims // 4 + self.hop_length = hop_length + self.sample_rate = sample_rate + + # initialize parameters + initialize(self, init_type) + + self.upsample = UpsampleNetwork( + aux_channels=aux_channels, + upsample_scales=upsample_scales, + compute_dims=compute_dims, + res_blocks=res_blocks, + res_out_dims=res_out_dims, + aux_context_window=aux_context_window) + self.I = nn.Linear(aux_channels + self.aux_dims + 1, rnn_dims) + + self.rnn1 = nn.GRU(rnn_dims, rnn_dims) + self.rnn2 = nn.GRU(rnn_dims + self.aux_dims, rnn_dims) + + self._to_flatten += [self.rnn1, self.rnn2] + + self.fc1 = nn.Linear(rnn_dims + self.aux_dims, fc_dims) + self.fc2 = nn.Linear(fc_dims + self.aux_dims, fc_dims) + self.fc3 = nn.Linear(fc_dims, self.n_classes) + + # Avoid fragmentation of RNN parameters and associated warning + self._flatten_parameters() + + nn.initializer.set_global_initializer(None) + + def forward(self, x, c): + ''' + Args: + x (Tensor): wav sequence, [B, T] + c (Tensor): mel spectrogram [B, C_aux, T'] + + T = (T' - 2 * aux_context_window ) * hop_length + Returns: + Tensor: [B, T, n_classes] + ''' + # Although we `_flatten_parameters()` on init, when using DataParallel + # the model gets replicated, making it no longer guaranteed that the + # weights are contiguous in GPU memory. Hence, we must call it again + self._flatten_parameters() + + bsize = paddle.shape(x)[0] + h1 = paddle.zeros([1, bsize, self.rnn_dims]) + h2 = paddle.zeros([1, bsize, self.rnn_dims]) + # c: [B, T, C_aux] + # aux: [B, T, res_out_dims] + c, aux = self.upsample(c) + + aux_idx = [self.aux_dims * 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]] + + x = paddle.concat([x.unsqueeze(-1), c, a1], axis=2) + x = self.I(x) + res = x + x, _ = self.rnn1(x, h1) + + x = x + res + res = x + x = paddle.concat([x, a2], axis=2) + x, _ = self.rnn2(x, h2) + + x = x + res + x = paddle.concat([x, a3], axis=2) + x = F.relu(self.fc1(x)) + + x = paddle.concat([x, a4], axis=2) + x = F.relu(self.fc2(x)) + + return self.fc3(x) + + @paddle.no_grad() + def generate(self, + c, + batched: bool=True, + target: int=12000, + overlap: int=600, + mu_law: bool=True, + gen_display: bool=False): + """ + Args: + c(Tensor): input mels, (T', C_aux) + batched(bool): generate in batch or not + target(int): target number of samples to be generated in each batch entry + overlap(int): number of samples for crossfading between batches + mu_law(bool) + Returns: + wav sequence: Output (T' * prod(upsample_scales), out_channels, C_out). + """ + + self.eval() + + mu_law = mu_law if self.mode == 'RAW' else False + + output = [] + start = time.time() + + # pseudo batch + # (T, C_aux) -> (1, C_aux, T) + c = paddle.transpose(c, [1, 0]).unsqueeze(0) + T = paddle.shape(c)[-1] + wave_len = T * self.hop_length + # TODO remove two transpose op by modifying function pad_tensor + c = self.pad_tensor( + c.transpose([0, 2, 1]), pad=self.aux_context_window, + side='both').transpose([0, 2, 1]) + + c, aux = self.upsample(c) + + if batched: + # (num_folds, target + 2 * overlap, features) + c = self.fold_with_overlap(c, target, overlap) + aux = self.fold_with_overlap(aux, target, overlap) + + # for dygraph to static graph, if use seq_len of `b_size, seq_len, _ = paddle.shape(c)` in for + # will not get TensorArray + # see https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/04_dygraph_to_static/case_analysis_cn.html#list-lodtensorarray + # b_size, seq_len, _ = paddle.shape(c) + b_size = paddle.shape(c)[0] + seq_len = paddle.shape(c)[1] + + h1 = paddle.zeros([b_size, self.rnn_dims]) + h2 = paddle.zeros([b_size, self.rnn_dims]) + x = paddle.zeros([b_size, 1]) + + d = self.aux_dims + aux_split = [aux[:, :, d * i:d * (i + 1)] for i in range(4)] + + for i in range(seq_len): + m_t = c[:, i, :] + # for dygraph to static graph + # a1_t, a2_t, a3_t, a4_t = (a[:, i, :] for a in aux_split) + a1_t = aux_split[0][:, i, :] + a2_t = aux_split[1][:, i, :] + a3_t = aux_split[2][:, i, :] + a4_t = aux_split[3][:, i, :] + x = paddle.concat([x, m_t, a1_t], axis=1) + x = self.I(x) + # use GRUCell here + h1, _ = self.rnn1[0].cell(x, h1) + x = x + h1 + inp = paddle.concat([x, a2_t], axis=1) + # use GRUCell here + h2, _ = self.rnn2[0].cell(inp, h2) + + x = x + h2 + x = paddle.concat([x, a3_t], axis=1) + x = F.relu(self.fc1(x)) + + x = paddle.concat([x, a4_t], axis=1) + x = F.relu(self.fc2(x)) + + logits = self.fc3(x) + + if self.mode == 'MOL': + sample = sample_from_discretized_mix_logistic( + logits.unsqueeze(0).transpose([0, 2, 1])) + output.append(sample.reshape([-1])) + x = sample.transpose([1, 0, 2]) + + elif self.mode == 'RAW': + posterior = F.softmax(logits, axis=1) + distrib = paddle.distribution.Categorical(posterior) + # corresponding operate [np.floor((fx + 1) / 2 * mu + 0.5)] in enocde_mu_law + # distrib.sample([1])[0].cast('float32'): [0, 2**bits-1] + # sample: [-1, 1] + sample = 2 * distrib.sample([1])[0].cast('float32') / ( + self.n_classes - 1.) - 1. + output.append(sample) + x = sample.unsqueeze(-1) + else: + raise RuntimeError('Unknown model mode value - ', self.mode) + + if gen_display: + if i % 1000 == 0: + self.gen_display(i, int(seq_len), int(b_size), start) + + output = paddle.stack(output).transpose([1, 0]) + + if mu_law: + output = decode_mu_law(output, self.n_classes, False) + + if batched: + output = self.xfade_and_unfold(output, target, overlap) + else: + output = output[0] + + # Fade-out at the end to avoid signal cutting out suddenly + fade_out = paddle.linspace(1, 0, 10 * self.hop_length) + output = output[:wave_len] + output[-10 * self.hop_length:] *= fade_out + + self.train() + + # 增加 C_out 维度 + return output.unsqueeze(-1) + + def _flatten_parameters(self): + [m.flatten_parameters() for m in self._to_flatten] + + def pad_tensor(self, x, pad, side='both'): + ''' + Args: + x(Tensor): mel, [1, n_frames, 80] + pad(int): + side(str, optional): (Default value = 'both') + + Returns: + Tensor + ''' + b, t, _ = paddle.shape(x) + # for dygraph to static graph + c = x.shape[-1] + total = t + 2 * pad if side == 'both' else t + pad + padded = paddle.zeros([b, total, c]) + if side == 'before' or side == 'both': + padded[:, pad:pad + t, :] = x + elif side == 'after': + padded[:, :t, :] = x + return padded + + def fold_with_overlap(self, x, target, overlap): + ''' + Fold the tensor with overlap for quick batched inference. + Overlap will be used for crossfading in xfade_and_unfold() + + Args: + x(Tensor): Upsampled conditioning features. mels or aux + shape=(1, T, features) + mels: [1, T, 80] + aux: [1, T, 128] + target(int): Target timesteps for each index of batch + overlap(int): Timesteps for both xfade and rnn warmup + + Returns: + Tensor: + shape=(num_folds, target + 2 * overlap, features) + num_flods = (time_seq - overlap) // (target + overlap) + mel: [num_folds, target + 2 * overlap, 80] + aux: [num_folds, target + 2 * overlap, 128] + + Details: + x = [[h1, h2, ... hn]] + Where each h is a vector of conditioning features + Eg: target=2, overlap=1 with x.size(1)=10 + + folded = [[h1, h2, h3, h4], + [h4, h5, h6, h7], + [h7, h8, h9, h10]] + ''' + + _, total_len, features = paddle.shape(x) + + # Calculate variables needed + num_folds = (total_len - overlap) // (target + overlap) + extended_len = num_folds * (overlap + target) + overlap + remaining = total_len - extended_len + + # Pad if some time steps poking out + if remaining != 0: + num_folds += 1 + padding = target + 2 * overlap - remaining + x = self.pad_tensor(x, padding, side='after') + + folded = paddle.zeros([num_folds, target + 2 * overlap, features]) + + # Get the values for the folded tensor + for i in range(num_folds): + start = i * (target + overlap) + end = start + target + 2 * overlap + folded[i] = x[0][start:end, :] + return folded + + def xfade_and_unfold(self, y, target: int=12000, overlap: int=600): + ''' Applies a crossfade and unfolds into a 1d array. + + Args: + y (Tensor): + Batched sequences of audio samples + shape=(num_folds, target + 2 * overlap) + dtype=paddle.float32 + overlap (int): Timesteps for both xfade and rnn warmup + + Returns: + Tensor + audio samples in a 1d array + shape=(total_len) + dtype=paddle.float32 + + Details: + y = [[seq1], + [seq2], + [seq3]] + + Apply a gain envelope at both ends of the sequences + + y = [[seq1_in, seq1_target, seq1_out], + [seq2_in, seq2_target, seq2_out], + [seq3_in, seq3_target, seq3_out]] + + Stagger and add up the groups of samples: + + [seq1_in, seq1_target, (seq1_out + seq2_in), seq2_target, ...] + + ''' + # num_folds = (total_len - overlap) // (target + overlap) + num_folds, length = paddle.shape(y) + target = length - 2 * overlap + total_len = num_folds * (target + overlap) + overlap + + # Need some silence for the run warmup + slience_len = 0 + linear_len = slience_len + fade_len = overlap - slience_len + slience = paddle.zeros([slience_len], dtype=paddle.float32) + linear = paddle.ones([linear_len], dtype=paddle.float32) + + # Equal power crossfade + # fade_in increase from 0 to 1, fade_out reduces from 1 to 0 + sigmoid_scale = 2.3 + t = paddle.linspace( + -sigmoid_scale, sigmoid_scale, fade_len, dtype=paddle.float32) + # sigmoid 曲线应该更好 + fade_in = paddle.nn.functional.sigmoid(t) + fade_out = 1 - paddle.nn.functional.sigmoid(t) + # Concat the silence to the fades + fade_out = paddle.concat([linear, fade_out]) + fade_in = paddle.concat([slience, fade_in]) + + # Apply the gain to the overlap samples + y[:, :overlap] *= fade_in + y[:, -overlap:] *= fade_out + + unfolded = paddle.zeros([total_len], dtype=paddle.float32) + + # Loop to add up all the samples + for i in range(num_folds): + start = i * (target + overlap) + end = start + target + 2 * overlap + unfolded[start:end] += y[i] + + return unfolded + + def gen_display(self, i, seq_len, b_size, start): + gen_rate = (i + 1) / (time.time() - start) * b_size / 1000 + pbar = self.progbar(i, seq_len) + msg = f'| {pbar} {i*b_size}/{seq_len*b_size} | Batch Size: {b_size} | Gen Rate: {gen_rate:.1f}kHz | ' + sys.stdout.write(f"\r{msg}") + + def progbar(self, i, n, size=16): + done = int(i * size) // n + bar = '' + for i in range(size): + bar += '█' if i <= done else '░' + return bar + + +class WaveRNNInference(nn.Layer): + def __init__(self, normalizer, wavernn): + super().__init__() + self.normalizer = normalizer + self.wavernn = wavernn + + def forward(self, + logmel, + batched: bool=True, + target: int=12000, + overlap: int=600, + mu_law: bool=True, + gen_display: bool=False): + normalized_mel = self.normalizer(logmel) + + wav = self.wavernn.generate( + normalized_mel, ) + # batched=batched, + # target=target, + # overlap=overlap, + # mu_law=mu_law, + # gen_display=gen_display) + + return wav diff --git a/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn_updater.py b/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn_updater.py new file mode 100644 index 0000000..b2756d0 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/models/wavernn/wavernn_updater.py @@ -0,0 +1,201 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from pathlib import Path + +import paddle +import soundfile as sf +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def calculate_grad_norm(parameters, norm_type: str=2): + ''' + calculate grad norm of mdoel's parameters + parameters: + model's parameters + norm_type: str + Returns + ------------ + Tensor + grad_norm + ''' + + grad_list = [ + paddle.to_tensor(p.grad) for p in parameters if p.grad is not None + ] + norm_list = paddle.stack( + [paddle.norm(grad, norm_type) for grad in grad_list]) + total_norm = paddle.norm(norm_list) + return total_norm + + +# for save name in gen_valid_samples() +ITERATION = 0 + + +class WaveRNNUpdater(StandardUpdater): + def __init__(self, + model: Layer, + optimizer: Optimizer, + criterion: Layer, + dataloader: DataLoader, + init_state=None, + output_dir: Path=None, + mode='RAW'): + super().__init__(model, optimizer, dataloader, init_state=None) + + self.criterion = criterion + # self.scheduler = scheduler + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + self.mode = mode + + def update_core(self, batch): + + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + # parse batch + self.model.train() + self.optimizer.clear_grad() + + wav, y, mel = batch + + y_hat = self.model(wav, mel) + if self.mode == 'RAW': + y_hat = y_hat.transpose([0, 2, 1]).unsqueeze(-1) + elif self.mode == 'MOL': + y_hat = paddle.cast(y, dtype='float32') + + y = y.unsqueeze(-1) + loss = self.criterion(y_hat, y) + loss.backward() + grad_norm = float( + calculate_grad_norm(self.model.parameters(), norm_type=2)) + + self.optimizer.step() + + report("train/loss", float(loss)) + report("train/grad_norm", float(grad_norm)) + + losses_dict["loss"] = float(loss) + losses_dict["grad_norm"] = float(grad_norm) + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + global ITERATION + ITERATION = self.state.iteration + 1 + + +class WaveRNNEvaluator(StandardEvaluator): + def __init__(self, + model: Layer, + criterion: Layer, + dataloader: Optimizer, + output_dir: Path=None, + valid_generate_loader=None, + config=None): + super().__init__(model, dataloader) + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + self.criterion = criterion + self.valid_generate_loader = valid_generate_loader + self.config = config + self.mode = config.model.mode + + self.valid_samples_dir = output_dir / "valid_samples" + self.valid_samples_dir.mkdir(parents=True, exist_ok=True) + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + # parse batch + wav, y, mel = batch + y_hat = self.model(wav, mel) + + if self.mode == 'RAW': + y_hat = y_hat.transpose([0, 2, 1]).unsqueeze(-1) + elif self.mode == 'MOL': + y_hat = paddle.cast(y, dtype='float32') + + y = y.unsqueeze(-1) + loss = self.criterion(y_hat, y) + report("eval/loss", float(loss)) + + losses_dict["loss"] = float(loss) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) + + def gen_valid_samples(self): + + for i, item in enumerate(self.valid_generate_loader): + if i >= self.config.generate_num: + break + print( + '\n| Generating: {}/{}'.format(i + 1, self.config.generate_num)) + + mel = item['feats'] + wav = item['wave'] + wav = wav.squeeze(0) + + origin_save_path = self.valid_samples_dir / '{}_steps_{}_target.wav'.format( + self.iteration, i) + sf.write(origin_save_path, wav.numpy(), samplerate=self.config.fs) + + if self.config.inference.gen_batched: + batch_str = 'gen_batched_target{}_overlap{}'.format( + self.config.inference.target, self.config.inference.overlap) + else: + batch_str = 'gen_not_batched' + gen_save_path = str(self.valid_samples_dir / + '{}_steps_{}_{}.wav'.format(self.iteration, i, + batch_str)) + # (1, T, C_aux) -> (T, C_aux) + mel = mel.squeeze(0) + gen_sample = self.model.generate( + mel, self.config.inference.gen_batched, + self.config.inference.target, self.config.inference.overlap, + self.config.mu_law) + sf.write( + gen_save_path, gen_sample.numpy(), samplerate=self.config.fs) + + def __call__(self, trainer=None): + summary = self.evaluate() + for k, v in summary.items(): + report(k, v) + # gen samples at then end of evaluate + self.iteration = ITERATION + if self.iteration % self.config.gen_eval_samples_interval_steps == 0: + self.gen_valid_samples() diff --git a/ernie-sat/paddlespeech/t2s/modules/__init__.py b/ernie-sat/paddlespeech/t2s/modules/__init__.py new file mode 100644 index 0000000..1e33120 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .conv import * +from .geometry import * +from .losses import * +from .positional_encoding import * diff --git a/ernie-sat/paddlespeech/t2s/modules/activation.py b/ernie-sat/paddlespeech/t2s/modules/activation.py new file mode 100644 index 0000000..8d8cd62 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/activation.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import paddle.nn.functional as F +from paddle import nn + + +class GLU(nn.Layer): + """Gated Linear Units (GLU) Layer""" + + def __init__(self, dim: int=-1): + super().__init__() + self.dim = dim + + def forward(self, xs): + return F.glu(xs, axis=self.dim) + + +def get_activation(act, **kwargs): + """Return activation function.""" + + activation_funcs = { + "hardtanh": paddle.nn.Hardtanh, + "tanh": paddle.nn.Tanh, + "relu": paddle.nn.ReLU, + "selu": paddle.nn.SELU, + "leakyrelu": paddle.nn.LeakyReLU, + "swish": paddle.nn.Swish, + "glu": GLU + } + + return activation_funcs[act](**kwargs) diff --git a/ernie-sat/paddlespeech/t2s/modules/causal_conv.py b/ernie-sat/paddlespeech/t2s/modules/causal_conv.py new file mode 100644 index 0000000..3abccc1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/causal_conv.py @@ -0,0 +1,74 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Causal convolusion layer modules.""" +import paddle +from paddle import nn + + +class CausalConv1D(nn.Layer): + """CausalConv1D module with customized initialization.""" + + def __init__( + self, + in_channels, + out_channels, + kernel_size, + dilation=1, + bias=True, + pad="Pad1D", + pad_params={"value": 0.0}, ): + """Initialize CausalConv1d module.""" + super().__init__() + self.pad = getattr(paddle.nn, pad)((kernel_size - 1) * dilation, + **pad_params) + self.conv = nn.Conv1D( + in_channels, + out_channels, + kernel_size, + dilation=dilation, + bias_attr=bias) + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input tensor (B, in_channels, T). + Returns: + Tensor: Output tensor (B, out_channels, T). + """ + return self.conv(self.pad(x))[:, :, :x.shape[2]] + + +class CausalConv1DTranspose(nn.Layer): + """CausalConv1DTranspose module with customized initialization.""" + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride, + bias=True): + """Initialize CausalConvTranspose1d module.""" + super().__init__() + self.deconv = nn.Conv1DTranspose( + in_channels, out_channels, kernel_size, stride, bias_attr=bias) + self.stride = stride + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input tensor (B, in_channels, T_in). + Returns: + Tensor: Output tensor (B, out_channels, T_out). + """ + return self.deconv(x)[:, :, :-self.stride] diff --git a/ernie-sat/paddlespeech/t2s/modules/conformer/__init__.py b/ernie-sat/paddlespeech/t2s/modules/conformer/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/conformer/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/modules/conformer/convolution.py b/ernie-sat/paddlespeech/t2s/modules/conformer/convolution.py new file mode 100644 index 0000000..185c62f --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/conformer/convolution.py @@ -0,0 +1,81 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""ConvolutionModule definition.""" +from paddle import nn + + +class ConvolutionModule(nn.Layer): + """ConvolutionModule in Conformer model. + + Args: + channels (int): The number of channels of conv layers. + kernel_size (int): Kernerl size of conv layers. + """ + + def __init__(self, channels, kernel_size, activation=nn.ReLU(), bias=True): + """Construct an ConvolutionModule object.""" + super().__init__() + # kernerl_size should be a odd number for 'SAME' padding + assert (kernel_size - 1) % 2 == 0 + + self.pointwise_conv1 = nn.Conv1D( + channels, + 2 * channels, + kernel_size=1, + stride=1, + padding=0, + bias_attr=bias, ) + self.depthwise_conv = nn.Conv1D( + channels, + channels, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, + groups=channels, + bias_attr=bias, ) + self.norm = nn.BatchNorm1D(channels) + self.pointwise_conv2 = nn.Conv1D( + channels, + channels, + kernel_size=1, + stride=1, + padding=0, + bias_attr=bias, ) + self.activation = activation + + def forward(self, x): + """Compute convolution module. + + Args: + x (Tensor): Input tensor (#batch, time, channels). + Returns: + Tensor: Output tensor (#batch, time, channels). + """ + # exchange the temporal dimension and the feature dimension + x = x.transpose([0, 2, 1]) + + # GLU mechanism + # (batch, 2*channel, time) + x = self.pointwise_conv1(x) + # (batch, channel, time) + x = nn.functional.glu(x, axis=1) + + # 1D Depthwise Conv + x = self.depthwise_conv(x) + x = self.activation(self.norm(x)) + + x = self.pointwise_conv2(x) + + return x.transpose([0, 2, 1]) diff --git a/ernie-sat/paddlespeech/t2s/modules/conformer/encoder_layer.py b/ernie-sat/paddlespeech/t2s/modules/conformer/encoder_layer.py new file mode 100644 index 0000000..61c3261 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/conformer/encoder_layer.py @@ -0,0 +1,182 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Encoder self-attention layer definition.""" +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.layer_norm import LayerNorm + + +class EncoderLayer(nn.Layer): + """Encoder layer module. + + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` or `RelPositionMultiHeadedAttention` instance + can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward`, `MultiLayeredConv1d`, or `Conv1dLinear` instance + can be used as the argument. + feed_forward_macaron (nn.Layer): Additional feed-forward module instance. + `PositionwiseFeedForward`, `MultiLayeredConv1d`, or `Conv1dLinear` instance + can be used as the argument. + conv_module (nn.Layer): Convolution module instance. + `ConvlutionModule` instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + stochastic_depth_rate (float): Proability to skip this layer. + During training, the layer may skip residual computation and return input + as-is with given probability. + """ + + def __init__( + self, + size, + self_attn, + feed_forward, + feed_forward_macaron, + conv_module, + dropout_rate, + normalize_before=True, + concat_after=False, + stochastic_depth_rate=0.0, ): + """Construct an EncoderLayer object.""" + super().__init__() + self.self_attn = self_attn + self.feed_forward = feed_forward + self.feed_forward_macaron = feed_forward_macaron + self.conv_module = conv_module + self.norm_ff = LayerNorm(size) # for the FNN module + self.norm_mha = LayerNorm(size) # for the MHA module + if feed_forward_macaron is not None: + self.norm_ff_macaron = LayerNorm(size) + self.ff_scale = 0.5 + else: + self.ff_scale = 1.0 + if self.conv_module is not None: + self.norm_conv = LayerNorm(size) # for the CNN module + self.norm_final = LayerNorm( + size) # for the final output of the block + self.dropout = nn.Dropout(dropout_rate) + self.size = size + self.normalize_before = normalize_before + self.concat_after = concat_after + if self.concat_after: + self.concat_linear = nn.Linear(size + size, size) + self.stochastic_depth_rate = stochastic_depth_rate + + def forward(self, x_input, mask, cache=None): + """Compute encoded features. + + Args: + x_input(Union[Tuple, Tensor]): Input tensor w/ or w/o pos emb. + - w/ pos emb: Tuple of tensors [(#batch, time, size), (1, time, size)]. + - w/o pos emb: Tensor (#batch, time, size). + mask(Tensor): Mask tensor for the input (#batch, time). + cache (Tensor): + + Returns: + Tensor: Output tensor (#batch, time, size). + Tensor: Mask tensor (#batch, time). + """ + if isinstance(x_input, tuple): + x, pos_emb = x_input[0], x_input[1] + else: + x, pos_emb = x_input, None + + skip_layer = False + # with stochastic depth, residual connection `x + f(x)` becomes + # `x <- x + 1 / (1 - p) * f(x)` at training time. + stoch_layer_coeff = 1.0 + if self.training and self.stochastic_depth_rate > 0: + skip_layer = paddle.rand(1).item() < self.stochastic_depth_rate + stoch_layer_coeff = 1.0 / (1 - self.stochastic_depth_rate) + + if skip_layer: + if cache is not None: + x = paddle.concat([cache, x], axis=1) + if pos_emb is not None: + return (x, pos_emb), mask + return x, mask + + # whether to use macaron style + if self.feed_forward_macaron is not None: + residual = x + if self.normalize_before: + x = self.norm_ff_macaron(x) + x = residual + stoch_layer_coeff * self.ff_scale * self.dropout( + self.feed_forward_macaron(x)) + if not self.normalize_before: + x = self.norm_ff_macaron(x) + + # multi-headed self-attention module + residual = x + if self.normalize_before: + x = self.norm_mha(x) + + if cache is None: + x_q = x + else: + assert cache.shape == (x.shape[0], x.shape[1] - 1, self.size) + x_q = x[:, -1:, :] + residual = residual[:, -1:, :] + mask = None if mask is None else mask[:, -1:, :] + + if pos_emb is not None: + x_att = self.self_attn(x_q, x, x, pos_emb, mask) + else: + x_att = self.self_attn(x_q, x, x, mask) + + if self.concat_after: + x_concat = paddle.concat((x, x_att), axis=-1) + x = residual + stoch_layer_coeff * self.concat_linear(x_concat) + else: + x = residual + stoch_layer_coeff * self.dropout(x_att) + if not self.normalize_before: + x = self.norm_mha(x) + + # convolution module + if self.conv_module is not None: + residual = x + if self.normalize_before: + x = self.norm_conv(x) + x = residual + stoch_layer_coeff * self.dropout(self.conv_module(x)) + if not self.normalize_before: + x = self.norm_conv(x) + + # feed forward module + residual = x + if self.normalize_before: + x = self.norm_ff(x) + x = residual + stoch_layer_coeff * self.ff_scale * self.dropout( + self.feed_forward(x)) + if not self.normalize_before: + x = self.norm_ff(x) + + if self.conv_module is not None: + x = self.norm_final(x) + + if cache is not None: + x = paddle.concat([cache, x], axis=1) + + if pos_emb is not None: + return (x, pos_emb), mask + + return x, mask diff --git a/ernie-sat/paddlespeech/t2s/modules/conv.py b/ernie-sat/paddlespeech/t2s/modules/conv.py new file mode 100644 index 0000000..aa875bd --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/conv.py @@ -0,0 +1,238 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + +__all__ = [ + "Conv1dCell", + "Conv1dBatchNorm", +] + + +class Conv1dCell(nn.Conv1D): + """A subclass of Conv1D layer, which can be used in an autoregressive + decoder like an RNN cell. + + When used in autoregressive decoding, it performs causal temporal + convolution incrementally. At each time step, it takes a step input and + returns a step output. + + Notes + ------ + It is done by caching an internal buffer of length ``receptive_file - 1``. + when adding a step input, the buffer is shited by one step, the latest + input is added to be buffer and the oldest step is discarded. And it + returns a step output. For single step case, convolution is equivalent to a + linear transformation. + That it can be used as a cell depends on several restrictions: + 1. stride must be 1; + 2. padding must be a causal padding (recpetive_field - 1, 0). + Thus, these arguments are removed from the ``__init__`` method of this + class. + + Args: + in_channels (int): The feature size of the input. + out_channels (int): The feature size of the output. + kernel_size (int or Tuple[int]): The size of the kernel. + dilation (int or Tuple[int]): The dilation of the convolution, by default 1 + weight_attr (ParamAttr, Initializer, str or bool, optional) : The parameter attribute of the convolution kernel, + by default None. + bias_attr (ParamAttr, Initializer, str or bool, optional):The parameter attribute of the bias. + If ``False``, this layer does not have a bias, by default None. + + Examples: + >>> cell = Conv1dCell(3, 4, kernel_size=5) + >>> inputs = [paddle.randn([4, 3]) for _ in range(16)] + >>> outputs = [] + >>> cell.eval() + >>> cell.start_sequence() + >>> for xt in inputs: + >>> outputs.append(cell.add_input(xt)) + >>> len(outputs)) + 16 + >>> outputs[0].shape + [4, 4] + """ + + def __init__(self, + in_channels, + out_channels, + kernel_size, + dilation=1, + weight_attr=None, + bias_attr=None): + _dilation = dilation[0] if isinstance(dilation, + (tuple, list)) else dilation + _kernel_size = kernel_size[0] if isinstance(kernel_size, ( + tuple, list)) else kernel_size + self._r = 1 + (_kernel_size - 1) * _dilation + super().__init__( + in_channels, + out_channels, + kernel_size, + padding=(self._r - 1, 0), + dilation=dilation, + weight_attr=weight_attr, + bias_attr=bias_attr, + data_format="NCL") + + @property + def receptive_field(self): + """The receptive field of the Conv1dCell. + """ + return self._r + + def start_sequence(self): + """Prepare the layer for a series of incremental forward. + + Warnings: + This method should be called before a sequence of calls to + ``add_input``. + + Raises: + Exception + If this method is called when the layer is in training mode. + """ + if self.training: + raise Exception("only use start_sequence in evaluation") + self._buffer = None + + # NOTE: call self's weight norm hook expliccitly since self.weight + # is visited directly in this method without calling self.__call__ + # method. If we do not trigger the weight norm hook, the weight + # may be outdated. e.g. after loading from a saved checkpoint + # see also: https://github.com/pytorch/pytorch/issues/47588 + for hook in self._forward_pre_hooks.values(): + hook(self, None) + self._reshaped_weight = paddle.reshape(self.weight, + (self._out_channels, -1)) + + def initialize_buffer(self, x_t): + """Initialize the buffer for the step input. + + Args: + x_t (Tensor): The step input. shape=(batch_size, in_channels) + + """ + batch_size, _ = x_t.shape + self._buffer = paddle.zeros( + (batch_size, self._in_channels, self.receptive_field), + dtype=x_t.dtype) + + def update_buffer(self, x_t): + """Shift the buffer by one step. + + Args: + x_t (Tensor): The step input. shape=(batch_size, in_channels) + + """ + self._buffer = paddle.concat( + [self._buffer[:, :, 1:], paddle.unsqueeze(x_t, -1)], -1) + + def add_input(self, x_t): + """Add step input and compute step output. + + Args: + x_t (Tensor): The step input. shape=(batch_size, in_channels) + + Returns: + y_t (Tensor): The step output. shape=(batch_size, out_channels) + + """ + batch_size = x_t.shape[0] + if self.receptive_field > 1: + if self._buffer is None: + self.initialize_buffer(x_t) + + # update buffer + self.update_buffer(x_t) + if self._dilation[0] > 1: + input = self._buffer[:, :, ::self._dilation[0]] + else: + input = self._buffer + input = paddle.reshape(input, (batch_size, -1)) + else: + input = x_t + y_t = paddle.matmul(input, self._reshaped_weight, transpose_y=True) + y_t = y_t + self.bias + return y_t + + +class Conv1dBatchNorm(nn.Layer): + """A Conv1D Layer followed by a BatchNorm1D. + + Args: + in_channels (int): The feature size of the input. + out_channels (int): The feature size of the output. + kernel_size (int): The size of the convolution kernel. + stride (int, optional): The stride of the convolution, by default 1. + padding (int, str or Tuple[int], optional): + The padding of the convolution. + If int, a symmetrical padding is applied before convolution; + If str, it should be "same" or "valid"; + If Tuple[int], its length should be 2, meaning + ``(pad_before, pad_after)``, by default 0. + weight_attr (ParamAttr, Initializer, str or bool, optional): + The parameter attribute of the convolution kernel, + by default None. + bias_attr (ParamAttr, Initializer, str or bool, optional): + The parameter attribute of the bias of the convolution, + by defaultNone. + data_format (str ["NCL" or "NLC"], optional): The data layout of the input, by default "NCL" + momentum (float, optional): The momentum of the BatchNorm1D layer, by default 0.9 + epsilon (float, optional): The epsilon of the BatchNorm1D layer, by default 1e-05 + """ + + def __init__(self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding=0, + weight_attr=None, + bias_attr=None, + data_format="NCL", + momentum=0.9, + epsilon=1e-05): + super().__init__() + self.conv = nn.Conv1D( + in_channels, + out_channels, + kernel_size, + stride, + padding=padding, + weight_attr=weight_attr, + bias_attr=bias_attr, + data_format=data_format) + self.bn = nn.BatchNorm1D( + out_channels, + momentum=momentum, + epsilon=epsilon, + data_format=data_format) + + def forward(self, x): + """Forward pass of the Conv1dBatchNorm layer. + + Args: + x (Tensor): The input tensor. Its data layout depends on ``data_format``. + shape=(B, C_in, T_in) or (B, T_in, C_in) + + Returns: + Tensor: The output tensor. + shape=(B, C_out, T_out) or (B, T_out, C_out) + + """ + x = self.conv(x) + x = self.bn(x) + return x diff --git a/ernie-sat/paddlespeech/t2s/modules/geometry.py b/ernie-sat/paddlespeech/t2s/modules/geometry.py new file mode 100644 index 0000000..01eb5ad --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/geometry.py @@ -0,0 +1,44 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle + + +def shuffle_dim(x, axis, perm=None): + """Permute input tensor along aixs given the permutation or randomly. + + Args: + x (Tensor): The input tensor. + axis (int): The axis to shuffle. + perm (List[int], ndarray, optional): + The order to reorder the tensor along the ``axis``-th dimension. + It is a permutation of ``[0, d)``, where d is the size of the + ``axis``-th dimension of the input tensor. If not provided, + a random permutation is used. Defaults to None. + + Returns: + Tensor: The shuffled tensor, which has the same shape as x does. + """ + size = x.shape[axis] + if perm is not None and len(perm) != size: + raise ValueError("length of permutation should equals the input " + "tensor's axis-th dimension's size") + if perm is not None: + perm = np.array(perm) + else: + perm = np.random.permutation(size) + + perm = paddle.to_tensor(perm) + out = paddle.gather(x, perm, axis) + return out diff --git a/ernie-sat/paddlespeech/t2s/modules/layer_norm.py b/ernie-sat/paddlespeech/t2s/modules/layer_norm.py new file mode 100644 index 0000000..088b98e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/layer_norm.py @@ -0,0 +1,60 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Layer normalization module.""" +import paddle +from paddle import nn + + +class LayerNorm(nn.LayerNorm): + """Layer normalization module. + Args: + nout (int): Output dim size. + dim (int): Dimension to be normalized. + """ + + def __init__(self, nout, dim=-1): + """Construct an LayerNorm object.""" + super().__init__(nout) + self.dim = dim + + def forward(self, x): + """Apply layer normalization. + + Args: + x (Tensor):Input tensor. + + Returns: + Tensor: Normalized tensor. + """ + + if self.dim == -1: + return super(LayerNorm, self).forward(x) + else: + len_dim = len(x.shape) + if self.dim < 0: + self.dim = len_dim + self.dim + assert self.dim >= 0 + + orig_perm = list(range(len_dim)) + new_perm = orig_perm[:] + # Python style item change is not able when converting dygraph to static graph. + # new_perm[self.dim], new_perm[len_dim -1] = new_perm[len_dim -1], new_perm[self.dim] + # use C++ style item change here + temp = new_perm[self.dim] + new_perm[self.dim] = new_perm[len_dim - 1] + new_perm[len_dim - 1] = temp + + return paddle.transpose( + super(LayerNorm, self).forward(paddle.transpose(x, new_perm)), + new_perm) diff --git a/ernie-sat/paddlespeech/t2s/modules/losses.py b/ernie-sat/paddlespeech/t2s/modules/losses.py new file mode 100644 index 0000000..db31bcf --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/losses.py @@ -0,0 +1,1008 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import librosa +import numpy as np +import paddle +from paddle import nn +from paddle.fluid.layers import sequence_mask +from paddle.nn import functional as F +from scipy import signal + +from paddlespeech.t2s.modules.nets_utils import make_non_pad_mask + + +# Losses for WaveRNN +def log_sum_exp(x): + """ numerically stable log_sum_exp implementation that prevents overflow """ + # TF ordering + axis = len(x.shape) - 1 + m = paddle.max(x, axis=axis) + m2 = paddle.max(x, axis=axis, keepdim=True) + return m + paddle.log(paddle.sum(paddle.exp(x - m2), axis=axis)) + + +# It is adapted from https://github.com/r9y9/wavenet_vocoder/blob/master/wavenet_vocoder/mixture.py +def discretized_mix_logistic_loss(y_hat, + y, + num_classes=65536, + log_scale_min=None, + reduce=True): + if log_scale_min is None: + log_scale_min = float(np.log(1e-14)) + y_hat = y_hat.transpose([0, 2, 1]) + assert y_hat.dim() == 3 + assert y_hat.shape[1] % 3 == 0 + nr_mix = y_hat.shape[1] // 3 + + # (B x T x C) + y_hat = y_hat.transpose([0, 2, 1]) + + # unpack parameters. (B, T, num_mixtures) x 3 + logit_probs = y_hat[:, :, :nr_mix] + means = y_hat[:, :, nr_mix:2 * nr_mix] + log_scales = paddle.clip( + y_hat[:, :, 2 * nr_mix:3 * nr_mix], min=log_scale_min) + + # B x T x 1 -> B x T x num_mixtures + y = y.expand_as(means) + centered_y = paddle.cast(y, dtype=paddle.get_default_dtype()) - means + inv_stdv = paddle.exp(-log_scales) + plus_in = inv_stdv * (centered_y + 1. / (num_classes - 1)) + cdf_plus = F.sigmoid(plus_in) + min_in = inv_stdv * (centered_y - 1. / (num_classes - 1)) + cdf_min = F.sigmoid(min_in) + + # log probability for edge case of 0 (before scaling) + # equivalent: torch.log(F.sigmoid(plus_in)) + # softplus: log(1+ e^{-x}) + log_cdf_plus = plus_in - F.softplus(plus_in) + + # log probability for edge case of 255 (before scaling) + # equivalent: (1 - F.sigmoid(min_in)).log() + log_one_minus_cdf_min = -F.softplus(min_in) + + # probability for all other cases + cdf_delta = cdf_plus - cdf_min + + mid_in = inv_stdv * centered_y + # log probability in the center of the bin, to be used in extreme cases + # (not actually used in our code) + log_pdf_mid = mid_in - log_scales - 2. * F.softplus(mid_in) + + # TODO: cdf_delta <= 1e-5 actually can happen. How can we choose the value + # for num_classes=65536 case? 1e-7? not sure.. + inner_inner_cond = cdf_delta > 1e-5 + + inner_inner_cond = paddle.cast( + inner_inner_cond, dtype=paddle.get_default_dtype()) + + # inner_inner_out = inner_inner_cond * \ + # paddle.log(paddle.clip(cdf_delta, min=1e-12)) + \ + # (1. - inner_inner_cond) * (log_pdf_mid - np.log((num_classes - 1) / 2)) + + inner_inner_out = inner_inner_cond * paddle.log( + paddle.clip(cdf_delta, min=1e-12)) + (1. - inner_inner_cond) * ( + log_pdf_mid - np.log((num_classes - 1) / 2)) + + inner_cond = y > 0.999 + + inner_cond = paddle.cast(inner_cond, dtype=paddle.get_default_dtype()) + + inner_out = inner_cond * log_one_minus_cdf_min + (1. - inner_cond + ) * inner_inner_out + cond = y < -0.999 + cond = paddle.cast(cond, dtype=paddle.get_default_dtype()) + + log_probs = cond * log_cdf_plus + (1. - cond) * inner_out + log_probs = log_probs + F.log_softmax(logit_probs, -1) + + if reduce: + return -paddle.mean(log_sum_exp(log_probs)) + else: + return -log_sum_exp(log_probs).unsqueeze(-1) + + +def sample_from_discretized_mix_logistic(y, log_scale_min=None): + """ + Sample from discretized mixture of logistic distributions + + Args: + y(Tensor): (B, C, T) + log_scale_min(float, optional): (Default value = None) + + Returns: + Tensor: sample in range of [-1, 1]. + """ + if log_scale_min is None: + log_scale_min = float(np.log(1e-14)) + + assert y.shape[1] % 3 == 0 + nr_mix = y.shape[1] // 3 + + # (B, T, C) + y = y.transpose([0, 2, 1]) + logit_probs = y[:, :, :nr_mix] + + # sample mixture indicator from softmax + temp = paddle.uniform( + logit_probs.shape, dtype=logit_probs.dtype, min=1e-5, max=1.0 - 1e-5) + temp = logit_probs - paddle.log(-paddle.log(temp)) + argmax = paddle.argmax(temp, axis=-1) + + # (B, T) -> (B, T, nr_mix) + one_hot = F.one_hot(argmax, nr_mix) + one_hot = paddle.cast(one_hot, dtype=paddle.get_default_dtype()) + + # select logistic parameters + means = paddle.sum(y[:, :, nr_mix:2 * nr_mix] * one_hot, axis=-1) + log_scales = paddle.clip( + paddle.sum(y[:, :, 2 * nr_mix:3 * nr_mix] * one_hot, axis=-1), + min=log_scale_min) + # sample from logistic & clip to interval + # we don't actually round to the nearest 8bit value when sampling + u = paddle.uniform(means.shape, min=1e-5, max=1.0 - 1e-5) + x = means + paddle.exp(log_scales) * (paddle.log(u) - paddle.log(1. - u)) + x = paddle.clip(x, min=-1., max=-1.) + + return x + + +# Loss for new Tacotron2 +class GuidedAttentionLoss(nn.Layer): + """Guided attention loss function module. + + This module calculates the guided attention loss described + in `Efficiently Trainable Text-to-Speech System Based + on Deep Convolutional Networks with Guided Attention`_, + which forces the attention to be diagonal. + + .. _`Efficiently Trainable Text-to-Speech System + Based on Deep Convolutional Networks with Guided Attention`: + https://arxiv.org/abs/1710.08969 + + """ + + def __init__(self, sigma=0.4, alpha=1.0, reset_always=True): + """Initialize guided attention loss module. + + Args: + sigma (float, optional): Standard deviation to control how close attention to a diagonal. + alpha (float, optional): Scaling coefficient (lambda). + reset_always (bool, optional): Whether to always reset masks. + + """ + super().__init__() + self.sigma = sigma + self.alpha = alpha + self.reset_always = reset_always + self.guided_attn_masks = None + self.masks = None + + def _reset_masks(self): + self.guided_attn_masks = None + self.masks = None + + def forward(self, att_ws, ilens, olens): + """Calculate forward propagation. + + Args: + att_ws(Tensor): Batch of attention weights (B, T_max_out, T_max_in). + ilens(Tensor(int64)): Batch of input lenghts (B,). + olens(Tensor(int64)): Batch of output lenghts (B,). + + Returns: + Tensor: Guided attention loss value. + + """ + if self.guided_attn_masks is None: + self.guided_attn_masks = self._make_guided_attention_masks(ilens, + olens) + if self.masks is None: + self.masks = self._make_masks(ilens, olens) + losses = self.guided_attn_masks * att_ws + loss = paddle.mean( + losses.masked_select(self.masks.broadcast_to(losses.shape))) + if self.reset_always: + self._reset_masks() + return self.alpha * loss + + def _make_guided_attention_masks(self, ilens, olens): + n_batches = len(ilens) + max_ilen = max(ilens) + max_olen = max(olens) + guided_attn_masks = paddle.zeros((n_batches, max_olen, max_ilen)) + + for idx, (ilen, olen) in enumerate(zip(ilens, olens)): + guided_attn_masks[idx, :olen, : + ilen] = self._make_guided_attention_mask( + ilen, olen, self.sigma) + return guided_attn_masks + + @staticmethod + def _make_guided_attention_mask(ilen, olen, sigma): + """Make guided attention mask. + + Examples + ---------- + >>> guided_attn_mask =_make_guided_attention(5, 5, 0.4) + >>> guided_attn_mask.shape + [5, 5] + >>> guided_attn_mask + tensor([[0.0000, 0.1175, 0.3935, 0.6753, 0.8647], + [0.1175, 0.0000, 0.1175, 0.3935, 0.6753], + [0.3935, 0.1175, 0.0000, 0.1175, 0.3935], + [0.6753, 0.3935, 0.1175, 0.0000, 0.1175], + [0.8647, 0.6753, 0.3935, 0.1175, 0.0000]]) + >>> guided_attn_mask =_make_guided_attention(3, 6, 0.4) + >>> guided_attn_mask.shape + [6, 3] + >>> guided_attn_mask + tensor([[0.0000, 0.2934, 0.7506], + [0.0831, 0.0831, 0.5422], + [0.2934, 0.0000, 0.2934], + [0.5422, 0.0831, 0.0831], + [0.7506, 0.2934, 0.0000], + [0.8858, 0.5422, 0.0831]]) + + """ + grid_x, grid_y = paddle.meshgrid( + paddle.arange(olen), paddle.arange(ilen)) + grid_x = grid_x.cast(dtype=paddle.float32) + grid_y = grid_y.cast(dtype=paddle.float32) + return 1.0 - paddle.exp(-( + (grid_y / ilen - grid_x / olen)**2) / (2 * (sigma**2))) + + @staticmethod + def _make_masks(ilens, olens): + """Make masks indicating non-padded part. + + Args: + ilens(Tensor(int64) or List): Batch of lengths (B,). + olens(Tensor(int64) or List): Batch of lengths (B,). + + Returns: + Tensor: Mask tensor indicating non-padded part. + + Examples: + >>> ilens, olens = [5, 2], [8, 5] + >>> _make_mask(ilens, olens) + tensor([[[1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1], + [1, 1, 1, 1, 1]], + + [[1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [1, 1, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0]]], dtype=paddle.uint8) + + """ + # (B, T_in) + in_masks = make_non_pad_mask(ilens) + # (B, T_out) + out_masks = make_non_pad_mask(olens) + # (B, T_out, T_in) + + return paddle.logical_and( + out_masks.unsqueeze(-1), in_masks.unsqueeze(-2)) + + +class GuidedMultiHeadAttentionLoss(GuidedAttentionLoss): + """Guided attention loss function module for multi head attention. + + Args: + sigma (float, optional): Standard deviation to controlGuidedAttentionLoss + how close attention to a diagonal. + alpha (float, optional): Scaling coefficient (lambda). + reset_always (bool, optional): Whether to always reset masks. + + """ + + def forward(self, att_ws, ilens, olens): + """Calculate forward propagation. + + Args: + att_ws(Tensor): Batch of multi head attention weights (B, H, T_max_out, T_max_in). + ilens(Tensor): Batch of input lenghts (B,). + olens(Tensor): Batch of output lenghts (B,). + + Returns: + Tensor: Guided attention loss value. + + """ + if self.guided_attn_masks is None: + self.guided_attn_masks = ( + self._make_guided_attention_masks(ilens, olens).unsqueeze(1)) + if self.masks is None: + self.masks = self._make_masks(ilens, olens).unsqueeze(1) + losses = self.guided_attn_masks * att_ws + loss = paddle.mean( + losses.masked_select(self.masks.broadcast_to(losses.shape))) + if self.reset_always: + self._reset_masks() + + return self.alpha * loss + + +class Tacotron2Loss(nn.Layer): + """Loss function module for Tacotron2.""" + + def __init__(self, + use_masking=True, + use_weighted_masking=False, + bce_pos_weight=20.0): + """Initialize Tactoron2 loss module. + + Args: + use_masking (bool): Whether to apply masking for padded part in loss calculation. + use_weighted_masking (bool): Whether to apply weighted masking in loss calculation. + bce_pos_weight (float): Weight of positive sample of stop token. + """ + super().__init__() + assert (use_masking != use_weighted_masking) or not use_masking + self.use_masking = use_masking + self.use_weighted_masking = use_weighted_masking + + # define criterions + reduction = "none" if self.use_weighted_masking else "mean" + self.l1_criterion = nn.L1Loss(reduction=reduction) + self.mse_criterion = nn.MSELoss(reduction=reduction) + self.bce_criterion = nn.BCEWithLogitsLoss( + reduction=reduction, pos_weight=paddle.to_tensor(bce_pos_weight)) + + def forward(self, after_outs, before_outs, logits, ys, stop_labels, olens): + """Calculate forward propagation. + + Args: + after_outs(Tensor): Batch of outputs after postnets (B, Lmax, odim). + before_outs(Tensor): Batch of outputs before postnets (B, Lmax, odim). + logits(Tensor): Batch of stop logits (B, Lmax). + ys(Tensor): Batch of padded target features (B, Lmax, odim). + stop_labels(Tensor(int64)): Batch of the sequences of stop token labels (B, Lmax). + olens(Tensor(int64)): + + Returns: + Tensor: L1 loss value. + Tensor: Mean square error loss value. + Tensor: Binary cross entropy loss value. + """ + # make mask and apply it + if self.use_masking: + masks = make_non_pad_mask(olens).unsqueeze(-1) + ys = ys.masked_select(masks.broadcast_to(ys.shape)) + after_outs = after_outs.masked_select( + masks.broadcast_to(after_outs.shape)) + before_outs = before_outs.masked_select( + masks.broadcast_to(before_outs.shape)) + stop_labels = stop_labels.masked_select( + masks[:, :, 0].broadcast_to(stop_labels.shape)) + logits = logits.masked_select( + masks[:, :, 0].broadcast_to(logits.shape)) + + # calculate loss + l1_loss = self.l1_criterion(after_outs, ys) + self.l1_criterion( + before_outs, ys) + mse_loss = self.mse_criterion(after_outs, ys) + self.mse_criterion( + before_outs, ys) + bce_loss = self.bce_criterion(logits, stop_labels) + + # make weighted mask and apply it + if self.use_weighted_masking: + masks = make_non_pad_mask(olens).unsqueeze(-1) + weights = masks.float() / masks.sum(axis=1, keepdim=True).float() + out_weights = weights.divide( + paddle.shape(ys)[0] * paddle.shape(ys)[2]) + logit_weights = weights.divide(paddle.shape(ys)[0]) + + # apply weight + l1_loss = l1_loss.multiply(out_weights) + l1_loss = l1_loss.masked_select(masks.broadcast_to(l1_loss)).sum() + mse_loss = mse_loss.multiply(out_weights) + mse_loss = mse_loss.masked_select( + masks.broadcast_to(mse_loss)).sum() + bce_loss = bce_loss.multiply(logit_weights.squeeze(-1)) + bce_loss = bce_loss.masked_select( + masks.squeeze(-1).broadcast_to(bce_loss)).sum() + + return l1_loss, mse_loss, bce_loss + + +# Loss for Tacotron2 +def attention_guide(dec_lens, enc_lens, N, T, g, dtype=None): + """Build that W matrix. shape(B, T_dec, T_enc) + W[i, n, t] = 1 - exp(-(n/dec_lens[i] - t/enc_lens[i])**2 / (2g**2)) + + See also: + Tachibana, Hideyuki, Katsuya Uenoyama, and Shunsuke Aihara. 2017. “Efficiently Trainable Text-to-Speech System Based on Deep Convolutional Networks with Guided Attention.” ArXiv:1710.08969 [Cs, Eess], October. http://arxiv.org/abs/1710.08969. + """ + dtype = dtype or paddle.get_default_dtype() + dec_pos = paddle.arange(0, N).astype(dtype) / dec_lens.unsqueeze( + -1) # n/N # shape(B, T_dec) + enc_pos = paddle.arange(0, T).astype(dtype) / enc_lens.unsqueeze( + -1) # t/T # shape(B, T_enc) + W = 1 - paddle.exp(-(dec_pos.unsqueeze(-1) - enc_pos.unsqueeze(1))**2 / + (2 * g**2)) + + dec_mask = sequence_mask(dec_lens, maxlen=N) + enc_mask = sequence_mask(enc_lens, maxlen=T) + mask = dec_mask.unsqueeze(-1) * enc_mask.unsqueeze(1) + mask = paddle.cast(mask, W.dtype) + + W *= mask + return W + + +def guided_attention_loss(attention_weight, dec_lens, enc_lens, g): + """Guided attention loss, masked to excluded padding parts.""" + _, N, T = attention_weight.shape + W = attention_guide(dec_lens, enc_lens, N, T, g, attention_weight.dtype) + + total_tokens = (dec_lens * enc_lens).astype(W.dtype) + loss = paddle.mean(paddle.sum(W * attention_weight, [1, 2]) / total_tokens) + return loss + + +# Losses for GAN Vocoder +def stft(x, + fft_size, + hop_length=None, + win_length=None, + window='hann', + center=True, + pad_mode='reflect'): + """Perform STFT and convert to magnitude spectrogram. + Args: + x(Tensor): Input signal tensor (B, T). + fft_size(int): FFT size. + hop_size(int): Hop size. + win_length(int, optional): window : str, optional (Default value = None) + window(str, optional): Name of window function, see `scipy.signal.get_window` for more + details. Defaults to "hann". + center(bool, optional, optional): center (bool, optional): Whether to pad `x` to make that the + :math:`t \times hop\\_length` at the center of :math:`t`-th frame. Default: `True`. + pad_mode(str, optional, optional): (Default value = 'reflect') + hop_length: (Default value = None) + + Returns: + Tensor: Magnitude spectrogram (B, #frames, fft_size // 2 + 1). + """ + # calculate window + window = signal.get_window(window, win_length, fftbins=True) + window = paddle.to_tensor(window, dtype=x.dtype) + x_stft = paddle.signal.stft( + x, + fft_size, + hop_length, + win_length, + window=window, + center=center, + pad_mode=pad_mode) + + real = x_stft.real() + imag = x_stft.imag() + + return paddle.sqrt(paddle.clip(real**2 + imag**2, min=1e-7)).transpose( + [0, 2, 1]) + + +class SpectralConvergenceLoss(nn.Layer): + """Spectral convergence loss module.""" + + def __init__(self): + """Initilize spectral convergence loss module.""" + super().__init__() + + def forward(self, x_mag, y_mag): + """Calculate forward propagation. + Args: + x_mag (Tensor): Magnitude spectrogram of predicted signal (B, #frames, #freq_bins). + y_mag (Tensor): Magnitude spectrogram of groundtruth signal (B, #frames, #freq_bins). + Returns: + Tensor: Spectral convergence loss value. + """ + return paddle.norm( + y_mag - x_mag, p="fro") / paddle.clip( + paddle.norm(y_mag, p="fro"), min=1e-10) + + +class LogSTFTMagnitudeLoss(nn.Layer): + """Log STFT magnitude loss module.""" + + def __init__(self, epsilon=1e-7): + """Initilize los STFT magnitude loss module.""" + super().__init__() + self.epsilon = epsilon + + def forward(self, x_mag, y_mag): + """Calculate forward propagation. + Args: + x_mag (Tensor): Magnitude spectrogram of predicted signal (B, #frames, #freq_bins). + y_mag (Tensor): Magnitude spectrogram of groundtruth signal (B, #frames, #freq_bins). + Returns: + Tensor: Log STFT magnitude loss value. + """ + return F.l1_loss( + paddle.log(paddle.clip(y_mag, min=self.epsilon)), + paddle.log(paddle.clip(x_mag, min=self.epsilon))) + + +class STFTLoss(nn.Layer): + """STFT loss module.""" + + def __init__(self, + fft_size=1024, + shift_size=120, + win_length=600, + window="hann"): + """Initialize STFT loss module.""" + super().__init__() + self.fft_size = fft_size + self.shift_size = shift_size + self.win_length = win_length + self.window = window + self.spectral_convergence_loss = SpectralConvergenceLoss() + self.log_stft_magnitude_loss = LogSTFTMagnitudeLoss() + + def forward(self, x, y): + """Calculate forward propagation. + Args: + x (Tensor): Predicted signal (B, T). + y (Tensor): Groundtruth signal (B, T). + Returns: + Tensor: Spectral convergence loss value. + Tensor: Log STFT magnitude loss value. + """ + x_mag = stft(x, self.fft_size, self.shift_size, self.win_length, + self.window) + y_mag = stft(y, self.fft_size, self.shift_size, self.win_length, + self.window) + sc_loss = self.spectral_convergence_loss(x_mag, y_mag) + mag_loss = self.log_stft_magnitude_loss(x_mag, y_mag) + + return sc_loss, mag_loss + + +class MultiResolutionSTFTLoss(nn.Layer): + """Multi resolution STFT loss module.""" + + def __init__( + self, + fft_sizes=[1024, 2048, 512], + hop_sizes=[120, 240, 50], + win_lengths=[600, 1200, 240], + window="hann", ): + """Initialize Multi resolution STFT loss module. + Args: + fft_sizes (list): List of FFT sizes. + hop_sizes (list): List of hop sizes. + win_lengths (list): List of window lengths. + window (str): Window function type. + """ + super().__init__() + assert len(fft_sizes) == len(hop_sizes) == len(win_lengths) + self.stft_losses = nn.LayerList() + for fs, ss, wl in zip(fft_sizes, hop_sizes, win_lengths): + self.stft_losses.append(STFTLoss(fs, ss, wl, window)) + + def forward(self, x, y): + """Calculate forward propagation. + + Args: + x (Tensor): Predicted signal (B, T) or (B, #subband, T). + y (Tensor): Groundtruth signal (B, T) or (B, #subband, T). + Returns: + Tensor: Multi resolution spectral convergence loss value. + Tensor: Multi resolution log STFT magnitude loss value. + """ + if len(x.shape) == 3: + # (B, C, T) -> (B x C, T) + x = x.reshape([-1, x.shape[2]]) + # (B, C, T) -> (B x C, T) + y = y.reshape([-1, y.shape[2]]) + sc_loss = 0.0 + mag_loss = 0.0 + for f in self.stft_losses: + sc_l, mag_l = f(x, y) + sc_loss += sc_l + mag_loss += mag_l + sc_loss /= len(self.stft_losses) + mag_loss /= len(self.stft_losses) + + return sc_loss, mag_loss + + +class GeneratorAdversarialLoss(nn.Layer): + """Generator adversarial loss module.""" + + def __init__( + self, + average_by_discriminators=True, + loss_type="mse", ): + """Initialize GeneratorAversarialLoss module.""" + super().__init__() + self.average_by_discriminators = average_by_discriminators + assert loss_type in ["mse", "hinge"], f"{loss_type} is not supported." + if loss_type == "mse": + self.criterion = self._mse_loss + else: + self.criterion = self._hinge_loss + + def forward(self, outputs): + """Calcualate generator adversarial loss. + Args: + outputs (Tensor or List): Discriminator outputs or list of discriminator outputs. + Returns: + Tensor: Generator adversarial loss value. + """ + if isinstance(outputs, (tuple, list)): + adv_loss = 0.0 + for i, outputs_ in enumerate(outputs): + if isinstance(outputs_, (tuple, list)): + # case including feature maps + outputs_ = outputs_[-1] + adv_loss += self.criterion(outputs_) + if self.average_by_discriminators: + adv_loss /= i + 1 + else: + adv_loss = self.criterion(outputs) + + return adv_loss + + def _mse_loss(self, x): + return F.mse_loss(x, paddle.ones_like(x)) + + def _hinge_loss(self, x): + return -x.mean() + + +class DiscriminatorAdversarialLoss(nn.Layer): + """Discriminator adversarial loss module.""" + + def __init__( + self, + average_by_discriminators=True, + loss_type="mse", ): + """Initialize DiscriminatorAversarialLoss module.""" + super().__init__() + self.average_by_discriminators = average_by_discriminators + assert loss_type in ["mse"], f"{loss_type} is not supported." + if loss_type == "mse": + self.fake_criterion = self._mse_fake_loss + self.real_criterion = self._mse_real_loss + + def forward(self, outputs_hat, outputs): + """Calcualate discriminator adversarial loss. + + Args: + outputs_hat (Tensor or list): Discriminator outputs or list of + discriminator outputs calculated from generator outputs. + outputs (Tensor or list): Discriminator outputs or list of + discriminator outputs calculated from groundtruth. + Returns: + Tensor: Discriminator real loss value. + Tensor: Discriminator fake loss value. + """ + if isinstance(outputs, (tuple, list)): + real_loss = 0.0 + fake_loss = 0.0 + for i, (outputs_hat_, + outputs_) in enumerate(zip(outputs_hat, outputs)): + if isinstance(outputs_hat_, (tuple, list)): + # case including feature maps + outputs_hat_ = outputs_hat_[-1] + outputs_ = outputs_[-1] + real_loss += self.real_criterion(outputs_) + fake_loss += self.fake_criterion(outputs_hat_) + if self.average_by_discriminators: + fake_loss /= i + 1 + real_loss /= i + 1 + else: + real_loss = self.real_criterion(outputs) + fake_loss = self.fake_criterion(outputs_hat) + + return real_loss, fake_loss + + def _mse_real_loss(self, x): + return F.mse_loss(x, paddle.ones_like(x)) + + def _mse_fake_loss(self, x): + return F.mse_loss(x, paddle.zeros_like(x)) + + +# Losses for SpeedySpeech +# Structural Similarity Index Measure (SSIM) +def gaussian(window_size, sigma): + gauss = paddle.to_tensor([ + math.exp(-(x - window_size // 2)**2 / float(2 * sigma**2)) + for x in range(window_size) + ]) + return gauss / gauss.sum() + + +def create_window(window_size, channel): + _1D_window = gaussian(window_size, 1.5).unsqueeze(1) + _2D_window = paddle.matmul(_1D_window, paddle.transpose( + _1D_window, [1, 0])).unsqueeze([0, 1]) + window = paddle.expand(_2D_window, [channel, 1, window_size, window_size]) + return window + + +def _ssim(img1, img2, window, window_size, channel, size_average=True): + mu1 = F.conv2d(img1, window, padding=window_size // 2, groups=channel) + mu2 = F.conv2d(img2, window, padding=window_size // 2, groups=channel) + + mu1_sq = mu1.pow(2) + mu2_sq = mu2.pow(2) + mu1_mu2 = mu1 * mu2 + + sigma1_sq = F.conv2d( + img1 * img1, window, padding=window_size // 2, groups=channel) - mu1_sq + sigma2_sq = F.conv2d( + img2 * img2, window, padding=window_size // 2, groups=channel) - mu2_sq + sigma12 = F.conv2d( + img1 * img2, window, padding=window_size // 2, groups=channel) - mu1_mu2 + + C1 = 0.01**2 + C2 = 0.03**2 + + ssim_map = ((2 * mu1_mu2 + C1) * (2 * sigma12 + C2)) \ + / ((mu1_sq + mu2_sq + C1) * (sigma1_sq + sigma2_sq + C2)) + + if size_average: + return ssim_map.mean() + else: + return ssim_map.mean(1).mean(1).mean(1) + + +def ssim(img1, img2, window_size=11, size_average=True): + (_, channel, _, _) = img1.shape + window = create_window(window_size, channel) + return _ssim(img1, img2, window, window_size, channel, size_average) + + +def weighted_mean(input, weight): + """Weighted mean. It can also be used as masked mean. + + Args: + input(Tensor): The input tensor. + weight(Tensor): The weight tensor with broadcastable shape with the input. + + Returns: + Tensor: Weighted mean tensor with the same dtype as input. shape=(1,) + + """ + weight = paddle.cast(weight, input.dtype) + # paddle.Tensor.size is different with torch.size() and has been overrided in s2t.__init__ + broadcast_ratio = input.numel() / weight.numel() + return paddle.sum(input * weight) / (paddle.sum(weight) * broadcast_ratio) + + +def masked_l1_loss(prediction, target, mask): + """Compute maksed L1 loss. + + Args: + prediction(Tensor): The prediction. + target(Tensor): The target. The shape should be broadcastable to ``prediction``. + mask(Tensor): The mask. The shape should be broadcatable to the broadcasted shape of + ``prediction`` and ``target``. + + Returns: + Tensor: The masked L1 loss. shape=(1,) + + """ + abs_error = F.l1_loss(prediction, target, reduction='none') + loss = weighted_mean(abs_error, mask) + return loss + + +class MelSpectrogram(nn.Layer): + """Calculate Mel-spectrogram.""" + + def __init__( + self, + fs=22050, + fft_size=1024, + hop_size=256, + win_length=None, + window="hann", + num_mels=80, + fmin=80, + fmax=7600, + center=True, + normalized=False, + onesided=True, + eps=1e-10, + log_base=10.0, ): + """Initialize MelSpectrogram module.""" + super().__init__() + self.fft_size = fft_size + if win_length is None: + self.win_length = fft_size + else: + self.win_length = win_length + self.hop_size = hop_size + self.center = center + self.normalized = normalized + self.onesided = onesided + + if window is not None and not hasattr(signal.windows, f"{window}"): + raise ValueError(f"{window} window is not implemented") + self.window = window + self.eps = eps + + fmin = 0 if fmin is None else fmin + fmax = fs / 2 if fmax is None else fmax + melmat = librosa.filters.mel( + sr=fs, + n_fft=fft_size, + n_mels=num_mels, + fmin=fmin, + fmax=fmax, ) + + self.melmat = paddle.to_tensor(melmat.T) + self.stft_params = { + "n_fft": self.fft_size, + "win_length": self.win_length, + "hop_length": self.hop_size, + "center": self.center, + "normalized": self.normalized, + "onesided": self.onesided, + } + + self.log_base = log_base + if self.log_base is None: + self.log = paddle.log + elif self.log_base == 2.0: + self.log = paddle.log2 + elif self.log_base == 10.0: + self.log = paddle.log10 + else: + raise ValueError(f"log_base: {log_base} is not supported.") + + def forward(self, x): + """Calculate Mel-spectrogram. + Args: + + x (Tensor): Input waveform tensor (B, T) or (B, 1, T). + Returns: + Tensor: Mel-spectrogram (B, #mels, #frames). + """ + if len(x.shape) == 3: + # (B, C, T) -> (B*C, T) + x = x.reshape([-1, paddle.shape(x)[2]]) + + if self.window is not None: + # calculate window + window = signal.get_window( + self.window, self.win_length, fftbins=True) + window = paddle.to_tensor(window, dtype=x.dtype) + else: + window = None + + x_stft = paddle.signal.stft(x, window=window, **self.stft_params) + real = x_stft.real() + imag = x_stft.imag() + # (B, #freqs, #frames) -> (B, $frames, #freqs) + real = real.transpose([0, 2, 1]) + imag = imag.transpose([0, 2, 1]) + x_power = real**2 + imag**2 + x_amp = paddle.sqrt(paddle.clip(x_power, min=self.eps)) + x_mel = paddle.matmul(x_amp, self.melmat) + x_mel = paddle.clip(x_mel, min=self.eps) + + return self.log(x_mel).transpose([0, 2, 1]) + + +class MelSpectrogramLoss(nn.Layer): + """Mel-spectrogram loss.""" + + def __init__( + self, + fs=22050, + fft_size=1024, + hop_size=256, + win_length=None, + window="hann", + num_mels=80, + fmin=80, + fmax=7600, + center=True, + normalized=False, + onesided=True, + eps=1e-10, + log_base=10.0, ): + """Initialize Mel-spectrogram loss.""" + super().__init__() + self.mel_spectrogram = MelSpectrogram( + fs=fs, + fft_size=fft_size, + hop_size=hop_size, + win_length=win_length, + window=window, + num_mels=num_mels, + fmin=fmin, + fmax=fmax, + center=center, + normalized=normalized, + onesided=onesided, + eps=eps, + log_base=log_base, ) + + def forward(self, y_hat, y): + """Calculate Mel-spectrogram loss. + Args: + y_hat(Tensor): Generated single tensor (B, 1, T). + y(Tensor): Groundtruth single tensor (B, 1, T). + + Returns: + Tensor: Mel-spectrogram loss value. + """ + mel_hat = self.mel_spectrogram(y_hat) + mel = self.mel_spectrogram(y) + mel_loss = F.l1_loss(mel_hat, mel) + + return mel_loss + + +class FeatureMatchLoss(nn.Layer): + """Feature matching loss module.""" + + def __init__( + self, + average_by_layers=True, + average_by_discriminators=True, + include_final_outputs=False, ): + """Initialize FeatureMatchLoss module.""" + super().__init__() + self.average_by_layers = average_by_layers + self.average_by_discriminators = average_by_discriminators + self.include_final_outputs = include_final_outputs + + def forward(self, feats_hat, feats): + """Calcualate feature matching loss. + + Args: + feats_hat(list): List of list of discriminator outputs + calcuated from generater outputs. + feats(list): List of list of discriminator outputs + + Returns: + Tensor: Feature matching loss value. + + """ + feat_match_loss = 0.0 + for i, (feats_hat_, feats_) in enumerate(zip(feats_hat, feats)): + feat_match_loss_ = 0.0 + if not self.include_final_outputs: + feats_hat_ = feats_hat_[:-1] + feats_ = feats_[:-1] + for j, (feat_hat_, feat_) in enumerate(zip(feats_hat_, feats_)): + feat_match_loss_ += F.l1_loss(feat_hat_, feat_.detach()) + if self.average_by_layers: + feat_match_loss_ /= j + 1 + feat_match_loss += feat_match_loss_ + if self.average_by_discriminators: + feat_match_loss /= i + 1 + + return feat_match_loss diff --git a/ernie-sat/paddlespeech/t2s/modules/masked_fill.py b/ernie-sat/paddlespeech/t2s/modules/masked_fill.py new file mode 100644 index 0000000..b322225 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/masked_fill.py @@ -0,0 +1,49 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Union + +import paddle + + +def is_broadcastable(shp1, shp2): + for a, b in zip(shp1[::-1], shp2[::-1]): + if a == 1 or b == 1 or a == b: + pass + else: + return False + return True + + +# assume that len(shp1) == len(shp2) +def broadcast_shape(shp1, shp2): + result = [] + for a, b in zip(shp1[::-1], shp2[::-1]): + result.append(max(a, b)) + return result[::-1] + + +def masked_fill(xs: paddle.Tensor, + mask: paddle.Tensor, + value: Union[float, int]): + # comment following line for converting dygraph to static graph. + # assert is_broadcastable(xs.shape, mask.shape) is True + # bshape = paddle.broadcast_shape(xs.shape, mask.shape) + bshape = broadcast_shape(xs.shape, mask.shape) + mask.stop_gradient = True + mask = mask.broadcast_to(bshape) + + trues = paddle.ones_like(xs) * value + mask = mask.cast(dtype=paddle.bool) + xs = paddle.where(mask, trues, xs) + return xs diff --git a/ernie-sat/paddlespeech/t2s/modules/nets_utils.py b/ernie-sat/paddlespeech/t2s/modules/nets_utils.py new file mode 100644 index 0000000..4207d31 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/nets_utils.py @@ -0,0 +1,131 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +import paddle +from paddle import nn +from typeguard import check_argument_types + + +def pad_list(xs, pad_value): + """Perform padding for the list of tensors. + + Args: + xs (List[Tensor]): List of Tensors [(T_1, `*`), (T_2, `*`), ..., (T_B, `*`)]. + pad_value (float): Value for padding. + + Returns: + Tensor: Padded tensor (B, Tmax, `*`). + + Examples: + >>> x = [paddle.ones([4]), paddle.ones([2]), paddle.ones([1])] + >>> x + [tensor([1., 1., 1., 1.]), tensor([1., 1.]), tensor([1.])] + >>> pad_list(x, 0) + tensor([[1., 1., 1., 1.], + [1., 1., 0., 0.], + [1., 0., 0., 0.]]) + """ + n_batch = len(xs) + max_len = max(x.shape[0] for x in xs) + pad = paddle.full([n_batch, max_len, *xs[0].shape[1:]], pad_value) + + for i in range(n_batch): + pad[i, :xs[i].shape[0]] = xs[i] + + return pad + + +def make_pad_mask(lengths, length_dim=-1): + """Make mask tensor containing indices of padded part. + + Args: + lengths (Tensor(int64)): Batch of lengths (B,). + + Returns: + Tensor(bool): Mask tensor containing indices of padded part bool. + + Examples: + With only lengths. + + >>> lengths = [5, 3, 2] + >>> make_non_pad_mask(lengths) + masks = [[0, 0, 0, 0 ,0], + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]] + """ + if length_dim == 0: + raise ValueError("length_dim cannot be 0: {}".format(length_dim)) + + bs = paddle.shape(lengths)[0] + maxlen = lengths.max() + seq_range = paddle.arange(0, maxlen, dtype=paddle.int64) + seq_range_expand = seq_range.unsqueeze(0).expand([bs, maxlen]) + seq_length_expand = lengths.unsqueeze(-1) + mask = seq_range_expand >= seq_length_expand + + return mask + + +def make_non_pad_mask(lengths, length_dim=-1): + """Make mask tensor containing indices of non-padded part. + + Args: + lengths (Tensor(int64) or List): Batch of lengths (B,). + xs (Tensor, optional): The reference tensor. + If set, masks will be the same shape as this tensor. + length_dim (int, optional): Dimension indicator of the above tensor. + See the example. + + Returns: + Tensor(bool): mask tensor containing indices of padded part bool. + + Examples: + With only lengths. + + >>> lengths = [5, 3, 2] + >>> make_non_pad_mask(lengths) + masks = [[1, 1, 1, 1 ,1], + [1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]] + """ + return paddle.logical_not(make_pad_mask(lengths, length_dim)) + + +def initialize(model: nn.Layer, init: str): + """Initialize weights of a neural network module. + + Parameters are initialized using the given method or distribution. + + Custom initialization routines can be implemented into submodules + + Args: + model (nn.Layer): Target. + init (str): Method of initialization. + """ + assert check_argument_types() + + if init == "xavier_uniform": + nn.initializer.set_global_initializer(nn.initializer.XavierUniform(), + nn.initializer.Constant()) + elif init == "xavier_normal": + nn.initializer.set_global_initializer(nn.initializer.XavierNormal(), + nn.initializer.Constant()) + elif init == "kaiming_uniform": + nn.initializer.set_global_initializer(nn.initializer.KaimingUniform(), + nn.initializer.Constant()) + elif init == "kaiming_normal": + nn.initializer.set_global_initializer(nn.initializer.KaimingNormal(), + nn.initializer.Constant()) + else: + raise ValueError("Unknown initialization: " + init) diff --git a/ernie-sat/paddlespeech/t2s/modules/normalizer.py b/ernie-sat/paddlespeech/t2s/modules/normalizer.py new file mode 100644 index 0000000..a4fc598 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/normalizer.py @@ -0,0 +1,33 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + + +class ZScore(nn.Layer): + # feature last + def __init__(self, mu, sigma): + super().__init__() + self.register_buffer("mu", mu) + self.register_buffer("sigma", sigma) + + def forward(self, x): + # NOTE: to be compatible with paddle's to_static, we must explicitly + # call multiply, or add, etc, instead of +-*/, etc. + return paddle.divide(paddle.subtract(x, self.mu), self.sigma) + + def inverse(self, x): + # NOTE: to be compatible with paddle's to_static, we must explicitly + # call multiply, or add, etc, instead of +-*/, etc. + return paddle.add(paddle.multiply(x, self.sigma), self.mu) diff --git a/ernie-sat/paddlespeech/t2s/modules/positional_encoding.py b/ernie-sat/paddlespeech/t2s/modules/positional_encoding.py new file mode 100644 index 0000000..715c576 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/positional_encoding.py @@ -0,0 +1,67 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import Tensor + +__all__ = ["sinusoid_position_encoding", "scaled_position_encoding"] + + +def sinusoid_position_encoding(num_positions: int, + feature_size: int, + omega: float=1.0, + start_pos: int=0, + dtype=None) -> paddle.Tensor: + # return tensor shape (num_positions, feature_size) + # NOTE: to be compatible with paddle's to_static, we cannnot raise + # an exception here, take care of it by yourself + # if (feature_size % 2 != 0): + # raise ValueError("size should be divisible by 2") + dtype = dtype or paddle.get_default_dtype() + + channel = paddle.arange(0, feature_size, 2, dtype=dtype) + index = paddle.arange(start_pos, start_pos + num_positions, 1, dtype=dtype) + denominator = channel / float(feature_size) + denominator = paddle.to_tensor([10000.0], dtype='float32')**denominator + p = (paddle.unsqueeze(index, -1) * omega) / denominator + encodings = paddle.zeros([num_positions, feature_size], dtype=dtype) + encodings[:, 0::2] = paddle.sin(p) + encodings[:, 1::2] = paddle.cos(p) + return encodings + + +def scaled_position_encoding(num_positions: int, + feature_size: int, + omega: Tensor, + start_pos: int=0, + dtype=None) -> Tensor: + # omega: Tensor (batch_size, ) + # return tensor shape (batch_size, num_positions, feature_size) + # consider renaming this as batched positioning encoding + if (feature_size % 2 != 0): + raise ValueError("size should be divisible by 2") + dtype = dtype or paddle.get_default_dtype() + + channel = paddle.arange(0, feature_size, 2, dtype=dtype) + index = paddle.arange( + start_pos, start_pos + num_positions, 1, dtype=omega.dtype) + batch_size = omega.shape[0] + omega = paddle.unsqueeze(omega, [1, 2]) + p = (paddle.unsqueeze(index, -1) * + omega) / (10000.0**(channel / float(feature_size))) + encodings = paddle.zeros( + [batch_size, num_positions, feature_size], dtype=dtype) + # it is nice to have fancy indexing and inplace operations + encodings[:, :, 0::2] = paddle.sin(p) + encodings[:, :, 1::2] = paddle.cos(p) + return encodings diff --git a/ernie-sat/paddlespeech/t2s/modules/pqmf.py b/ernie-sat/paddlespeech/t2s/modules/pqmf.py new file mode 100644 index 0000000..9860da9 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/pqmf.py @@ -0,0 +1,127 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Pseudo QMF modules.""" +import numpy as np +import paddle +import paddle.nn.functional as F +from paddle import nn +from scipy.signal import kaiser + + +def design_prototype_filter(taps=62, cutoff_ratio=0.142, beta=9.0): + """Design prototype filter for PQMF. + This method is based on `A Kaiser window approach for the design of prototype + filters of cosine modulated filterbanks`_. + + Args: + taps (int): The number of filter taps. + cutoff_ratio (float): Cut-off frequency ratio. + beta (float): Beta coefficient for kaiser window. + Returns: + ndarray: + Impluse response of prototype filter (taps + 1,). + .. _`A Kaiser window approach for the design of prototype filters of cosine modulated filterbanks`: + https://ieeexplore.ieee.org/abstract/document/681427 + """ + # check the arguments are valid + assert taps % 2 == 0, "The number of taps mush be even number." + assert 0.0 < cutoff_ratio < 1.0, "Cutoff ratio must be > 0.0 and < 1.0." + # make initial filter + omega_c = np.pi * cutoff_ratio + with np.errstate(invalid="ignore"): + h_i = np.sin(omega_c * (np.arange(taps + 1) - 0.5 * taps)) / ( + np.pi * (np.arange(taps + 1) - 0.5 * taps)) + h_i[taps // + 2] = np.cos(0) * cutoff_ratio # fix nan due to indeterminate form + + # apply kaiser window + w = kaiser(taps + 1, beta) + h = h_i * w + + return h + + +class PQMF(nn.Layer): + """PQMF module. + This module is based on `Near-perfect-reconstruction pseudo-QMF banks`_. + .. _`Near-perfect-reconstruction pseudo-QMF banks`: + https://ieeexplore.ieee.org/document/258122 + """ + + def __init__(self, subbands=4, taps=62, cutoff_ratio=0.142, beta=9.0): + """Initilize PQMF module. + The cutoff_ratio and beta parameters are optimized for #subbands = 4. + See dicussion in https://github.com/kan-bayashi/ParallelWaveGAN/issues/195. + + Args: + subbands (int): The number of subbands. + taps (int): The number of filter taps. + cutoff_ratio (float): Cut-off frequency ratio. + beta (float): Beta coefficient for kaiser window. + """ + super().__init__() + + h_proto = design_prototype_filter(taps, cutoff_ratio, beta) + h_analysis = np.zeros((subbands, len(h_proto))) + h_synthesis = np.zeros((subbands, len(h_proto))) + for k in range(subbands): + h_analysis[k] = ( + 2 * h_proto * np.cos((2 * k + 1) * (np.pi / (2 * subbands)) * ( + np.arange(taps + 1) - (taps / 2)) + (-1)**k * np.pi / 4)) + h_synthesis[k] = ( + 2 * h_proto * np.cos((2 * k + 1) * (np.pi / (2 * subbands)) * ( + np.arange(taps + 1) - (taps / 2)) - (-1)**k * np.pi / 4)) + + # convert to tensor + self.analysis_filter = paddle.to_tensor( + h_analysis, dtype="float32").unsqueeze(1) + self.synthesis_filter = paddle.to_tensor( + h_synthesis, dtype="float32").unsqueeze(0) + + # filter for downsampling & upsampling + updown_filter = paddle.zeros( + (subbands, subbands, subbands), dtype="float32") + for k in range(subbands): + updown_filter[k, k, 0] = 1.0 + self.updown_filter = updown_filter + self.subbands = subbands + # keep padding info + self.pad_fn = nn.Pad1D(taps // 2, mode='constant', value=0.0) + + def analysis(self, x): + """Analysis with PQMF. + Args: + x (Tensor): Input tensor (B, 1, T). + Returns: + Tensor: Output tensor (B, subbands, T // subbands). + """ + x = F.conv1d(self.pad_fn(x), self.analysis_filter) + return F.conv1d(x, self.updown_filter, stride=self.subbands) + + def synthesis(self, x): + """Synthesis with PQMF. + Args: + x (Tensor): Input tensor (B, subbands, T // subbands). + Returns: + Tensor: Output tensor (B, 1, T). + """ + x = F.conv1d_transpose( + x, self.updown_filter * self.subbands, stride=self.subbands) + + return F.conv1d(self.pad_fn(x), self.synthesis_filter) + + # when converting dygraph to static graph, can not use self.pqmf.synthesis directly + def forward(self, x): + return self.synthesis(x) diff --git a/ernie-sat/paddlespeech/t2s/modules/predictor/__init__.py b/ernie-sat/paddlespeech/t2s/modules/predictor/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/predictor/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/modules/predictor/duration_predictor.py b/ernie-sat/paddlespeech/t2s/modules/predictor/duration_predictor.py new file mode 100644 index 0000000..33ed575 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/predictor/duration_predictor.py @@ -0,0 +1,156 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Duration predictor related modules.""" +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.layer_norm import LayerNorm +from paddlespeech.t2s.modules.masked_fill import masked_fill + + +class DurationPredictor(nn.Layer): + """Duration predictor module. + + This is a module of duration predictor described + in `FastSpeech: Fast, Robust and Controllable Text to Speech`_. + The duration predictor predicts a duration of each frame in log domain + from the hidden embeddings of encoder. + + .. _`FastSpeech: Fast, Robust and Controllable Text to Speech`: + https://arxiv.org/pdf/1905.09263.pdf + + Note + ---------- + The calculation domain of outputs is different + between in `forward` and in `inference`. In `forward`, + the outputs are calculated in log domain but in `inference`, + those are calculated in linear domain. + + """ + + def __init__(self, + idim, + n_layers=2, + n_chans=384, + kernel_size=3, + dropout_rate=0.1, + offset=1.0): + """Initilize duration predictor module. + + Args: + idim (int):Input dimension. + n_layers (int, optional): Number of convolutional layers. + n_chans (int, optional): Number of channels of convolutional layers. + kernel_size (int, optional): Kernel size of convolutional layers. + dropout_rate (float, optional): Dropout rate. + offset (float, optional): Offset value to avoid nan in log domain. + + """ + super().__init__() + self.offset = offset + self.conv = nn.LayerList() + for idx in range(n_layers): + in_chans = idim if idx == 0 else n_chans + self.conv.append( + nn.Sequential( + nn.Conv1D( + in_chans, + n_chans, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, ), + nn.ReLU(), + LayerNorm(n_chans, dim=1), + nn.Dropout(dropout_rate), )) + self.linear = nn.Linear(n_chans, 1, bias_attr=True) + + def _forward(self, xs, x_masks=None, is_inference=False): + # (B, idim, Tmax) + xs = xs.transpose([0, 2, 1]) + # (B, C, Tmax) + for f in self.conv: + xs = f(xs) + + # NOTE: calculate in log domain + # (B, Tmax) + xs = self.linear(xs.transpose([0, 2, 1])).squeeze(-1) + + if is_inference: + # NOTE: calculate in linear domain + xs = paddle.clip(paddle.round(xs.exp() - self.offset), min=0) + + if x_masks is not None: + xs = masked_fill(xs, x_masks, 0.0) + + return xs + + def forward(self, xs, x_masks=None): + """Calculate forward propagation. + Args: + xs(Tensor): Batch of input sequences (B, Tmax, idim). + x_masks(ByteTensor, optional, optional): Batch of masks indicating padded part (B, Tmax). (Default value = None) + + Returns: + Tensor: Batch of predicted durations in log domain (B, Tmax). + """ + return self._forward(xs, x_masks, False) + + def inference(self, xs, x_masks=None): + """Inference duration. + Args: + xs(Tensor): Batch of input sequences (B, Tmax, idim). + x_masks(Tensor(bool), optional, optional): Batch of masks indicating padded part (B, Tmax). (Default value = None) + + Returns: + Tensor: Batch of predicted durations in linear domain int64 (B, Tmax). + """ + return self._forward(xs, x_masks, True) + + +class DurationPredictorLoss(nn.Layer): + """Loss function module for duration predictor. + + The loss value is Calculated in log domain to make it Gaussian. + + """ + + def __init__(self, offset=1.0, reduction="mean"): + """Initilize duration predictor loss module. + Args: + offset (float, optional): Offset value to avoid nan in log domain. + reduction (str): Reduction type in loss calculation. + """ + super().__init__() + self.criterion = nn.MSELoss(reduction=reduction) + self.offset = offset + + def forward(self, outputs, targets): + """Calculate forward propagation. + + Args: + outputs(Tensor): Batch of prediction durations in log domain (B, T) + targets(Tensor): Batch of groundtruth durations in linear domain (B, T) + + Returns: + Tensor: Mean squared error loss value. + + Note: + `outputs` is in log domain but `targets` is in linear domain. + """ + # NOTE: outputs is in log domain while targets in linear + targets = paddle.log(targets.cast(dtype='float32') + self.offset) + loss = self.criterion(outputs, targets) + + return loss diff --git a/ernie-sat/paddlespeech/t2s/modules/predictor/length_regulator.py b/ernie-sat/paddlespeech/t2s/modules/predictor/length_regulator.py new file mode 100644 index 0000000..be788e6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/predictor/length_regulator.py @@ -0,0 +1,123 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Length regulator related modules.""" +import numpy as np +import paddle +from paddle import nn + + +class LengthRegulator(nn.Layer): + """Length regulator module for feed-forward Transformer. + + This is a module of length regulator described in + `FastSpeech: Fast, Robust and Controllable Text to Speech`_. + The length regulator expands char or + phoneme-level embedding features to frame-level by repeating each + feature based on the corresponding predicted durations. + + .. _`FastSpeech: Fast, Robust and Controllable Text to Speech`: + https://arxiv.org/pdf/1905.09263.pdf + + """ + + def __init__(self, pad_value=0.0): + """Initilize length regulator module. + + Args: + pad_value (float, optional): Value used for padding. + + """ + super().__init__() + self.pad_value = pad_value + + # expand_numpy is faster than expand + def expand_numpy(self, encodings: paddle.Tensor, + durations: paddle.Tensor) -> paddle.Tensor: + """ + encodings: (B, T, C) + durations: (B, T) + """ + batch_size, t_enc = durations.shape + durations = durations.numpy() + slens = np.sum(durations, -1) + t_dec = np.max(slens) + M = np.zeros([batch_size, t_dec, t_enc]) + for i in range(batch_size): + k = 0 + for j in range(t_enc): + d = durations[i, j] + M[i, k:k + d, j] = 1 + k += d + M = paddle.to_tensor(M, dtype=encodings.dtype) + encodings = paddle.matmul(M, encodings) + return encodings + + def expand(self, encodings: paddle.Tensor, + durations: paddle.Tensor) -> paddle.Tensor: + """ + encodings: (B, T, C) + durations: (B, T) + """ + batch_size, t_enc = paddle.shape(durations) + slens = paddle.sum(durations, -1) + t_dec = paddle.max(slens) + t_dec_1 = t_dec + 1 + flatten_duration = paddle.cumsum( + paddle.reshape(durations, [batch_size * t_enc])) + 1 + init = paddle.zeros(t_dec_1) + m_batch = batch_size * t_enc + M = paddle.zeros([t_dec_1, m_batch]) + for i in range(m_batch): + d = flatten_duration[i] + m = paddle.concat( + [paddle.ones(d), paddle.zeros(t_dec_1 - d)], axis=0) + M[:, i] = m - init + init = m + M = paddle.reshape(M, shape=[t_dec_1, batch_size, t_enc]) + M = M[1:, :, :] + M = paddle.transpose(M, (1, 0, 2)) + encodings = paddle.matmul(M, encodings) + return encodings + + def forward(self, xs, ds, alpha=1.0, is_inference=False): + """Calculate forward propagation. + + Args: + xs (Tensor): Batch of sequences of char or phoneme embeddings (B, Tmax, D). + ds (Tensor(int64)): Batch of durations of each frame (B, T). + alpha (float, optional): Alpha value to control speed of speech. + + Returns: + Tensor: replicated input tensor based on durations (B, T*, D). + """ + + if alpha != 1.0: + assert alpha > 0 + ds = paddle.round(ds.cast(dtype=paddle.float32) * alpha) + ds = ds.cast(dtype=paddle.int64) + ''' + from distutils.version import LooseVersion + from paddlespeech.t2s.modules.nets_utils import pad_list + # 这里在 paddle 2.2.2 的动转静是不通的 + # if LooseVersion(paddle.__version__) >= "2.3.0" or hasattr(paddle, 'repeat_interleave'): + # if LooseVersion(paddle.__version__) >= "2.3.0": + if hasattr(paddle, 'repeat_interleave'): + repeat = [paddle.repeat_interleave(x, d, axis=0) for x, d in zip(xs, ds)] + return pad_list(repeat, self.pad_value) + ''' + if is_inference: + return self.expand(xs, ds) + else: + return self.expand_numpy(xs, ds) diff --git a/ernie-sat/paddlespeech/t2s/modules/predictor/variance_predictor.py b/ernie-sat/paddlespeech/t2s/modules/predictor/variance_predictor.py new file mode 100644 index 0000000..8afbf25 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/predictor/variance_predictor.py @@ -0,0 +1,94 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Variance predictor related modules.""" +import paddle +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.t2s.modules.layer_norm import LayerNorm +from paddlespeech.t2s.modules.masked_fill import masked_fill + + +class VariancePredictor(nn.Layer): + """Variance predictor module. + + This is a module of variacne predictor described in `FastSpeech 2: + Fast and High-Quality End-to-End Text to Speech`_. + + .. _`FastSpeech 2: Fast and High-Quality End-to-End Text to Speech`: + https://arxiv.org/abs/2006.04558 + + """ + + def __init__( + self, + idim: int, + n_layers: int=2, + n_chans: int=384, + kernel_size: int=3, + bias: bool=True, + dropout_rate: float=0.5, ): + """Initilize duration predictor module. + + Args: + idim (int): Input dimension. + n_layers (int, optional): Number of convolutional layers. + n_chans (int, optional): Number of channels of convolutional layers. + kernel_size (int, optional): Kernel size of convolutional layers. + dropout_rate (float, optional): Dropout rate. + """ + assert check_argument_types() + super().__init__() + self.conv = nn.LayerList() + for idx in range(n_layers): + in_chans = idim if idx == 0 else n_chans + self.conv.append( + nn.Sequential( + nn.Conv1D( + in_chans, + n_chans, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, + bias_attr=True, ), + nn.ReLU(), + LayerNorm(n_chans, dim=1), + nn.Dropout(dropout_rate), )) + + self.linear = nn.Linear(n_chans, 1, bias_attr=True) + + def forward(self, xs: paddle.Tensor, + x_masks: paddle.Tensor=None) -> paddle.Tensor: + """Calculate forward propagation. + + Args: + xs (Tensor): Batch of input sequences (B, Tmax, idim). + x_masks (Tensor(bool), optional): Batch of masks indicating padded part (B, Tmax, 1). + + Returns: + Tensor: Batch of predicted sequences (B, Tmax, 1). + """ + # (B, idim, Tmax) + xs = xs.transpose([0, 2, 1]) + # (B, C, Tmax) + for f in self.conv: + # (B, C, Tmax) + xs = f(xs) + # (B, Tmax, 1) + xs = self.linear(xs.transpose([0, 2, 1])) + + if x_masks is not None: + xs = masked_fill(xs, x_masks, 0.0) + return xs diff --git a/ernie-sat/paddlespeech/t2s/modules/residual_block.py b/ernie-sat/paddlespeech/t2s/modules/residual_block.py new file mode 100644 index 0000000..efbfce2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/residual_block.py @@ -0,0 +1,179 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math +from typing import Any +from typing import Dict +from typing import List + +import paddle +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.t2s.modules.activation import get_activation + + +class WaveNetResidualBlock(nn.Layer): + """A gated activation unit composed of an 1D convolution, a gated tanh + unit and parametric redidual and skip connections. For more details, + refer to `WaveNet: A Generative Model for Raw Audio `_. + + Args: + kernel_size (int, optional): Kernel size of the 1D convolution, by default 3 + residual_channels (int, optional): Feature size of the resiaudl output(and also the input), by default 64 + gate_channels (int, optional): Output feature size of the 1D convolution, by default 128 + skip_channels (int, optional): Feature size of the skip output, by default 64 + aux_channels (int, optional): Feature size of the auxiliary input (e.g. spectrogram), by default 80 + dropout (float, optional): Probability of the dropout before the 1D convolution, by default 0. + dilation (int, optional): Dilation of the 1D convolution, by default 1 + bias (bool, optional): Whether to use bias in the 1D convolution, by default True + use_causal_conv (bool, optional): Whether to use causal padding for the 1D convolution, by default False + """ + + def __init__(self, + kernel_size: int=3, + residual_channels: int=64, + gate_channels: int=128, + skip_channels: int=64, + aux_channels: int=80, + dropout: float=0., + dilation: int=1, + bias: bool=True, + use_causal_conv: bool=False): + super().__init__() + self.dropout = dropout + if use_causal_conv: + padding = (kernel_size - 1) * dilation + else: + assert kernel_size % 2 == 1 + padding = (kernel_size - 1) // 2 * dilation + self.use_causal_conv = use_causal_conv + + self.conv = nn.Conv1D( + residual_channels, + gate_channels, + kernel_size, + padding=padding, + dilation=dilation, + bias_attr=bias) + if aux_channels is not None: + self.conv1x1_aux = nn.Conv1D( + aux_channels, gate_channels, kernel_size=1, bias_attr=False) + else: + self.conv1x1_aux = None + + gate_out_channels = gate_channels // 2 + self.conv1x1_out = nn.Conv1D( + gate_out_channels, residual_channels, kernel_size=1, bias_attr=bias) + self.conv1x1_skip = nn.Conv1D( + gate_out_channels, skip_channels, kernel_size=1, bias_attr=bias) + + def forward(self, x, c): + """ + Args: + x (Tensor): the input features. Shape (N, C_res, T) + c (Tensor): the auxiliary input. Shape (N, C_aux, T) + + Returns: + res (Tensor): Shape (N, C_res, T), the residual output, which is used as the + input of the next ResidualBlock in a stack of ResidualBlocks. + skip (Tensor): Shape (N, C_skip, T), the skip output, which is collected among + each layer in a stack of ResidualBlocks. + """ + x_input = x + x = F.dropout(x, self.dropout, training=self.training) + x = self.conv(x) + x = x[:, :, x_input.shape[-1]] if self.use_causal_conv else x + if c is not None: + c = self.conv1x1_aux(c) + x += c + + a, b = paddle.chunk(x, 2, axis=1) + x = paddle.tanh(a) * F.sigmoid(b) + + skip = self.conv1x1_skip(x) + res = (self.conv1x1_out(x) + x_input) * math.sqrt(0.5) + return res, skip + + +class HiFiGANResidualBlock(nn.Layer): + """Residual block module in HiFiGAN.""" + + def __init__( + self, + kernel_size: int=3, + channels: int=512, + dilations: List[int]=(1, 3, 5), + bias: bool=True, + use_additional_convs: bool=True, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.1}, + ): + """Initialize HiFiGANResidualBlock module. + Args: + kernel_size (int): Kernel size of dilation convolution layer. + channels (int): Number of channels for convolution layer. + dilations (List[int]): List of dilation factors. + use_additional_convs (bool): Whether to use additional convolution layers. + bias (bool): Whether to add bias parameter in convolution layers. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (dict): Hyperparameters for activation function. + """ + super().__init__() + + self.use_additional_convs = use_additional_convs + self.convs1 = nn.LayerList() + if use_additional_convs: + self.convs2 = nn.LayerList() + assert kernel_size % 2 == 1, "Kernel size must be odd number." + + for dilation in dilations: + self.convs1.append( + nn.Sequential( + get_activation(nonlinear_activation, ** + nonlinear_activation_params), + nn.Conv1D( + channels, + channels, + kernel_size, + 1, + dilation=dilation, + bias_attr=bias, + padding=(kernel_size - 1) // 2 * dilation, ), )) + if use_additional_convs: + self.convs2.append( + nn.Sequential( + get_activation(nonlinear_activation, ** + nonlinear_activation_params), + nn.Conv1D( + channels, + channels, + kernel_size, + 1, + dilation=1, + bias_attr=bias, + padding=(kernel_size - 1) // 2, ), )) + + def forward(self, x): + """Calculate forward propagation. + Args: + x (Tensor): Input tensor (B, channels, T). + Returns: + Tensor: Output tensor (B, channels, T). + """ + for idx in range(len(self.convs1)): + xt = self.convs1[idx](x) + if self.use_additional_convs: + xt = self.convs2[idx](xt) + x = xt + x + return x diff --git a/ernie-sat/paddlespeech/t2s/modules/residual_stack.py b/ernie-sat/paddlespeech/t2s/modules/residual_stack.py new file mode 100644 index 0000000..0d949b5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/residual_stack.py @@ -0,0 +1,102 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Residual stack module in MelGAN.""" +from typing import Any +from typing import Dict + +from paddle import nn + +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.causal_conv import CausalConv1D + + +class ResidualStack(nn.Layer): + """Residual stack module introduced in MelGAN.""" + + def __init__( + self, + kernel_size: int=3, + channels: int=32, + dilation: int=1, + bias: bool=True, + nonlinear_activation: str="leakyrelu", + nonlinear_activation_params: Dict[str, Any]={"negative_slope": 0.2}, + pad: str="Pad1D", + pad_params: Dict[str, Any]={"mode": "reflect"}, + use_causal_conv: bool=False, ): + """Initialize ResidualStack module. + + Args: + kernel_size (int): Kernel size of dilation convolution layer. + channels (int): Number of channels of convolution layers. + dilation (int): Dilation factor. + bias (bool): Whether to add bias parameter in convolution layers. + nonlinear_activation (str): Activation function module name. + nonlinear_activation_params (Dict[str,Any]): Hyperparameters for activation function. + pad (str): Padding function module name before dilated convolution layer. + pad_params (Dict[str, Any]): Hyperparameters for padding function. + use_causal_conv (bool): Whether to use causal convolution. + """ + super().__init__() + # for compatibility + if nonlinear_activation: + nonlinear_activation = nonlinear_activation.lower() + + # defile residual stack part + if not use_causal_conv: + assert (kernel_size - 1 + ) % 2 == 0, "Not support even number kernel size." + self.stack = nn.Sequential( + get_activation(nonlinear_activation, + **nonlinear_activation_params), + getattr(nn, pad)((kernel_size - 1) // 2 * dilation, + **pad_params), + nn.Conv1D( + channels, + channels, + kernel_size, + dilation=dilation, + bias_attr=bias), + get_activation(nonlinear_activation, + **nonlinear_activation_params), + nn.Conv1D(channels, channels, 1, bias_attr=bias), ) + else: + self.stack = nn.Sequential( + get_activation(nonlinear_activation, + **nonlinear_activation_params), + CausalConv1D( + channels, + channels, + kernel_size, + dilation=dilation, + bias=bias, + pad=pad, + pad_params=pad_params, ), + get_activation(nonlinear_activation, + **nonlinear_activation_params), + nn.Conv1D(channels, channels, 1, bias_attr=bias), ) + + # defile extra layer for skip connection + self.skip_layer = nn.Conv1D(channels, channels, 1, bias_attr=bias) + + def forward(self, c): + """Calculate forward propagation. + + Args: + c (Tensor): Input tensor (B, channels, T). + Returns: + Tensor: Output tensor (B, chennels, T). + """ + return self.stack(c) + self.skip_layer(c) diff --git a/ernie-sat/paddlespeech/t2s/modules/style_encoder.py b/ernie-sat/paddlespeech/t2s/modules/style_encoder.py new file mode 100644 index 0000000..49091ea --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/style_encoder.py @@ -0,0 +1,273 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Style encoder of GST-Tacotron.""" +from typing import Sequence + +import paddle +from paddle import nn +from typeguard import check_argument_types + +from paddlespeech.t2s.modules.transformer.attention import MultiHeadedAttention as BaseMultiHeadedAttention + + +class StyleEncoder(nn.Layer): + """Style encoder. + + This module is style encoder introduced in `Style Tokens: Unsupervised Style + Modeling, Control and Transfer in End-to-End Speech Synthesis`. + + .. _`Style Tokens: Unsupervised Style Modeling, Control and Transfer in End-to-End + Speech Synthesis`: https://arxiv.org/abs/1803.09017 + + Args: + idim (int, optional): Dimension of the input mel-spectrogram. + gst_tokens (int, optional): The number of GST embeddings. + gst_token_dim (int, optional): Dimension of each GST embedding. + gst_heads (int, optional): The number of heads in GST multihead attention. + conv_layers (int, optional): The number of conv layers in the reference encoder. + conv_chans_list (Sequence[int], optional): List of the number of channels of conv layers in the referece encoder. + conv_kernel_size (int, optional): Kernal size of conv layers in the reference encoder. + conv_stride (int, optional): Stride size of conv layers in the reference encoder. + gru_layers (int, optional): The number of GRU layers in the reference encoder. + gru_units (int, optional):The number of GRU units in the reference encoder. + + Todo: + * Support manual weight specification in inference. + + """ + + def __init__( + self, + idim: int=80, + gst_tokens: int=10, + gst_token_dim: int=256, + gst_heads: int=4, + conv_layers: int=6, + conv_chans_list: Sequence[int]=(32, 32, 64, 64, 128, 128), + conv_kernel_size: int=3, + conv_stride: int=2, + gru_layers: int=1, + gru_units: int=128, ): + """Initilize global style encoder module.""" + assert check_argument_types() + super().__init__() + + self.ref_enc = ReferenceEncoder( + idim=idim, + conv_layers=conv_layers, + conv_chans_list=conv_chans_list, + conv_kernel_size=conv_kernel_size, + conv_stride=conv_stride, + gru_layers=gru_layers, + gru_units=gru_units, ) + self.stl = StyleTokenLayer( + ref_embed_dim=gru_units, + gst_tokens=gst_tokens, + gst_token_dim=gst_token_dim, + gst_heads=gst_heads, ) + + def forward(self, speech: paddle.Tensor) -> paddle.Tensor: + """Calculate forward propagation. + + Args: + speech (Tensor): Batch of padded target features (B, Lmax, odim). + + Returns: + Tensor: Style token embeddings (B, token_dim). + + """ + ref_embs = self.ref_enc(speech) + style_embs = self.stl(ref_embs) + + return style_embs + + +class ReferenceEncoder(nn.Layer): + """Reference encoder module. + + This module is refernece encoder introduced in `Style Tokens: Unsupervised Style + Modeling, Control and Transfer in End-to-End Speech Synthesis`. + + .. _`Style Tokens: Unsupervised Style Modeling, Control and Transfer in End-to-End + Speech Synthesis`: https://arxiv.org/abs/1803.09017 + + Args: + idim (int, optional): Dimension of the input mel-spectrogram. + conv_layers (int, optional): The number of conv layers in the reference encoder. + conv_chans_list: (Sequence[int], optional): List of the number of channels of conv layers in the referece encoder. + conv_kernel_size (int, optional): Kernal size of conv layers in the reference encoder. + conv_stride (int, optional): Stride size of conv layers in the reference encoder. + gru_layers (int, optional): The number of GRU layers in the reference encoder. + gru_units (int, optional): The number of GRU units in the reference encoder. + + """ + + def __init__( + self, + idim=80, + conv_layers: int=6, + conv_chans_list: Sequence[int]=(32, 32, 64, 64, 128, 128), + conv_kernel_size: int=3, + conv_stride: int=2, + gru_layers: int=1, + gru_units: int=128, ): + """Initilize reference encoder module.""" + assert check_argument_types() + super().__init__() + + # check hyperparameters are valid + assert conv_kernel_size % 2 == 1, "kernel size must be odd." + assert ( + len(conv_chans_list) == conv_layers + ), "the number of conv layers and length of channels list must be the same." + + convs = [] + padding = (conv_kernel_size - 1) // 2 + for i in range(conv_layers): + conv_in_chans = 1 if i == 0 else conv_chans_list[i - 1] + conv_out_chans = conv_chans_list[i] + convs += [ + nn.Conv2D( + conv_in_chans, + conv_out_chans, + kernel_size=conv_kernel_size, + stride=conv_stride, + padding=padding, + # Do not use bias due to the following batch norm + bias_attr=False, ), + nn.BatchNorm2D(conv_out_chans), + nn.ReLU(), + ] + self.convs = nn.Sequential(*convs) + + self.conv_layers = conv_layers + self.kernel_size = conv_kernel_size + self.stride = conv_stride + self.padding = padding + + # get the number of GRU input units + gru_in_units = idim + for i in range(conv_layers): + gru_in_units = (gru_in_units - conv_kernel_size + 2 * padding + ) // conv_stride + 1 + gru_in_units *= conv_out_chans + self.gru = nn.GRU(gru_in_units, gru_units, gru_layers, time_major=False) + + def forward(self, speech: paddle.Tensor) -> paddle.Tensor: + """Calculate forward propagation. + Args: + speech (Tensor): Batch of padded target features (B, Lmax, idim). + + Returns: + Tensor: Reference embedding (B, gru_units) + + """ + batch_size = speech.shape[0] + # (B, 1, Lmax, idim) + xs = speech.unsqueeze(1) + # (B, Lmax', conv_out_chans, idim') + hs = self.convs(xs).transpose([0, 2, 1, 3]) + time_length = hs.shape[1] + # (B, Lmax', gru_units) + hs = hs.reshape(shape=[batch_size, time_length, -1]) + self.gru.flatten_parameters() + # (gru_layers, batch_size, gru_units) + _, ref_embs = self.gru(hs) + # (batch_size, gru_units) + ref_embs = ref_embs[-1] + + return ref_embs + + +class StyleTokenLayer(nn.Layer): + """Style token layer module. + + This module is style token layer introduced in `Style Tokens: Unsupervised Style + Modeling, Control and Transfer in End-to-End Speech Synthesis`. + + .. _`Style Tokens: Unsupervised Style Modeling, Control and Transfer in End-to-End + Speech Synthesis`: https://arxiv.org/abs/1803.09017 + Args: + ref_embed_dim (int, optional): Dimension of the input reference embedding. + gst_tokens (int, optional): The number of GST embeddings. + gst_token_dim (int, optional): Dimension of each GST embedding. + gst_heads (int, optional): The number of heads in GST multihead attention. + dropout_rate (float, optional): Dropout rate in multi-head attention. + + """ + + def __init__( + self, + ref_embed_dim: int=128, + gst_tokens: int=10, + gst_token_dim: int=256, + gst_heads: int=4, + dropout_rate: float=0.0, ): + """Initilize style token layer module.""" + assert check_argument_types() + super().__init__() + + gst_embs = paddle.randn(shape=[gst_tokens, gst_token_dim // gst_heads]) + self.gst_embs = paddle.create_parameter( + shape=gst_embs.shape, + dtype=str(gst_embs.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign(gst_embs)) + self.mha = MultiHeadedAttention( + q_dim=ref_embed_dim, + k_dim=gst_token_dim // gst_heads, + v_dim=gst_token_dim // gst_heads, + n_head=gst_heads, + n_feat=gst_token_dim, + dropout_rate=dropout_rate, ) + + def forward(self, ref_embs: paddle.Tensor) -> paddle.Tensor: + """Calculate forward propagation. + + Args: + ref_embs (Tensor): Reference embeddings (B, ref_embed_dim). + + Returns: + Tensor: Style token embeddings (B, gst_token_dim). + + """ + batch_size = ref_embs.shape[0] + # (num_tokens, token_dim) -> (batch_size, num_tokens, token_dim) + gst_embs = paddle.tanh(self.gst_embs).unsqueeze(0).expand( + [batch_size, -1, -1]) + # (batch_size, 1 ,ref_embed_dim) + ref_embs = ref_embs.unsqueeze(1) + style_embs = self.mha(ref_embs, gst_embs, gst_embs, None) + + return style_embs.squeeze(1) + + +class MultiHeadedAttention(BaseMultiHeadedAttention): + """Multi head attention module with different input dimension.""" + + def __init__(self, q_dim, k_dim, v_dim, n_head, n_feat, dropout_rate=0.0): + """Initialize multi head attention module.""" + # Do not use super().__init__() here since we want to + # overwrite BaseMultiHeadedAttention.__init__() method. + nn.Layer.__init__(self) + assert n_feat % n_head == 0 + # We assume d_v always equals d_k + self.d_k = n_feat // n_head + self.h = n_head + self.linear_q = nn.Linear(q_dim, n_feat) + self.linear_k = nn.Linear(k_dim, n_feat) + self.linear_v = nn.Linear(v_dim, n_feat) + self.linear_out = nn.Linear(n_feat, n_feat) + self.attn = None + self.dropout = nn.Dropout(p=dropout_rate) diff --git a/ernie-sat/paddlespeech/t2s/modules/tacotron2/__init__.py b/ernie-sat/paddlespeech/t2s/modules/tacotron2/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/tacotron2/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/modules/tacotron2/attentions.py b/ernie-sat/paddlespeech/t2s/modules/tacotron2/attentions.py new file mode 100644 index 0000000..a6fde74 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/tacotron2/attentions.py @@ -0,0 +1,454 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Attention modules for RNN.""" +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.modules.masked_fill import masked_fill +from paddlespeech.t2s.modules.nets_utils import make_pad_mask + + +def _apply_attention_constraint(e, + last_attended_idx, + backward_window=1, + forward_window=3): + """Apply monotonic attention constraint. + + This function apply the monotonic attention constraint + introduced in `Deep Voice 3: Scaling + Text-to-Speech with Convolutional Sequence Learning`_. + + Args: + e(Tensor): Attention energy before applying softmax (1, T). + last_attended_idx(int): The index of the inputs of the last attended [0, T]. + backward_window(int, optional, optional): Backward window size in attention constraint. (Default value = 1) + forward_window(int, optional, optional): Forward window size in attetion constraint. (Default value = 3) + + Returns: + Tensor: Monotonic constrained attention energy (1, T). + + .. _`Deep Voice 3: Scaling Text-to-Speech with Convolutional Sequence Learning`: + https://arxiv.org/abs/1710.07654 + + """ + if paddle.shape(e)[0] != 1: + raise NotImplementedError( + "Batch attention constraining is not yet supported.") + backward_idx = last_attended_idx - backward_window + forward_idx = last_attended_idx + forward_window + if backward_idx > 0: + e[:, :backward_idx] = -float("inf") + if forward_idx < paddle.shape(e)[1]: + e[:, forward_idx:] = -float("inf") + return e + + +class AttLoc(nn.Layer): + """location-aware attention module. + + Reference: Attention-Based Models for Speech Recognition + (https://arxiv.org/pdf/1506.07503.pdf) + + Args: + eprojs (int): projection-units of encoder + dunits (int): units of decoder + att_dim (int): attention dimension + aconv_chans (int): channels of attention convolution + aconv_filts (int): filter size of attention convolution + han_mode (bool): flag to swith on mode of hierarchical attention and not store pre_compute_enc_h + """ + + def __init__(self, + eprojs, + dunits, + att_dim, + aconv_chans, + aconv_filts, + han_mode=False): + super().__init__() + self.mlp_enc = nn.Linear(eprojs, att_dim) + self.mlp_dec = nn.Linear(dunits, att_dim, bias_attr=False) + self.mlp_att = nn.Linear(aconv_chans, att_dim, bias_attr=False) + self.loc_conv = nn.Conv2D( + 1, + aconv_chans, + (1, 2 * aconv_filts + 1), + padding=(0, aconv_filts), + bias_attr=False, ) + self.gvec = nn.Linear(att_dim, 1) + + self.dunits = dunits + self.eprojs = eprojs + self.att_dim = att_dim + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + self.han_mode = han_mode + + def reset(self): + """reset states""" + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + + def forward( + self, + enc_hs_pad, + enc_hs_len, + dec_z, + att_prev, + scaling=2.0, + last_attended_idx=None, + backward_window=1, + forward_window=3, ): + """Calculate AttLoc forward propagation. + Args: + enc_hs_pad(Tensor): padded encoder hidden state (B, T_max, D_enc) + enc_hs_len(Tensor): padded encoder hidden state length (B) + dec_z(Tensor dec_z): decoder hidden state (B, D_dec) + att_prev(Tensor): previous attention weight (B, T_max) + scaling(float, optional): scaling parameter before applying softmax (Default value = 2.0) + forward_window(Tensor, optional): forward window size when constraining attention (Default value = 3) + last_attended_idx(int, optional): index of the inputs of the last attended (Default value = None) + backward_window(int, optional): backward window size in attention constraint (Default value = 1) + forward_window(int, optional): forward window size in attetion constraint (Default value = 3) + Returns: + Tensor: attention weighted encoder state (B, D_enc) + Tensor: previous attention weights (B, T_max) + """ + batch = paddle.shape(enc_hs_pad)[0] + # pre-compute all h outside the decoder loop + if self.pre_compute_enc_h is None or self.han_mode: + # (utt, frame, hdim) + self.enc_h = enc_hs_pad + self.h_length = paddle.shape(self.enc_h)[1] + # (utt, frame, att_dim) + self.pre_compute_enc_h = self.mlp_enc(self.enc_h) + + if dec_z is None: + dec_z = paddle.zeros([batch, self.dunits]) + else: + dec_z = dec_z.reshape([batch, self.dunits]) + + # initialize attention weight with uniform dist. + if paddle.sum(att_prev) == 0: + # if no bias, 0 0-pad goes 0 + att_prev = 1.0 - make_pad_mask(enc_hs_len) + att_prev = att_prev / enc_hs_len.unsqueeze(-1) + + # att_prev: (utt, frame) -> (utt, 1, 1, frame) + # -> (utt, att_conv_chans, 1, frame) + att_conv = self.loc_conv(att_prev.reshape([batch, 1, 1, self.h_length])) + # att_conv: (utt, att_conv_chans, 1, frame) -> (utt, frame, att_conv_chans) + att_conv = att_conv.squeeze(2).transpose([0, 2, 1]) + # att_conv: (utt, frame, att_conv_chans) -> (utt, frame, att_dim) + att_conv = self.mlp_att(att_conv) + # dec_z_tiled: (utt, frame, att_dim) + dec_z_tiled = self.mlp_dec(dec_z).reshape([batch, 1, self.att_dim]) + + # dot with gvec + # (utt, frame, att_dim) -> (utt, frame) + e = paddle.tanh(att_conv + self.pre_compute_enc_h + dec_z_tiled) + e = self.gvec(e).squeeze(2) + + # NOTE: consider zero padding when compute w. + if self.mask is None: + self.mask = make_pad_mask(enc_hs_len) + + e = masked_fill(e, self.mask, -float("inf")) + # apply monotonic attention constraint (mainly for TTS) + if last_attended_idx is not None: + e = _apply_attention_constraint(e, last_attended_idx, + backward_window, forward_window) + + w = F.softmax(scaling * e, axis=1) + + # weighted sum over frames + # utt x hdim + c = paddle.sum( + self.enc_h * w.reshape([batch, self.h_length, 1]), axis=1) + return c, w + + +class AttForward(nn.Layer): + """Forward attention module. + Reference + ---------- + Forward attention in sequence-to-sequence acoustic modeling for speech synthesis + (https://arxiv.org/pdf/1807.06736.pdf) + + Args: + eprojs (int): projection-units of encoder + dunits (int): units of decoder + att_dim (int): attention dimension + aconv_chans (int): channels of attention convolution + aconv_filts (int): filter size of attention convolution + """ + + def __init__(self, eprojs, dunits, att_dim, aconv_chans, aconv_filts): + super().__init__() + self.mlp_enc = nn.Linear(eprojs, att_dim) + self.mlp_dec = nn.Linear(dunits, att_dim, bias_attr=False) + self.mlp_att = nn.Linear(aconv_chans, att_dim, bias_attr=False) + self.loc_conv = nn.Conv2D( + 1, + aconv_chans, + (1, 2 * aconv_filts + 1), + padding=(0, aconv_filts), + bias_attr=False, ) + self.gvec = nn.Linear(att_dim, 1) + self.dunits = dunits + self.eprojs = eprojs + self.att_dim = att_dim + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + + def reset(self): + """reset states""" + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + + def forward( + self, + enc_hs_pad, + enc_hs_len, + dec_z, + att_prev, + scaling=1.0, + last_attended_idx=None, + backward_window=1, + forward_window=3, ): + """Calculate AttForward forward propagation. + + Args: + enc_hs_pad(Tensor): padded encoder hidden state (B, T_max, D_enc) + enc_hs_len(list): padded encoder hidden state length (B,) + dec_z(Tensor): decoder hidden state (B, D_dec) + att_prev(Tensor): attention weights of previous step (B, T_max) + scaling(float, optional): scaling parameter before applying softmax (Default value = 1.0) + last_attended_idx(int, optional): index of the inputs of the last attended (Default value = None) + backward_window(int, optional): backward window size in attention constraint (Default value = 1) + forward_window(int, optional): (Default value = 3) + + Returns: + Tensor: attention weighted encoder state (B, D_enc) + Tensor: previous attention weights (B, T_max) + """ + batch = len(enc_hs_pad) + # pre-compute all h outside the decoder loop + if self.pre_compute_enc_h is None: + self.enc_h = enc_hs_pad # utt x frame x hdim + self.h_length = paddle.shape(self.enc_h)[1] + # utt x frame x att_dim + self.pre_compute_enc_h = self.mlp_enc(self.enc_h) + + if dec_z is None: + dec_z = paddle.zeros([batch, self.dunits]) + else: + dec_z = dec_z.reshape([batch, self.dunits]) + + if att_prev is None: + # initial attention will be [1, 0, 0, ...] + att_prev = paddle.zeros([*paddle.shape(enc_hs_pad)[:2]]) + att_prev[:, 0] = 1.0 + + # att_prev: utt x frame -> utt x 1 x 1 x frame + # -> utt x att_conv_chans x 1 x frame + att_conv = self.loc_conv(att_prev.reshape([batch, 1, 1, self.h_length])) + # att_conv: utt x att_conv_chans x 1 x frame -> utt x frame x att_conv_chans + att_conv = att_conv.squeeze(2).transpose([0, 2, 1]) + # att_conv: utt x frame x att_conv_chans -> utt x frame x att_dim + att_conv = self.mlp_att(att_conv) + + # dec_z_tiled: utt x frame x att_dim + dec_z_tiled = self.mlp_dec(dec_z).unsqueeze(1) + + # dot with gvec + # utt x frame x att_dim -> utt x frame + e = self.gvec( + paddle.tanh(self.pre_compute_enc_h + dec_z_tiled + + att_conv)).squeeze(2) + + # NOTE: consider zero padding when compute w. + if self.mask is None: + self.mask = make_pad_mask(enc_hs_len) + e = masked_fill(e, self.mask, -float("inf")) + + # apply monotonic attention constraint (mainly for TTS) + if last_attended_idx is not None: + e = _apply_attention_constraint(e, last_attended_idx, + backward_window, forward_window) + + w = F.softmax(scaling * e, axis=1) + + # forward attention + att_prev_shift = F.pad(att_prev, (0, 0, 1, 0))[:, :-1] + + w = (att_prev + att_prev_shift) * w + # NOTE: clip is needed to avoid nan gradient + w = F.normalize(paddle.clip(w, 1e-6), p=1, axis=1) + + # weighted sum over flames + # utt x hdim + # NOTE use bmm instead of sum(*) + c = paddle.sum(self.enc_h * w.unsqueeze(-1), axis=1) + + return c, w + + +class AttForwardTA(nn.Layer): + """Forward attention with transition agent module. + Reference: + Forward attention in sequence-to-sequence acoustic modeling for speech synthesis + (https://arxiv.org/pdf/1807.06736.pdf) + + Args: + eunits (int): units of encoder + dunits (int): units of decoder + att_dim (int): attention dimension + aconv_chans (int): channels of attention convolution + aconv_filts (int): filter size of attention convolution + odim (int): output dimension + """ + + def __init__(self, eunits, dunits, att_dim, aconv_chans, aconv_filts, odim): + super().__init__() + self.mlp_enc = nn.Linear(eunits, att_dim) + self.mlp_dec = nn.Linear(dunits, att_dim, bias_attr=False) + self.mlp_ta = nn.Linear(eunits + dunits + odim, 1) + self.mlp_att = nn.Linear(aconv_chans, att_dim, bias_attr=False) + self.loc_conv = nn.Conv2D( + 1, + aconv_chans, + (1, 2 * aconv_filts + 1), + padding=(0, aconv_filts), + bias_attr=False, ) + self.gvec = nn.Linear(att_dim, 1) + self.dunits = dunits + self.eunits = eunits + self.att_dim = att_dim + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + self.trans_agent_prob = 0.5 + + def reset(self): + self.h_length = None + self.enc_h = None + self.pre_compute_enc_h = None + self.mask = None + self.trans_agent_prob = 0.5 + + def forward( + self, + enc_hs_pad, + enc_hs_len, + dec_z, + att_prev, + out_prev, + scaling=1.0, + last_attended_idx=None, + backward_window=1, + forward_window=3, ): + """Calculate AttForwardTA forward propagation. + + Args: + enc_hs_pad(Tensor): padded encoder hidden state (B, Tmax, eunits) + enc_hs_len(list Tensor): padded encoder hidden state length (B,) + dec_z(Tensor): decoder hidden state (B, dunits) + att_prev(Tensor): attention weights of previous step (B, T_max) + out_prev(Tensor): decoder outputs of previous step (B, odim) + scaling(float, optional): scaling parameter before applying softmax (Default value = 1.0) + last_attended_idx(int, optional): index of the inputs of the last attended (Default value = None) + backward_window(int, optional): backward window size in attention constraint (Default value = 1) + forward_window(int, optional): (Default value = 3) + + Returns: + Tensor: attention weighted encoder state (B, dunits) + Tensor: previous attention weights (B, Tmax) + """ + batch = len(enc_hs_pad) + # pre-compute all h outside the decoder loop + if self.pre_compute_enc_h is None: + self.enc_h = enc_hs_pad # utt x frame x hdim + self.h_length = paddle.shape(self.enc_h)[1] + # utt x frame x att_dim + self.pre_compute_enc_h = self.mlp_enc(self.enc_h) + + if dec_z is None: + dec_z = paddle.zeros([batch, self.dunits]) + else: + dec_z = dec_z.reshape([batch, self.dunits]) + + if att_prev is None: + # initial attention will be [1, 0, 0, ...] + att_prev = paddle.zeros([*paddle.shape(enc_hs_pad)[:2]]) + att_prev[:, 0] = 1.0 + + # att_prev: utt x frame -> utt x 1 x 1 x frame + # -> utt x att_conv_chans x 1 x frame + att_conv = self.loc_conv(att_prev.reshape([batch, 1, 1, self.h_length])) + # att_conv: utt x att_conv_chans x 1 x frame -> utt x frame x att_conv_chans + att_conv = att_conv.squeeze(2).transpose([0, 2, 1]) + # att_conv: utt x frame x att_conv_chans -> utt x frame x att_dim + att_conv = self.mlp_att(att_conv) + + # dec_z_tiled: utt x frame x att_dim + dec_z_tiled = self.mlp_dec(dec_z).reshape([batch, 1, self.att_dim]) + + # dot with gvec + # utt x frame x att_dim -> utt x frame + e = self.gvec( + paddle.tanh(att_conv + self.pre_compute_enc_h + + dec_z_tiled)).squeeze(2) + + # NOTE consider zero padding when compute w. + if self.mask is None: + self.mask = make_pad_mask(enc_hs_len) + e = masked_fill(e, self.mask, -float("inf")) + + # apply monotonic attention constraint (mainly for TTS) + if last_attended_idx is not None: + e = _apply_attention_constraint(e, last_attended_idx, + backward_window, forward_window) + + w = F.softmax(scaling * e, axis=1) + + # forward attention + # att_prev_shift = F.pad(att_prev.unsqueeze(0), (1, 0), data_format='NCL').squeeze(0)[:, :-1] + att_prev_shift = F.pad(att_prev, (0, 0, 1, 0))[:, :-1] + w = (self.trans_agent_prob * att_prev + + (1 - self.trans_agent_prob) * att_prev_shift) * w + # NOTE: clip is needed to avoid nan gradient + w = F.normalize(paddle.clip(w, 1e-6), p=1, axis=1) + + # weighted sum over flames + # utt x hdim + # NOTE use bmm instead of sum(*) + c = paddle.sum( + self.enc_h * w.reshape([batch, self.h_length, 1]), axis=1) + + # update transition agent prob + self.trans_agent_prob = F.sigmoid( + self.mlp_ta(paddle.concat([c, out_prev, dec_z], axis=1))) + + return c, w diff --git a/ernie-sat/paddlespeech/t2s/modules/tacotron2/decoder.py b/ernie-sat/paddlespeech/t2s/modules/tacotron2/decoder.py new file mode 100644 index 0000000..ebdfa38 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/tacotron2/decoder.py @@ -0,0 +1,686 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Tacotron2 decoder related modules.""" +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.modules.tacotron2.attentions import AttForwardTA + + +class Prenet(nn.Layer): + """Prenet module for decoder of Spectrogram prediction network. + + This is a module of Prenet in the decoder of Spectrogram prediction network, + which described in `Natural TTS + Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`_. + The Prenet preforms nonlinear conversion + of inputs before input to auto-regressive lstm, + which helps to learn diagonal attentions. + + Notes + ---------- + This module alway applies dropout even in evaluation. + See the detail in `Natural TTS Synthesis by + Conditioning WaveNet on Mel Spectrogram Predictions`_. + + .. _`Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`: + https://arxiv.org/abs/1712.05884 + + """ + + def __init__(self, idim, n_layers=2, n_units=256, dropout_rate=0.5): + """Initialize prenet module. + + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + n_layers (int, optional): The number of prenet layers. + n_units (int, optional): The number of prenet units. + """ + super().__init__() + self.dropout_rate = dropout_rate + self.prenet = nn.LayerList() + for layer in range(n_layers): + n_inputs = idim if layer == 0 else n_units + self.prenet.append( + nn.Sequential(nn.Linear(n_inputs, n_units), nn.ReLU())) + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Batch of input tensors (B, ..., idim). + + Returns: + Tensor: Batch of output tensors (B, ..., odim). + + """ + for i in range(len(self.prenet)): + # F.dropout 引入了随机, tacotron2 的 dropout 是不能去掉的 + x = F.dropout(self.prenet[i](x)) + return x + + +class Postnet(nn.Layer): + """Postnet module for Spectrogram prediction network. + + This is a module of Postnet in Spectrogram prediction network, + which described in `Natural TTS Synthesis by + Conditioning WaveNet on Mel Spectrogram Predictions`_. + The Postnet predicts refines the predicted + Mel-filterbank of the decoder, + which helps to compensate the detail sturcture of spectrogram. + + .. _`Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`: + https://arxiv.org/abs/1712.05884 + + """ + + def __init__( + self, + idim, + odim, + n_layers=5, + n_chans=512, + n_filts=5, + dropout_rate=0.5, + use_batch_norm=True, ): + """Initialize postnet module. + + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + n_layers (int, optional): The number of layers. + n_filts (int, optional): The number of filter size. + n_units (int, optional): The number of filter channels. + use_batch_norm (bool, optional): Whether to use batch normalization.. + dropout_rate (float, optional): Dropout rate.. + """ + super().__init__() + self.postnet = nn.LayerList() + for layer in range(n_layers - 1): + ichans = odim if layer == 0 else n_chans + ochans = odim if layer == n_layers - 1 else n_chans + if use_batch_norm: + self.postnet.append( + nn.Sequential( + nn.Conv1D( + ichans, + ochans, + n_filts, + stride=1, + padding=(n_filts - 1) // 2, + bias_attr=False, ), + nn.BatchNorm1D(ochans), + nn.Tanh(), + nn.Dropout(dropout_rate), )) + else: + self.postnet.append( + nn.Sequential( + nn.Conv1D( + ichans, + ochans, + n_filts, + stride=1, + padding=(n_filts - 1) // 2, + bias_attr=False, ), + nn.Tanh(), + nn.Dropout(dropout_rate), )) + ichans = n_chans if n_layers != 1 else odim + if use_batch_norm: + self.postnet.append( + nn.Sequential( + nn.Conv1D( + ichans, + odim, + n_filts, + stride=1, + padding=(n_filts - 1) // 2, + bias_attr=False, ), + nn.BatchNorm1D(odim), + nn.Dropout(dropout_rate), )) + else: + self.postnet.append( + nn.Sequential( + nn.Conv1D( + ichans, + odim, + n_filts, + stride=1, + padding=(n_filts - 1) // 2, + bias_attr=False, ), + nn.Dropout(dropout_rate), )) + + def forward(self, xs): + """Calculate forward propagation. + + Args: + xs (Tensor): Batch of the sequences of padded input tensors (B, idim, Tmax). + Returns: + Tensor: Batch of padded output tensor. (B, odim, Tmax). + """ + for i in range(len(self.postnet)): + xs = self.postnet[i](xs) + return xs + + +class ZoneOutCell(nn.Layer): + """ZoneOut Cell module. + This is a module of zoneout described in + `Zoneout: Regularizing RNNs by Randomly Preserving Hidden Activations`_. + This code is modified from `eladhoffer/seq2seq.pytorch`_. + Examples + ---------- + >>> lstm = paddle.nn.LSTMCell(16, 32) + >>> lstm = ZoneOutCell(lstm, 0.5) + .. _`Zoneout: Regularizing RNNs by Randomly Preserving Hidden Activations`: + https://arxiv.org/abs/1606.01305 + .. _`eladhoffer/seq2seq.pytorch`: + https://github.com/eladhoffer/seq2seq.pytorch + """ + + def __init__(self, cell, zoneout_rate=0.1): + """Initialize zone out cell module. + + Args: + cell (nn.Layer): Paddle recurrent cell module + e.g. `paddle.nn.LSTMCell`. + zoneout_rate (float, optional): Probability of zoneout from 0.0 to 1.0. + """ + super().__init__() + self.cell = cell + self.hidden_size = cell.hidden_size + self.zoneout_rate = zoneout_rate + if zoneout_rate > 1.0 or zoneout_rate < 0.0: + raise ValueError( + "zoneout probability must be in the range from 0.0 to 1.0.") + + def forward(self, inputs, hidden): + """Calculate forward propagation. + + Args: + inputs (Tensor): Batch of input tensor (B, input_size). + hidden (tuple): + - Tensor: Batch of initial hidden states (B, hidden_size). + - Tensor: Batch of initial cell states (B, hidden_size). + Returns: + Tensor: + Batch of next hidden states (B, hidden_size). + tuple: + - Tensor: Batch of next hidden states (B, hidden_size). + - Tensor: Batch of next cell states (B, hidden_size). + """ + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.cell(inputs, hidden) + next_hidden = self._zoneout(hidden, next_hidden, self.zoneout_rate) + # to have the same output format with LSTMCell in paddle + return next_hidden[0], next_hidden + + def _zoneout(self, h, next_h, prob): + # apply recursively + if isinstance(h, tuple): + num_h = len(h) + if not isinstance(prob, tuple): + prob = tuple([prob] * num_h) + return tuple( + [self._zoneout(h[i], next_h[i], prob[i]) for i in range(num_h)]) + if self.training: + mask = paddle.bernoulli(paddle.ones([*paddle.shape(h)]) * prob) + return mask * h + (1 - mask) * next_h + else: + return prob * h + (1 - prob) * next_h + + +class Decoder(nn.Layer): + """Decoder module of Spectrogram prediction network. + This is a module of decoder of Spectrogram prediction network in Tacotron2, + which described in `Natural TTS + Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`_. + The decoder generates the sequence of + features from the sequence of the hidden states. + .. _`Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`: + https://arxiv.org/abs/1712.05884 + """ + + def __init__( + self, + idim, + odim, + att, + dlayers=2, + dunits=1024, + prenet_layers=2, + prenet_units=256, + postnet_layers=5, + postnet_chans=512, + postnet_filts=5, + output_activation_fn=None, + cumulate_att_w=True, + use_batch_norm=True, + use_concate=True, + dropout_rate=0.5, + zoneout_rate=0.1, + reduction_factor=1, ): + """Initialize Tacotron2 decoder module. + + Args: + idim (int): Dimension of the inputs. + odim (int): Dimension of the outputs. + att (nn.Layer): Instance of attention class. + dlayers (int, optional): The number of decoder lstm layers. + dunits (int, optional): The number of decoder lstm units. + prenet_layers (int, optional): The number of prenet layers. + prenet_units (int, optional): The number of prenet units. + postnet_layers (int, optional): The number of postnet layers. + postnet_filts (int, optional): The number of postnet filter size. + postnet_chans (int, optional): The number of postnet filter channels. + output_activation_fn (nn.Layer, optional): Activation function for outputs. + cumulate_att_w (bool, optional): Whether to cumulate previous attention weight. + use_batch_norm (bool, optional): Whether to use batch normalization. + use_concate : bool, optional + Whether to concatenate encoder embedding with decoder lstm outputs. + dropout_rate : float, optional + Dropout rate. + zoneout_rate : float, optional + Zoneout rate. + reduction_factor : int, optional + Reduction factor. + """ + super().__init__() + + # store the hyperparameters + self.idim = idim + self.odim = odim + self.att = att + self.output_activation_fn = output_activation_fn + self.cumulate_att_w = cumulate_att_w + self.use_concate = use_concate + self.reduction_factor = reduction_factor + + # check attention type + if isinstance(self.att, AttForwardTA): + self.use_att_extra_inputs = True + else: + self.use_att_extra_inputs = False + + # define lstm network + prenet_units = prenet_units if prenet_layers != 0 else odim + self.lstm = nn.LayerList() + for layer in range(dlayers): + iunits = idim + prenet_units if layer == 0 else dunits + lstm = nn.LSTMCell(iunits, dunits) + if zoneout_rate > 0.0: + lstm = ZoneOutCell(lstm, zoneout_rate) + self.lstm.append(lstm) + + # define prenet + if prenet_layers > 0: + self.prenet = Prenet( + idim=odim, + n_layers=prenet_layers, + n_units=prenet_units, + dropout_rate=dropout_rate, ) + else: + self.prenet = None + + # define postnet + if postnet_layers > 0: + self.postnet = Postnet( + idim=idim, + odim=odim, + n_layers=postnet_layers, + n_chans=postnet_chans, + n_filts=postnet_filts, + use_batch_norm=use_batch_norm, + dropout_rate=dropout_rate, ) + else: + self.postnet = None + + # define projection layers + iunits = idim + dunits if use_concate else dunits + self.feat_out = nn.Linear( + iunits, odim * reduction_factor, bias_attr=False) + self.prob_out = nn.Linear(iunits, reduction_factor) + + def _zero_state(self, hs): + init_hs = paddle.zeros([paddle.shape(hs)[0], self.lstm[0].hidden_size]) + return init_hs + + def forward(self, hs, hlens, ys): + """Calculate forward propagation. + + Args: + hs (Tensor): Batch of the sequences of padded hidden states (B, Tmax, idim). + hlens (Tensor(int64) padded): Batch of lengths of each input batch (B,). + ys (Tensor): Batch of the sequences of padded target features (B, Lmax, odim). + + Returns: + Tensor: Batch of output tensors after postnet (B, Lmax, odim). + Tensor: Batch of output tensors before postnet (B, Lmax, odim). + Tensor: Batch of logits of stop prediction (B, Lmax). + Tensor: Batch of attention weights (B, Lmax, Tmax). + + Note: + This computation is performed in teacher-forcing manner. + """ + # thin out frames (B, Lmax, odim) -> (B, Lmax/r, odim) + if self.reduction_factor > 1: + ys = ys[:, self.reduction_factor - 1::self.reduction_factor] + + # length list should be list of int + # hlens = list(map(int, hlens)) + + # initialize hidden states of decoder + c_list = [self._zero_state(hs)] + z_list = [self._zero_state(hs)] + for _ in range(1, len(self.lstm)): + c_list.append(self._zero_state(hs)) + z_list.append(self._zero_state(hs)) + prev_out = paddle.zeros([paddle.shape(hs)[0], self.odim]) + + # initialize attention + prev_att_ws = [] + prev_att_w = paddle.zeros(paddle.shape(hlens)) + prev_att_ws.append(prev_att_w) + self.att.reset() + + # loop for an output sequence + outs, logits, att_ws = [], [], [] + for y in ys.transpose([1, 0, 2]): + if self.use_att_extra_inputs: + att_c, att_w = self.att(hs, hlens, z_list[0], prev_att_ws[-1], + prev_out) + else: + att_c, att_w = self.att(hs, hlens, z_list[0], prev_att_ws[-1]) + prenet_out = self.prenet( + prev_out) if self.prenet is not None else prev_out + xs = paddle.concat([att_c, prenet_out], axis=1) + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.lstm[0](xs, (z_list[0], c_list[0])) + z_list[0], c_list[0] = next_hidden + for i in range(1, len(self.lstm)): + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.lstm[i](z_list[i - 1], + (z_list[i], c_list[i])) + z_list[i], c_list[i] = next_hidden + zcs = (paddle.concat([z_list[-1], att_c], axis=1) + if self.use_concate else z_list[-1]) + outs.append( + self.feat_out(zcs).reshape([paddle.shape(hs)[0], self.odim, -1 + ])) + logits.append(self.prob_out(zcs)) + att_ws.append(att_w) + # teacher forcing + prev_out = y + if self.cumulate_att_w and paddle.sum(prev_att_w) != 0: + prev_att_w = prev_att_w + att_w # Note: error when use += + else: + prev_att_w = att_w + prev_att_ws.append(prev_att_w) + # (B, Lmax) + logits = paddle.concat(logits, axis=1) + # (B, odim, Lmax) + before_outs = paddle.concat(outs, axis=2) + # (B, Lmax, Tmax) + att_ws = paddle.stack(att_ws, axis=1) + + if self.reduction_factor > 1: + # (B, odim, Lmax) + before_outs = before_outs.reshape( + [paddle.shape(before_outs)[0], self.odim, -1]) + + if self.postnet is not None: + # (B, odim, Lmax) + after_outs = before_outs + self.postnet(before_outs) + else: + after_outs = before_outs + # (B, Lmax, odim) + before_outs = before_outs.transpose([0, 2, 1]) + # (B, Lmax, odim) + after_outs = after_outs.transpose([0, 2, 1]) + logits = logits + + # apply activation function for scaling + if self.output_activation_fn is not None: + before_outs = self.output_activation_fn(before_outs) + after_outs = self.output_activation_fn(after_outs) + + return after_outs, before_outs, logits, att_ws + + def inference( + self, + h, + threshold=0.5, + minlenratio=0.0, + maxlenratio=10.0, + use_att_constraint=False, + backward_window=None, + forward_window=None, ): + """Generate the sequence of features given the sequences of characters. + Args: + h(Tensor): Input sequence of encoder hidden states (T, C). + threshold(float, optional, optional): Threshold to stop generation. (Default value = 0.5) + minlenratio(float, optional, optional): Minimum length ratio. If set to 1.0 and the length of input is 10, + the minimum length of outputs will be 10 * 1 = 10. (Default value = 0.0) + maxlenratio(float, optional, optional): Minimum length ratio. If set to 10 and the length of input is 10, + the maximum length of outputs will be 10 * 10 = 100. (Default value = 0.0) + use_att_constraint(bool, optional): Whether to apply attention constraint introduced in `Deep Voice 3`_. (Default value = False) + backward_window(int, optional): Backward window size in attention constraint. (Default value = None) + forward_window(int, optional): (Default value = None) + + Returns: + Tensor: Output sequence of features (L, odim). + Tensor: Output sequence of stop probabilities (L,). + Tensor: Attention weights (L, T). + + Note: + This computation is performed in auto-regressive manner. + .. _`Deep Voice 3`: https://arxiv.org/abs/1710.07654 + """ + # setup + + assert len(paddle.shape(h)) == 2 + hs = h.unsqueeze(0) + ilens = paddle.shape(h)[0] + # 本来 maxlen 和 minlen 外面有 int(),防止动转静的问题此处删除 + maxlen = paddle.shape(h)[0] * maxlenratio + minlen = paddle.shape(h)[0] * minlenratio + # 本来是直接使用 threshold 的,此处为了防止动转静的问题把 threshold 转成 tensor + threshold = paddle.ones([1]) * threshold + + # initialize hidden states of decoder + c_list = [self._zero_state(hs)] + z_list = [self._zero_state(hs)] + for _ in range(1, len(self.lstm)): + c_list.append(self._zero_state(hs)) + z_list.append(self._zero_state(hs)) + prev_out = paddle.zeros([1, self.odim]) + + # initialize attention + prev_att_ws = [] + prev_att_w = paddle.zeros([ilens]) + prev_att_ws.append(prev_att_w) + + self.att.reset() + + # setup for attention constraint + if use_att_constraint: + last_attended_idx = 0 + else: + last_attended_idx = None + + # loop for an output sequence + idx = 0 + outs, att_ws, probs = [], [], [] + prob = paddle.zeros([1]) + while True: + # updated index + idx += self.reduction_factor + + # decoder calculation + if self.use_att_extra_inputs: + att_c, att_w = self.att( + hs, + ilens, + z_list[0], + prev_att_ws[-1], + prev_out, + last_attended_idx=last_attended_idx, + backward_window=backward_window, + forward_window=forward_window, ) + else: + att_c, att_w = self.att( + hs, + ilens, + z_list[0], + prev_att_ws[-1], + last_attended_idx=last_attended_idx, + backward_window=backward_window, + forward_window=forward_window, ) + + att_ws.append(att_w) + prenet_out = self.prenet( + prev_out) if self.prenet is not None else prev_out + xs = paddle.concat([att_c, prenet_out], axis=1) + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.lstm[0](xs, (z_list[0], c_list[0])) + + z_list[0], c_list[0] = next_hidden + for i in range(1, len(self.lstm)): + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.lstm[i](z_list[i - 1], + (z_list[i], c_list[i])) + z_list[i], c_list[i] = next_hidden + zcs = (paddle.concat([z_list[-1], att_c], axis=1) + if self.use_concate else z_list[-1]) + # [(1, odim, r), ...] + outs.append(self.feat_out(zcs).reshape([1, self.odim, -1])) + + prob = F.sigmoid(self.prob_out(zcs))[0] + probs.append(prob) + + if self.output_activation_fn is not None: + prev_out = self.output_activation_fn( + outs[-1][:, :, -1]) # (1, odim) + else: + prev_out = outs[-1][:, :, -1] # (1, odim) + if self.cumulate_att_w and paddle.sum(prev_att_w) != 0: + prev_att_w = prev_att_w + att_w # Note: error when use += + else: + prev_att_w = att_w + prev_att_ws.append(prev_att_w) + if use_att_constraint: + last_attended_idx = int(att_w.argmax()) + + # tacotron2 ljspeech 动转静的问题应该是这里没有正确判断 prob >= threshold 导致的 + if prob >= threshold or idx >= maxlen: + # check mininum length + if idx < minlen: + continue + break + """ + 仅解开 665~667 行的代码块,动转静时会卡死,但是动态图时可以正确生成音频,证明模型没问题 + 同时解开 665~667 行 和 668 ~ 670 行的代码块,动转静时不会卡死,但是生成的音频末尾有多余的噪声 + 证明动转静没有进入 prob >= threshold 的判断,但是静态图可以进入 prob >= threshold 并退出循环 + 动转静时是通过 idx >= maxlen 退出循环(所以没有这个逻辑的时候会一直循环,也就是卡死), + 没有在模型判断该结束的时候结束,而是在超出最大长度时结束,所以合成的音频末尾有很长的额外预测的噪声 + 动转静用 prob <= threshold 的条件可以退出循环(虽然结果不正确),证明条件参数的类型本身没问题,可能是 prob 有问题 + """ + # if prob >= threshold: + # print("prob >= threshold") + # break + # elif idx >= maxlen: + # print("idx >= maxlen") + # break + + # (1, odim, L) + outs = paddle.concat(outs, axis=2) + if self.postnet is not None: + # (1, odim, L) + outs = outs + self.postnet(outs) + # (L, odim) + outs = outs.transpose([0, 2, 1]).squeeze(0) + probs = paddle.concat(probs, axis=0) + att_ws = paddle.concat(att_ws, axis=0) + + if self.output_activation_fn is not None: + outs = self.output_activation_fn(outs) + + return outs, probs, att_ws + + def calculate_all_attentions(self, hs, hlens, ys): + """Calculate all of the attention weights. + + Args: + hs (Tensor): Batch of the sequences of padded hidden states (B, Tmax, idim). + hlens (Tensor(int64)): Batch of lengths of each input batch (B,). + ys (Tensor): Batch of the sequences of padded target features (B, Lmax, odim). + + Returns: + numpy.ndarray: + Batch of attention weights (B, Lmax, Tmax). + + Note: + This computation is performed in teacher-forcing manner. + """ + # thin out frames (B, Lmax, odim) -> (B, Lmax/r, odim) + if self.reduction_factor > 1: + ys = ys[:, self.reduction_factor - 1::self.reduction_factor] + + # length list should be list of int + hlens = list(map(int, hlens)) + + # initialize hidden states of decoder + c_list = [self._zero_state(hs)] + z_list = [self._zero_state(hs)] + for _ in range(1, len(self.lstm)): + c_list.append(self._zero_state(hs)) + z_list.append(self._zero_state(hs)) + prev_out = paddle.zeros([paddle.shape(hs)[0], self.odim]) + + # initialize attention + prev_att_w = None + self.att.reset() + + # loop for an output sequence + att_ws = [] + for y in ys.transpose([1, 0, 2]): + if self.use_att_extra_inputs: + att_c, att_w = self.att(hs, hlens, z_list[0], prev_att_w, + prev_out) + else: + att_c, att_w = self.att(hs, hlens, z_list[0], prev_att_w) + att_ws.append(att_w) + prenet_out = self.prenet( + prev_out) if self.prenet is not None else prev_out + xs = paddle.concat([att_c, prenet_out], axis=1) + # we only use the second output of LSTMCell in paddle + _, next_hidden = self.lstm[0](xs, (z_list[0], c_list[0])) + z_list[0], c_list[0] = next_hidden + for i in range(1, len(self.lstm)): + z_list[i], c_list[i] = self.lstm[i](z_list[i - 1], + (z_list[i], c_list[i])) + # teacher forcing + prev_out = y + if self.cumulate_att_w and prev_att_w is not None: + # Note: error when use += + prev_att_w = prev_att_w + att_w + else: + prev_att_w = att_w + # (B, Lmax, Tmax) + att_ws = paddle.stack(att_ws, axis=1) + + return att_ws diff --git a/ernie-sat/paddlespeech/t2s/modules/tacotron2/encoder.py b/ernie-sat/paddlespeech/t2s/modules/tacotron2/encoder.py new file mode 100644 index 0000000..db102a1 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/tacotron2/encoder.py @@ -0,0 +1,174 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Tacotron2 encoder related modules.""" +import paddle +from paddle import nn + + +class Encoder(nn.Layer): + """Encoder module of Spectrogram prediction network. + + This is a module of encoder of Spectrogram prediction network in Tacotron2, + which described in `Natural TTS Synthesis by Conditioning WaveNet on Mel + Spectrogram Predictions`_. This is the encoder which converts either a sequence + of characters or acoustic features into the sequence of hidden states. + + .. _`Natural TTS Synthesis by Conditioning WaveNet on Mel Spectrogram Predictions`: + https://arxiv.org/abs/1712.05884 + + """ + + def __init__( + self, + idim, + input_layer="embed", + embed_dim=512, + elayers=1, + eunits=512, + econv_layers=3, + econv_chans=512, + econv_filts=5, + use_batch_norm=True, + use_residual=False, + dropout_rate=0.5, + padding_idx=0, ): + """Initialize Tacotron2 encoder module. + Args: + idim (int): Dimension of the inputs. + input_layer (str): Input layer type. + embed_dim (int, optional): Dimension of character embedding. + elayers (int, optional): The number of encoder blstm layers. + eunits (int, optional): The number of encoder blstm units. + econv_layers (int, optional): The number of encoder conv layers. + econv_filts (int, optional): The number of encoder conv filter size. + econv_chans (int, optional): The number of encoder conv filter channels. + use_batch_norm (bool, optional): Whether to use batch normalization. + use_residual (bool, optional): Whether to use residual connection. + dropout_rate (float, optional): Dropout rate. + + """ + super().__init__() + # store the hyperparameters + self.idim = idim + self.use_residual = use_residual + + # define network layer modules + if input_layer == "linear": + self.embed = nn.Linear(idim, econv_chans) + elif input_layer == "embed": + self.embed = nn.Embedding(idim, embed_dim, padding_idx=padding_idx) + else: + raise ValueError("unknown input_layer: " + input_layer) + + if econv_layers > 0: + self.convs = nn.LayerList() + for layer in range(econv_layers): + ichans = (embed_dim if layer == 0 and input_layer == "embed" + else econv_chans) + if use_batch_norm: + self.convs.append( + nn.Sequential( + nn.Conv1D( + ichans, + econv_chans, + econv_filts, + stride=1, + padding=(econv_filts - 1) // 2, + bias_attr=False, ), + nn.BatchNorm1D(econv_chans), + nn.ReLU(), + nn.Dropout(dropout_rate), )) + else: + self.convs += [ + nn.Sequential( + nn.Conv1D( + ichans, + econv_chans, + econv_filts, + stride=1, + padding=(econv_filts - 1) // 2, + bias_attr=False, ), + nn.ReLU(), + nn.Dropout(dropout_rate), ) + ] + else: + self.convs = None + if elayers > 0: + iunits = econv_chans if econv_layers != 0 else embed_dim + # batch_first=True, bidirectional=True + self.blstm = nn.LSTM( + iunits, + eunits // 2, + elayers, + time_major=False, + direction='bidirectional', + bias_ih_attr=True, + bias_hh_attr=True) + self.blstm.flatten_parameters() + else: + self.blstm = None + + # # initialize + # self.apply(encoder_init) + + def forward(self, xs, ilens=None): + """Calculate forward propagation. + + Args: + xs (Tensor): Batch of the padded sequence. Either character ids (B, Tmax) + or acoustic feature (B, Tmax, idim * encoder_reduction_factor). + Padded value should be 0. + ilens (Tensor(int64)): Batch of lengths of each input batch (B,). + + Returns: + Tensor: Batch of the sequences of encoder states(B, Tmax, eunits). + Tensor(int64): Batch of lengths of each sequence (B,) + """ + xs = self.embed(xs).transpose([0, 2, 1]) + if self.convs is not None: + for i in range(len(self.convs)): + if self.use_residual: + xs += self.convs[i](xs) + else: + xs = self.convs[i](xs) + if self.blstm is None: + return xs.transpose([0, 2, 1]) + if not isinstance(ilens, paddle.Tensor): + ilens = paddle.to_tensor(ilens) + xs = xs.transpose([0, 2, 1]) + # for dygraph to static graph + # self.blstm.flatten_parameters() + # (B, Tmax, C) + # see https://www.paddlepaddle.org.cn/documentation/docs/zh/faq/train_cn.html#paddletorch-nn-utils-rnn-pack-padded-sequencetorch-nn-utils-rnn-pad-packed-sequenceapi + xs, _ = self.blstm(xs, sequence_length=ilens) + hlens = ilens + + return xs, hlens + + def inference(self, x): + """Inference. + + Args: + x (Tensor): The sequeunce of character ids (T,) + or acoustic feature (T, idim * encoder_reduction_factor). + + Returns: + Tensor: The sequences of encoder states(T, eunits). + + """ + xs = x.unsqueeze(0) + ilens = paddle.shape(x)[0] + + return self.forward(xs, ilens)[0][0] diff --git a/ernie-sat/paddlespeech/t2s/modules/tade_res_block.py b/ernie-sat/paddlespeech/t2s/modules/tade_res_block.py new file mode 100644 index 0000000..b2275e2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/tade_res_block.py @@ -0,0 +1,157 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""StyleMelGAN's TADEResBlock Modules.""" +from functools import partial + +import paddle.nn.functional as F +from paddle import nn + + +class TADELayer(nn.Layer): + """TADE Layer module.""" + + def __init__( + self, + in_channels: int=64, + aux_channels: int=80, + kernel_size: int=9, + bias: bool=True, + upsample_factor: int=2, + upsample_mode: str="nearest", ): + """Initilize TADE layer.""" + super().__init__() + self.norm = nn.InstanceNorm1D( + in_channels, + momentum=0.1, + data_format="NCL", + weight_attr=False, + bias_attr=False) + self.aux_conv = nn.Sequential( + nn.Conv1D( + aux_channels, + in_channels, + kernel_size, + 1, + bias_attr=bias, + padding=(kernel_size - 1) // 2, ), ) + self.gated_conv = nn.Sequential( + nn.Conv1D( + in_channels, + in_channels * 2, + kernel_size, + 1, + bias_attr=bias, + padding=(kernel_size - 1) // 2, ), ) + self.upsample = nn.Upsample( + scale_factor=upsample_factor, mode=upsample_mode) + + def forward(self, x, c): + """Calculate forward propagation. + Args: + x (Tensor): Input tensor (B, in_channels, T). + c (Tensor): Auxiliary input tensor (B, aux_channels, T). + Returns: + Tensor: Output tensor (B, in_channels, T * upsample_factor). + Tensor: Upsampled aux tensor (B, in_channels, T * upsample_factor). + """ + + x = self.norm(x) + # 'bilinear', 'bicubic' and 'nearest' only support 4-D tensor. + c = self.upsample(c.unsqueeze(-1)) + c = c[:, :, :, 0] + + c = self.aux_conv(c) + cg = self.gated_conv(c) + cg1, cg2 = cg.split(2, axis=1) + # 'bilinear', 'bicubic' and 'nearest' only support 4-D tensor. + y = cg1 * self.upsample(x.unsqueeze(-1))[:, :, :, 0] + cg2 + return y, c + + +class TADEResBlock(nn.Layer): + """TADEResBlock module.""" + + def __init__( + self, + in_channels: int=64, + aux_channels: int=80, + kernel_size: int=9, + dilation: int=2, + bias: bool=True, + upsample_factor: int=2, + # this is a diff in paddle, the mode only can be "linear" when input is 3D + upsample_mode: str="nearest", + gated_function: str="softmax", ): + """Initialize TADEResBlock module.""" + super().__init__() + self.tade1 = TADELayer( + in_channels=in_channels, + aux_channels=aux_channels, + kernel_size=kernel_size, + bias=bias, + upsample_factor=1, + upsample_mode=upsample_mode, ) + self.gated_conv1 = nn.Conv1D( + in_channels, + in_channels * 2, + kernel_size, + 1, + bias_attr=bias, + padding=(kernel_size - 1) // 2, ) + self.tade2 = TADELayer( + in_channels=in_channels, + aux_channels=in_channels, + kernel_size=kernel_size, + bias=bias, + upsample_factor=upsample_factor, + upsample_mode=upsample_mode, ) + self.gated_conv2 = nn.Conv1D( + in_channels, + in_channels * 2, + kernel_size, + 1, + bias_attr=bias, + dilation=dilation, + padding=(kernel_size - 1) // 2 * dilation, ) + self.upsample = nn.Upsample( + scale_factor=upsample_factor, mode=upsample_mode) + if gated_function == "softmax": + self.gated_function = partial(F.softmax, axis=1) + elif gated_function == "sigmoid": + self.gated_function = F.sigmoid + else: + raise ValueError(f"{gated_function} is not supported.") + + def forward(self, x, c): + """Calculate forward propagation. + Args: + + x (Tensor): Input tensor (B, in_channels, T). + c (Tensor): Auxiliary input tensor (B, aux_channels, T). + Returns: + Tensor: Output tensor (B, in_channels, T * upsample_factor). + Tensor: Upsampled auxirialy tensor (B, in_channels, T * upsample_factor). + """ + residual = x + x, c = self.tade1(x, c) + x = self.gated_conv1(x) + xa, xb = x.split(2, axis=1) + x = self.gated_function(xa) * F.tanh(xb) + x, c = self.tade2(x, c) + x = self.gated_conv2(x) + xa, xb = x.split(2, axis=1) + x = self.gated_function(xa) * F.tanh(xb) + # 'bilinear', 'bicubic' and 'nearest' only support 4-D tensor. + return self.upsample(residual.unsqueeze(-1))[:, :, :, 0] + x, c diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/__init__.py b/ernie-sat/paddlespeech/t2s/modules/transformer/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/attention.py b/ernie-sat/paddlespeech/t2s/modules/transformer/attention.py new file mode 100644 index 0000000..cdb95b2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/attention.py @@ -0,0 +1,222 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Multi-Head Attention layer definition.""" +import math + +import numpy +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.masked_fill import masked_fill + + +class MultiHeadedAttention(nn.Layer): + """Multi-Head Attention layer. + Args: + n_head (int): The number of heads. + n_feat (int): The number of features. + dropout_rate (float): Dropout rate. + """ + + def __init__(self, n_head, n_feat, dropout_rate): + """Construct an MultiHeadedAttention object.""" + super().__init__() + assert n_feat % n_head == 0 + # We assume d_v always equals d_k + self.d_k = n_feat // n_head + self.h = n_head + self.linear_q = nn.Linear(n_feat, n_feat, bias_attr=True) + self.linear_k = nn.Linear(n_feat, n_feat, bias_attr=True) + self.linear_v = nn.Linear(n_feat, n_feat, bias_attr=True) + self.linear_out = nn.Linear(n_feat, n_feat, bias_attr=True) + self.attn = None + self.dropout = nn.Dropout(p=dropout_rate) + + def forward_qkv(self, query, key, value): + """Transform query, key and value. + + Args: + query(Tensor): query tensor (#batch, time1, size). + key(Tensor): Key tensor (#batch, time2, size). + value(Tensor): Value tensor (#batch, time2, size). + + Returns: + Tensor: Transformed query tensor (#batch, n_head, time1, d_k). + Tensor: Transformed key tensor (#batch, n_head, time2, d_k). + Tensor: Transformed value tensor (#batch, n_head, time2, d_k). + """ + n_batch = paddle.shape(query)[0] + + q = paddle.reshape( + self.linear_q(query), [n_batch, -1, self.h, self.d_k]) + k = paddle.reshape(self.linear_k(key), [n_batch, -1, self.h, self.d_k]) + v = paddle.reshape( + self.linear_v(value), [n_batch, -1, self.h, self.d_k]) + + # (batch, head, time1, d_k) + q = q.transpose((0, 2, 1, 3)) + # (batch, head, time2, d_k) + k = k.transpose((0, 2, 1, 3)) + # (batch, head, time2, d_k) + v = v.transpose((0, 2, 1, 3)) + return q, k, v + + def forward_attention(self, value, scores, mask=None): + """Compute attention context vector. + + Args: + value(Tensor): Transformed value (#batch, n_head, time2, d_k). + scores(Tensor): Attention score (#batch, n_head, time1, time2). + mask(Tensor, optional): Mask (#batch, 1, time2) or (#batch, time1, time2). (Default value = None) + + Returns: + Tensor: Transformed value (#batch, time1, d_model) weighted by the attention score (#batch, time1, time2). + """ + n_batch = paddle.shape(value)[0] + softmax = paddle.nn.Softmax(axis=-1) + if mask is not None: + mask = mask.unsqueeze(1) + mask = paddle.logical_not(mask) + # assume scores.dtype==paddle.float32, we only use "float32" here + dtype = str(scores.dtype).split(".")[-1] + min_value = numpy.finfo(dtype).min + scores = masked_fill(scores, mask, min_value) + # (batch, head, time1, time2) + self.attn = softmax(scores) + self.attn = masked_fill(self.attn, mask, 0.0) + else: + # (batch, head, time1, time2) + self.attn = softmax(scores) + # (batch, head, time1, time2) + p_attn = self.dropout(self.attn) + # (batch, head, time1, time2) * (batch, head, time2, d_k) -> # (batch, head, time1, d_k) + x = paddle.matmul(p_attn, value) + # (batch, time1, d_model) + x = (paddle.reshape( + x.transpose((0, 2, 1, 3)), (n_batch, -1, self.h * self.d_k))) + # (batch, time1, d_model) + return self.linear_out(x) + + def forward(self, query, key, value, mask=None): + """Compute scaled dot product attention. + + Args: + query(Tensor): Query tensor (#batch, time1, size). + key(Tensor): Key tensor (#batch, time2, size). + value(Tensor): Value tensor (#batch, time2, size). + mask(Tensor, optional): Mask tensor (#batch, 1, time2) or (#batch, time1, time2). (Default value = None) + + Returns: + Tensor: Output tensor (#batch, time1, d_model). + """ + q, k, v = self.forward_qkv(query, key, value) + scores = paddle.matmul(q, k.transpose( + (0, 1, 3, 2))) / math.sqrt(self.d_k) + + return self.forward_attention(v, scores, mask) + + +class RelPositionMultiHeadedAttention(MultiHeadedAttention): + """Multi-Head Attention layer with relative position encoding (new implementation). + Details can be found in https://github.com/espnet/espnet/pull/2816. + Paper: https://arxiv.org/abs/1901.02860 + + Args: + n_head (int): The number of heads. + n_feat (int): The number of features. + dropout_rate (float): Dropout rate. + zero_triu (bool): Whether to zero the upper triangular part of attention matrix. + """ + + def __init__(self, n_head, n_feat, dropout_rate, zero_triu=False): + """Construct an RelPositionMultiHeadedAttention object.""" + super().__init__(n_head, n_feat, dropout_rate) + self.zero_triu = zero_triu + # linear transformation for positional encoding + self.linear_pos = nn.Linear(n_feat, n_feat, bias_attr=False) + # these two learnable bias are used in matrix c and matrix d + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + + self.pos_bias_u = paddle.create_parameter( + shape=(self.h, self.d_k), + dtype='float32', + default_initializer=paddle.nn.initializer.XavierUniform()) + self.pos_bias_v = paddle.create_parameter( + shape=(self.h, self.d_k), + dtype='float32', + default_initializer=paddle.nn.initializer.XavierUniform()) + + def rel_shift(self, x): + """Compute relative positional encoding. + Args: + x(Tensor): Input tensor (batch, head, time1, 2*time1-1). + + Returns: + Tensor:Output tensor. + """ + b, h, t1, t2 = paddle.shape(x) + zero_pad = paddle.zeros((b, h, t1, 1)) + x_padded = paddle.concat([zero_pad, x], axis=-1) + x_padded = x_padded.reshape([b, h, t2 + 1, t1]) + # only keep the positions from 0 to time2 + x = x_padded[:, :, 1:].reshape([b, h, t1, t2])[:, :, :, :t2 // 2 + 1] + + if self.zero_triu: + ones = paddle.ones((t1, t2)) + x = x * paddle.tril(ones, t2 - 1)[None, None, :, :] + + return x + + def forward(self, query, key, value, pos_emb, mask): + """Compute 'Scaled Dot Product Attention' with rel. positional encoding. + + Args: + query(Tensor): Query tensor (#batch, time1, size). + key(Tensor): Key tensor (#batch, time2, size). + value(Tensor): Value tensor (#batch, time2, size). + pos_emb(Tensor): Positional embedding tensor (#batch, 2*time1-1, size). + mask(Tensor): Mask tensor (#batch, 1, time2) or (#batch, time1, time2). + + Returns: + Tensor: Output tensor (#batch, time1, d_model). + """ + q, k, v = self.forward_qkv(query, key, value) + # (batch, time1, head, d_k) + q = q.transpose([0, 2, 1, 3]) + + n_batch_pos = paddle.shape(pos_emb)[0] + p = self.linear_pos(pos_emb).reshape( + [n_batch_pos, -1, self.h, self.d_k]) + # (batch, head, 2*time1-1, d_k) + p = p.transpose([0, 2, 1, 3]) + # (batch, head, time1, d_k) + q_with_bias_u = (q + self.pos_bias_u).transpose([0, 2, 1, 3]) + # (batch, head, time1, d_k) + q_with_bias_v = (q + self.pos_bias_v).transpose([0, 2, 1, 3]) + + # compute attention score + # first compute matrix a and matrix c + # as described in https://arxiv.org/abs/1901.02860 Section 3.3 + # (batch, head, time1, time2) + matrix_ac = paddle.matmul(q_with_bias_u, k.transpose([0, 1, 3, 2])) + + # compute matrix b and matrix d + # (batch, head, time1, 2*time1-1) + matrix_bd = paddle.matmul(q_with_bias_v, p.transpose([0, 1, 3, 2])) + matrix_bd = self.rel_shift(matrix_bd) + # (batch, head, time1, time2) + scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k) + + return self.forward_attention(v, scores, mask) diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/decoder.py b/ernie-sat/paddlespeech/t2s/modules/transformer/decoder.py new file mode 100644 index 0000000..a8db734 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/decoder.py @@ -0,0 +1,250 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +# 暂时删除了 dyminic conv +"""Decoder definition.""" +import logging +from typing import Any +from typing import List +from typing import Tuple + +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.modules.layer_norm import LayerNorm +from paddlespeech.t2s.modules.transformer.attention import MultiHeadedAttention +from paddlespeech.t2s.modules.transformer.decoder_layer import DecoderLayer +from paddlespeech.t2s.modules.transformer.embedding import PositionalEncoding +from paddlespeech.t2s.modules.transformer.lightconv import LightweightConvolution +from paddlespeech.t2s.modules.transformer.mask import subsequent_mask +from paddlespeech.t2s.modules.transformer.positionwise_feed_forward import PositionwiseFeedForward +from paddlespeech.t2s.modules.transformer.repeat import repeat + + +class Decoder(nn.Layer): + """Transfomer decoder module. + + Args: + odim (int): Output diminsion. + self_attention_layer_type (str): Self-attention layer type. + attention_dim (int): Dimention of attention. + attention_heads (int): The number of heads of multi head attention. + conv_wshare (int): The number of kernel of convolution. Only used in + self_attention_layer_type == "lightconv*" or "dynamiconv*". + conv_kernel_length (Union[int, str]):Kernel size str of convolution + (e.g. 71_71_71_71_71_71). Only used in self_attention_layer_type == "lightconv*" or "dynamiconv*". + conv_usebias (bool): Whether to use bias in convolution. Only used in + self_attention_layer_type == "lightconv*" or "dynamiconv*". + linear_units(int): The number of units of position-wise feed forward. + num_blocks (int): The number of decoder blocks. + dropout_rate (float): Dropout rate. + positional_dropout_rate (float): Dropout rate after adding positional encoding. + self_attention_dropout_rate (float): Dropout rate in self-attention. + src_attention_dropout_rate (float): Dropout rate in source-attention. + input_layer (Union[str, nn.Layer]): Input layer type. + use_output_layer (bool): Whether to use output layer. + pos_enc_class (nn.Layer): Positional encoding module class. + `PositionalEncoding `or `ScaledPositionalEncoding` + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + + """ + + def __init__( + self, + odim, + selfattention_layer_type="selfattn", + attention_dim=256, + attention_heads=4, + conv_wshare=4, + conv_kernel_length=11, + conv_usebias=False, + linear_units=2048, + num_blocks=6, + dropout_rate=0.1, + positional_dropout_rate=0.1, + self_attention_dropout_rate=0.0, + src_attention_dropout_rate=0.0, + input_layer="embed", + use_output_layer=True, + pos_enc_class=PositionalEncoding, + normalize_before=True, + concat_after=False, ): + """Construct an Decoder object.""" + nn.Layer.__init__(self) + if input_layer == "embed": + self.embed = nn.Sequential( + nn.Embedding(odim, attention_dim), + pos_enc_class(attention_dim, positional_dropout_rate), ) + elif input_layer == "linear": + self.embed = nn.Sequential( + nn.Linear(odim, attention_dim), + nn.LayerNorm(attention_dim), + nn.Dropout(dropout_rate), + nn.ReLU(), + pos_enc_class(attention_dim, positional_dropout_rate), ) + elif isinstance(input_layer, nn.Layer): + self.embed = nn.Sequential( + input_layer, + pos_enc_class(attention_dim, positional_dropout_rate)) + else: + raise NotImplementedError("only `embed` or nn.Layer is supported.") + self.normalize_before = normalize_before + + # self-attention module definition + if selfattention_layer_type == "selfattn": + logging.info("decoder self-attention layer type = self-attention") + decoder_selfattn_layer = MultiHeadedAttention + decoder_selfattn_layer_args = [ + (attention_heads, attention_dim, self_attention_dropout_rate, ) + ] * num_blocks + elif selfattention_layer_type == "lightconv": + logging.info( + "decoder self-attention layer type = lightweight convolution") + decoder_selfattn_layer = LightweightConvolution + decoder_selfattn_layer_args = [( + conv_wshare, attention_dim, self_attention_dropout_rate, + int(conv_kernel_length.split("_")[lnum]), True, conv_usebias, ) + for lnum in range(num_blocks)] + + self.decoders = repeat( + num_blocks, + lambda lnum: DecoderLayer( + attention_dim, + decoder_selfattn_layer(*decoder_selfattn_layer_args[lnum]), + MultiHeadedAttention(attention_heads, attention_dim, src_attention_dropout_rate), + PositionwiseFeedForward(attention_dim, linear_units, dropout_rate), + dropout_rate, + normalize_before, + concat_after, ), ) + self.selfattention_layer_type = selfattention_layer_type + if self.normalize_before: + self.after_norm = LayerNorm(attention_dim) + if use_output_layer: + self.output_layer = nn.Linear(attention_dim, odim) + else: + self.output_layer = None + + def forward(self, tgt, tgt_mask, memory, memory_mask): + """Forward decoder. + Args: + tgt(Tensor): Input token ids, int64 (#batch, maxlen_out) if input_layer == "embed". + In the other case, input tensor (#batch, maxlen_out, odim). + tgt_mask(Tensor): Input token mask (#batch, maxlen_out). + memory(Tensor): Encoded memory, float32 (#batch, maxlen_in, feat). + memory_mask(Tensor): Encoded memory mask (#batch, maxlen_in). + + Returns: + Tensor: + Decoded token score before softmax (#batch, maxlen_out, odim) if use_output_layer is True. + In the other case,final block outputs (#batch, maxlen_out, attention_dim). + Tensor: Score mask before softmax (#batch, maxlen_out). + + """ + x = self.embed(tgt) + x, tgt_mask, memory, memory_mask = self.decoders(x, tgt_mask, memory, + memory_mask) + if self.normalize_before: + x = self.after_norm(x) + if self.output_layer is not None: + x = self.output_layer(x) + return x, tgt_mask + + def forward_one_step(self, tgt, tgt_mask, memory, cache=None): + """Forward one step. + + Args: + tgt(Tensor): Input token ids, int64 (#batch, maxlen_out). + tgt_mask(Tensor): Input token mask (#batch, maxlen_out). + memory(Tensor): Encoded memory, float32 (#batch, maxlen_in, feat). + cache((List[Tensor]), optional): List of cached tensors. (Default value = None) + + Returns: + Tensor: Output tensor (batch, maxlen_out, odim). + List[Tensor]: List of cache tensors of each decoder layer. + + """ + x = self.embed(tgt) + if cache is None: + cache = [None] * len(self.decoders) + new_cache = [] + for c, decoder in zip(cache, self.decoders): + x, tgt_mask, memory, memory_mask = decoder( + x, tgt_mask, memory, None, cache=c) + new_cache.append(x) + + if self.normalize_before: + y = self.after_norm(x[:, -1]) + else: + y = x[:, -1] + if self.output_layer is not None: + y = F.log_softmax(self.output_layer(y), axis=-1) + + return y, new_cache + + # beam search API (see ScorerInterface) + def score(self, ys, state, x): + """Score.""" + ys_mask = subsequent_mask(len(ys)).unsqueeze(0) + if self.selfattention_layer_type != "selfattn": + # TODO(karita): implement cache + logging.warning( + f"{self.selfattention_layer_type} does not support cached decoding." + ) + state = None + logp, state = self.forward_one_step( + ys.unsqueeze(0), ys_mask, x.unsqueeze(0), cache=state) + return logp.squeeze(0), state + + # batch beam search API (see BatchScorerInterface) + def batch_score(self, + ys: paddle.Tensor, + states: List[Any], + xs: paddle.Tensor) -> Tuple[paddle.Tensor, List[Any]]: + """Score new token batch (required). + + Args: + ys(Tensor): paddle.int64 prefix tokens (n_batch, ylen). + states(List[Any]): Scorer states for prefix tokens. + xs(Tensor): The encoder feature that generates ys (n_batch, xlen, n_feat). + + Returns: + tuple[Tensor, List[Any]]: + Tuple ofbatchfied scores for next token with shape of `(n_batch, n_vocab)` and next state list for ys. + + """ + # merge states + n_batch = len(ys) + n_layers = len(self.decoders) + if states[0] is None: + batch_state = None + else: + # transpose state of [batch, layer] into [layer, batch] + batch_state = [ + paddle.stack([states[b][i] for b in range(n_batch)]) + for i in range(n_layers) + ] + + # batch decoding + ys_mask = subsequent_mask(ys.shape[-1]).unsqueeze(0) + logp, states = self.forward_one_step(ys, ys_mask, xs, cache=batch_state) + + # transpose state of [layer, batch] into [batch, layer] + state_list = [[states[i][b] for i in range(n_layers)] + for b in range(n_batch)] + return logp, state_list diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/decoder_layer.py b/ernie-sat/paddlespeech/t2s/modules/transformer/decoder_layer.py new file mode 100644 index 0000000..9a13cd7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/decoder_layer.py @@ -0,0 +1,144 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Decoder self-attention layer definition.""" +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.layer_norm import LayerNorm + + +class DecoderLayer(nn.Layer): + """Single decoder layer module. + + + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` instance can be used as the argument. + src_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` instance can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward`, `MultiLayeredConv1d`, or `Conv1dLinear` instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + + """ + + def __init__( + self, + size, + self_attn, + src_attn, + feed_forward, + dropout_rate, + normalize_before=True, + concat_after=False, ): + """Construct an DecoderLayer object.""" + super().__init__() + self.size = size + self.self_attn = self_attn + self.src_attn = src_attn + self.feed_forward = feed_forward + self.norm1 = LayerNorm(size) + self.norm2 = LayerNorm(size) + self.norm3 = LayerNorm(size) + self.dropout = nn.Dropout(dropout_rate) + self.normalize_before = normalize_before + self.concat_after = concat_after + if self.concat_after: + self.concat_linear1 = nn.Linear(size + size, size) + self.concat_linear2 = nn.Linear(size + size, size) + + def forward(self, tgt, tgt_mask, memory, memory_mask, cache=None): + """Compute decoded features. + + Args: + tgt(Tensor): Input tensor (#batch, maxlen_out, size). + tgt_mask(Tensor): Mask for input tensor (#batch, maxlen_out). + memory(Tensor): Encoded memory, float32 (#batch, maxlen_in, size). + memory_mask(Tensor): Encoded memory mask (#batch, maxlen_in). + cache(List[Tensor], optional): List of cached tensors. + Each tensor shape should be (#batch, maxlen_out - 1, size). (Default value = None) + Returns: + Tensor + Output tensor(#batch, maxlen_out, size). + Tensor + Mask for output tensor (#batch, maxlen_out). + Tensor + Encoded memory (#batch, maxlen_in, size). + Tensor + Encoded memory mask (#batch, maxlen_in). + + """ + residual = tgt + if self.normalize_before: + tgt = self.norm1(tgt) + + if cache is None: + tgt_q = tgt + tgt_q_mask = tgt_mask + else: + # compute only the last frame query keeping dim: max_time_out -> 1 + assert cache.shape == [ + tgt.shape[0], + tgt.shape[1] - 1, + self.size, + ], f"{cache.shape} == {(tgt.shape[0], tgt.shape[1] - 1, self.size)}" + tgt_q = tgt[:, -1:, :] + residual = residual[:, -1:, :] + tgt_q_mask = None + if tgt_mask is not None: + tgt_mask = paddle.cast(tgt_mask, dtype="int64") + tgt_q_mask = tgt_mask[:, -1:, :] + tgt_q_mask = paddle.cast(tgt_q_mask, dtype="bool") + + if self.concat_after: + tgt_concat = paddle.concat( + (tgt_q, self.self_attn(tgt_q, tgt, tgt, tgt_q_mask)), axis=-1) + x = residual + self.concat_linear1(tgt_concat) + else: + x = residual + self.dropout( + self.self_attn(tgt_q, tgt, tgt, tgt_q_mask)) + if not self.normalize_before: + x = self.norm1(x) + + residual = x + if self.normalize_before: + x = self.norm2(x) + if self.concat_after: + x_concat = paddle.concat( + (x, self.src_attn(x, memory, memory, memory_mask)), axis=-1) + x = residual + self.concat_linear2(x_concat) + else: + x = residual + self.dropout( + self.src_attn(x, memory, memory, memory_mask)) + if not self.normalize_before: + x = self.norm2(x) + + residual = x + if self.normalize_before: + x = self.norm3(x) + x = residual + self.dropout(self.feed_forward(x)) + if not self.normalize_before: + x = self.norm3(x) + + if cache is not None: + x = paddle.concat([cache, x], axis=1) + + return x, tgt_mask, memory, memory_mask diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/embedding.py b/ernie-sat/paddlespeech/t2s/modules/transformer/embedding.py new file mode 100644 index 0000000..d9339d2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/embedding.py @@ -0,0 +1,187 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Positional Encoding Module.""" +import math + +import paddle +from paddle import nn + + +class PositionalEncoding(nn.Layer): + """Positional encoding. + + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int): Maximum input length. + reverse (bool): Whether to reverse the input position. + type (str): dtype of param + """ + + def __init__(self, + d_model, + dropout_rate, + max_len=5000, + dtype="float32", + reverse=False): + """Construct an PositionalEncoding object.""" + super().__init__() + self.d_model = d_model + self.reverse = reverse + self.xscale = math.sqrt(self.d_model) + self.dropout = nn.Dropout(p=dropout_rate) + self.pe = None + self.dtype = dtype + self.extend_pe(paddle.expand(paddle.zeros([1]), (1, max_len))) + + def extend_pe(self, x): + """Reset the positional encodings.""" + x_shape = paddle.shape(x) + pe = paddle.zeros([x_shape[1], self.d_model]) + if self.reverse: + position = paddle.arange( + x_shape[1] - 1, -1, -1.0, dtype=self.dtype).unsqueeze(1) + else: + position = paddle.arange( + 0, x_shape[1], dtype=self.dtype).unsqueeze(1) + div_term = paddle.exp( + paddle.arange(0, self.d_model, 2, dtype=self.dtype) * + -(math.log(10000.0) / self.d_model)) + pe[:, 0::2] = paddle.sin(position * div_term) + pe[:, 1::2] = paddle.cos(position * div_term) + pe = pe.unsqueeze(0) + self.pe = pe + + def forward(self, x: paddle.Tensor): + """Add positional encoding. + + Args: + x (Tensor): Input tensor (batch, time, `*`). + + Returns: + Tensor: Encoded tensor (batch, time, `*`). + """ + self.extend_pe(x) + T = paddle.shape(x)[1] + x = x * self.xscale + self.pe[:, :T] + return self.dropout(x) + + +class ScaledPositionalEncoding(PositionalEncoding): + """Scaled positional encoding module. + See Sec. 3.2 https://arxiv.org/abs/1809.08895 + + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int): Maximum input length. + dtype (str): dtype of param + """ + + def __init__(self, d_model, dropout_rate, max_len=5000, dtype="float32"): + """Initialize class.""" + super().__init__( + d_model=d_model, + dropout_rate=dropout_rate, + max_len=max_len, + dtype=dtype) + x = paddle.ones([1], dtype=self.dtype) + self.alpha = paddle.create_parameter( + shape=x.shape, + dtype=self.dtype, + default_initializer=nn.initializer.Assign(x)) + + def reset_parameters(self): + """Reset parameters.""" + self.alpha = paddle.ones([1]) + + def forward(self, x): + """Add positional encoding. + + Args: + x (Tensor): Input tensor (batch, time, `*`). + Returns: + Tensor: Encoded tensor (batch, time, `*`). + """ + self.extend_pe(x) + T = paddle.shape(x)[1] + x = x + self.alpha * self.pe[:, :T] + return self.dropout(x) + + +class RelPositionalEncoding(nn.Layer): + """Relative positional encoding module (new implementation). + Details can be found in https://github.com/espnet/espnet/pull/2816. + See : Appendix B in https://arxiv.org/abs/1901.02860 + + Args: + d_model (int): Embedding dimension. + dropout_rate (float): Dropout rate. + max_len (int): Maximum input length. + """ + + def __init__(self, d_model, dropout_rate, max_len=5000, dtype="float32"): + """Construct an PositionalEncoding object.""" + super().__init__() + self.d_model = d_model + self.xscale = math.sqrt(self.d_model) + self.dropout = nn.Dropout(p=dropout_rate) + self.pe = None + self.dtype = dtype + self.extend_pe(paddle.expand(paddle.zeros([1]), (1, max_len))) + + def extend_pe(self, x): + """Reset the positional encodings.""" + if self.pe is not None: + # self.pe contains both positive and negative parts + # the length of self.pe is 2 * input_len - 1 + if paddle.shape(self.pe)[1] >= paddle.shape(x)[1] * 2 - 1: + return + # Suppose `i` means to the position of query vecotr and `j` means the + # position of key vector. We use position relative positions when keys + # are to the left (i>j) and negative relative positions otherwise (i x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + positionwise_layer_type (str): "linear", "conv1d", or "conv1d-linear". + positionwise_conv_kernel_size (int): Kernel size of positionwise conv1d layer. + macaron_style (bool): Whether to use macaron style for positionwise layer. + pos_enc_layer_type (str): Encoder positional encoding layer type. + selfattention_layer_type (str): Encoder attention layer type. + activation_type (str): Encoder activation function type. + use_cnn_module (bool): Whether to use convolution module. + zero_triu (bool): Whether to zero the upper triangular part of attention matrix. + cnn_module_kernel (int): Kernerl size of convolution module. + padding_idx (int): Padding idx for input_layer=embed. + stochastic_depth_rate (float): Maximum probability to skip the encoder layer. + intermediate_layers (Union[List[int], None]): indices of intermediate CTC layer. + indices start from 1. + if not None, intermediate outputs are returned (which changes return type + signature.) + encoder_type (str): "transformer", or "conformer". + """ + + def __init__(self, + idim: int, + attention_dim: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + normalize_before: bool=True, + concat_after: bool=False, + positionwise_layer_type: str="linear", + positionwise_conv_kernel_size: int=1, + macaron_style: bool=False, + pos_enc_layer_type: str="abs_pos", + selfattention_layer_type: str="selfattn", + activation_type: str="swish", + use_cnn_module: bool=False, + zero_triu: bool=False, + cnn_module_kernel: int=31, + padding_idx: int=-1, + stochastic_depth_rate: float=0.0, + intermediate_layers: Union[List[int], None]=None, + encoder_type: str="transformer"): + """Construct an Base Encoder object.""" + super().__init__() + activation = get_activation(activation_type) + pos_enc_class = self.get_pos_enc_class(pos_enc_layer_type, + selfattention_layer_type) + self.encoder_type = encoder_type + + self.conv_subsampling_factor = 1 + self.embed = self.get_embed( + idim=idim, + input_layer=input_layer, + attention_dim=attention_dim, + pos_enc_class=pos_enc_class, + dropout_rate=dropout_rate, + positional_dropout_rate=positional_dropout_rate, + padding_idx=padding_idx) + + self.normalize_before = normalize_before + + # self-attention module definition + encoder_selfattn_layer, encoder_selfattn_layer_args = self.get_encoder_selfattn_layer( + selfattention_layer_type=selfattention_layer_type, + attention_heads=attention_heads, + attention_dim=attention_dim, + attention_dropout_rate=attention_dropout_rate, + zero_triu=zero_triu, + pos_enc_layer_type=pos_enc_layer_type) + # feed-forward module definition + positionwise_layer, positionwise_layer_args = self.get_positionwise_layer( + positionwise_layer_type, attention_dim, linear_units, dropout_rate, + positionwise_conv_kernel_size, activation) + + # convolution module definition + convolution_layer = ConvolutionModule + convolution_layer_args = (attention_dim, cnn_module_kernel, activation) + + if self.encoder_type == "transformer": + self.encoders = repeat( + num_blocks, + lambda lnum: EncoderLayer( + attention_dim, + encoder_selfattn_layer(*encoder_selfattn_layer_args), + positionwise_layer(*positionwise_layer_args), + dropout_rate, + normalize_before, + concat_after, ), ) + + elif self.encoder_type == "conformer": + self.encoders = repeat( + num_blocks, + lambda lnum: ConformerEncoderLayer( + attention_dim, + encoder_selfattn_layer(*encoder_selfattn_layer_args), + positionwise_layer(*positionwise_layer_args), + positionwise_layer(*positionwise_layer_args) if macaron_style else None, + convolution_layer(*convolution_layer_args) if use_cnn_module else None, + dropout_rate, + normalize_before, + concat_after, + stochastic_depth_rate * float(1 + lnum) / num_blocks, ), ) + self.intermediate_layers = intermediate_layers + else: + raise NotImplementedError("Support only linear or conv1d.") + + if self.normalize_before: + self.after_norm = LayerNorm(attention_dim) + + def get_positionwise_layer(self, + positionwise_layer_type: str="linear", + attention_dim: int=256, + linear_units: int=2048, + dropout_rate: float=0.1, + positionwise_conv_kernel_size: int=1, + activation: nn.Layer=nn.ReLU()): + """Define positionwise layer.""" + if positionwise_layer_type == "linear": + positionwise_layer = PositionwiseFeedForward + positionwise_layer_args = (attention_dim, linear_units, + dropout_rate, activation) + elif positionwise_layer_type == "conv1d": + positionwise_layer = MultiLayeredConv1d + positionwise_layer_args = (attention_dim, linear_units, + positionwise_conv_kernel_size, + dropout_rate, ) + elif positionwise_layer_type == "conv1d-linear": + positionwise_layer = Conv1dLinear + positionwise_layer_args = (attention_dim, linear_units, + positionwise_conv_kernel_size, + dropout_rate, ) + else: + raise NotImplementedError("Support only linear or conv1d.") + return positionwise_layer, positionwise_layer_args + + def get_encoder_selfattn_layer(self, + selfattention_layer_type: str="selfattn", + attention_heads: int=4, + attention_dim: int=256, + attention_dropout_rate: float=0.0, + zero_triu: bool=False, + pos_enc_layer_type: str="abs_pos"): + if selfattention_layer_type == "selfattn": + encoder_selfattn_layer = MultiHeadedAttention + encoder_selfattn_layer_args = (attention_heads, attention_dim, + attention_dropout_rate, ) + elif selfattention_layer_type == "rel_selfattn": + assert pos_enc_layer_type == "rel_pos" + encoder_selfattn_layer = RelPositionMultiHeadedAttention + encoder_selfattn_layer_args = (attention_heads, attention_dim, + attention_dropout_rate, zero_triu, ) + else: + raise ValueError("unknown encoder_attn_layer: " + + selfattention_layer_type) + return encoder_selfattn_layer, encoder_selfattn_layer_args + + def get_pos_enc_class(self, + pos_enc_layer_type: str="abs_pos", + selfattention_layer_type: str="selfattn"): + if pos_enc_layer_type == "abs_pos": + pos_enc_class = PositionalEncoding + elif pos_enc_layer_type == "scaled_abs_pos": + pos_enc_class = ScaledPositionalEncoding + elif pos_enc_layer_type == "rel_pos": + assert selfattention_layer_type == "rel_selfattn" + pos_enc_class = RelPositionalEncoding + else: + raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type) + return pos_enc_class + + def get_embed(self, + idim, + input_layer="conv2d", + attention_dim: int=256, + pos_enc_class=PositionalEncoding, + dropout_rate: int=0.1, + positional_dropout_rate: int=0.1, + padding_idx: int=-1): + + if input_layer == "linear": + embed = nn.Sequential( + nn.Linear(idim, attention_dim), + nn.LayerNorm(attention_dim), + nn.Dropout(dropout_rate), + nn.ReLU(), + pos_enc_class(attention_dim, positional_dropout_rate), ) + elif input_layer == "conv2d": + embed = Conv2dSubsampling( + idim, + attention_dim, + dropout_rate, + pos_enc_class(attention_dim, positional_dropout_rate), ) + self.conv_subsampling_factor = 4 + elif input_layer == "embed": + embed = nn.Sequential( + nn.Embedding(idim, attention_dim, padding_idx=padding_idx), + pos_enc_class(attention_dim, positional_dropout_rate), ) + elif isinstance(input_layer, nn.Layer): + embed = nn.Sequential( + input_layer, + pos_enc_class(attention_dim, positional_dropout_rate), ) + elif input_layer is None: + embed = nn.Sequential( + pos_enc_class(attention_dim, positional_dropout_rate)) + else: + raise ValueError("unknown input_layer: " + input_layer) + + return embed + + def forward(self, xs, masks): + """Encode input sequence. + + Args: + xs (Tensor): Input tensor (#batch, time, idim). + masks (Tensor): Mask tensor (#batch, 1, time). + + Returns: + Tensor: Output tensor (#batch, time, attention_dim). + Tensor: Mask tensor (#batch, 1, time). + """ + xs = self.embed(xs) + xs, masks = self.encoders(xs, masks) + if self.normalize_before: + xs = self.after_norm(xs) + return xs, masks + + +class TransformerEncoder(BaseEncoder): + """Transformer encoder module. + + Args: + idim (int): Input dimension. + attention_dim (int): Dimention of attention. + attention_heads (int): The number of heads of multi head attention. + linear_units (int): The number of units of position-wise feed forward. + num_blocks (int): The number of decoder blocks. + dropout_rate (float): Dropout rate. + positional_dropout_rate (float): Dropout rate after adding positional encoding. + attention_dropout_rate (float): Dropout rate in attention. + input_layer (Union[str, paddle.nn.Layer]): Input layer type. + pos_enc_layer_type (str): Encoder positional encoding layer type. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + positionwise_layer_type (str): "linear", "conv1d", or "conv1d-linear". + positionwise_conv_kernel_size (int): Kernel size of positionwise conv1d layer. + selfattention_layer_type (str): Encoder attention layer type. + activation_type (str): Encoder activation function type. + padding_idx (int): Padding idx for input_layer=embed. + """ + + def __init__( + self, + idim, + attention_dim: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + pos_enc_layer_type: str="abs_pos", + normalize_before: bool=True, + concat_after: bool=False, + positionwise_layer_type: str="linear", + positionwise_conv_kernel_size: int=1, + selfattention_layer_type: str="selfattn", + activation_type: str="relu", + padding_idx: int=-1, ): + """Construct an Transformer Encoder object.""" + super().__init__( + idim, + attention_dim=attention_dim, + attention_heads=attention_heads, + linear_units=linear_units, + num_blocks=num_blocks, + dropout_rate=dropout_rate, + positional_dropout_rate=positional_dropout_rate, + attention_dropout_rate=attention_dropout_rate, + input_layer=input_layer, + pos_enc_layer_type=pos_enc_layer_type, + normalize_before=normalize_before, + concat_after=concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, + selfattention_layer_type=selfattention_layer_type, + activation_type=activation_type, + padding_idx=padding_idx, + encoder_type="transformer") + + def forward(self, xs, masks): + """Encode input sequence. + + Args: + xs(Tensor): Input tensor (#batch, time, idim). + masks(Tensor): Mask tensor (#batch, 1, time). + + Returns: + Tensor: Output tensor (#batch, time, attention_dim). + Tensor:Mask tensor (#batch, 1, time). + """ + xs = self.embed(xs) + xs, masks = self.encoders(xs, masks) + if self.normalize_before: + xs = self.after_norm(xs) + return xs, masks + + def forward_one_step(self, xs, masks, cache=None): + """Encode input frame. + + Args: + xs (Tensor): Input tensor. + masks (Tensor): Mask tensor. + cache (List[Tensor]): List of cache tensors. + + Returns: + Tensor: Output tensor. + Tensor: Mask tensor. + List[Tensor]: List of new cache tensors. + """ + + xs = self.embed(xs) + if cache is None: + cache = [None for _ in range(len(self.encoders))] + new_cache = [] + for c, e in zip(cache, self.encoders): + xs, masks = e(xs, masks, cache=c) + new_cache.append(xs) + if self.normalize_before: + xs = self.after_norm(xs) + return xs, masks, new_cache + + +class ConformerEncoder(BaseEncoder): + """Conformer encoder module. + + Args: + idim (int): Input dimension. + attention_dim (int): Dimention of attention. + attention_heads (int): The number of heads of multi head attention. + linear_units (int): The number of units of position-wise feed forward. + num_blocks (int): The number of decoder blocks. + dropout_rate (float): Dropout rate. + positional_dropout_rate (float): Dropout rate after adding positional encoding. + attention_dropout_rate (float): Dropout rate in attention. + input_layer (Union[str, nn.Layer]): Input layer type. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool):Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + positionwise_layer_type (str): "linear", "conv1d", or "conv1d-linear". + positionwise_conv_kernel_size (int): Kernel size of positionwise conv1d layer. + macaron_style (bool): Whether to use macaron style for positionwise layer. + pos_enc_layer_type (str): Encoder positional encoding layer type. + selfattention_layer_type (str): Encoder attention layer type. + activation_type (str): Encoder activation function type. + use_cnn_module (bool): Whether to use convolution module. + zero_triu (bool): Whether to zero the upper triangular part of attention matrix. + cnn_module_kernel (int): Kernerl size of convolution module. + padding_idx (int): Padding idx for input_layer=embed. + stochastic_depth_rate (float): Maximum probability to skip the encoder layer. + intermediate_layers (Union[List[int], None]):indices of intermediate CTC layer. indices start from 1. + if not None, intermediate outputs are returned (which changes return type signature.) + """ + + def __init__( + self, + idim: int, + attention_dim: int=256, + attention_heads: int=4, + linear_units: int=2048, + num_blocks: int=6, + dropout_rate: float=0.1, + positional_dropout_rate: float=0.1, + attention_dropout_rate: float=0.0, + input_layer: str="conv2d", + normalize_before: bool=True, + concat_after: bool=False, + positionwise_layer_type: str="linear", + positionwise_conv_kernel_size: int=1, + macaron_style: bool=False, + pos_enc_layer_type: str="rel_pos", + selfattention_layer_type: str="rel_selfattn", + activation_type: str="swish", + use_cnn_module: bool=False, + zero_triu: bool=False, + cnn_module_kernel: int=31, + padding_idx: int=-1, + stochastic_depth_rate: float=0.0, + intermediate_layers: Union[List[int], None]=None, ): + """Construct an Conformer Encoder object.""" + super().__init__( + idim=idim, + attention_dim=attention_dim, + attention_heads=attention_heads, + linear_units=linear_units, + num_blocks=num_blocks, + dropout_rate=dropout_rate, + positional_dropout_rate=positional_dropout_rate, + attention_dropout_rate=attention_dropout_rate, + input_layer=input_layer, + normalize_before=normalize_before, + concat_after=concat_after, + positionwise_layer_type=positionwise_layer_type, + positionwise_conv_kernel_size=positionwise_conv_kernel_size, + macaron_style=macaron_style, + pos_enc_layer_type=pos_enc_layer_type, + selfattention_layer_type=selfattention_layer_type, + activation_type=activation_type, + use_cnn_module=use_cnn_module, + zero_triu=zero_triu, + cnn_module_kernel=cnn_module_kernel, + padding_idx=padding_idx, + stochastic_depth_rate=stochastic_depth_rate, + intermediate_layers=intermediate_layers, + encoder_type="conformer") + + def forward(self, xs, masks): + """Encode input sequence. + + Args: + xs (Tensor): Input tensor (#batch, time, idim). + masks (Tensor): Mask tensor (#batch, 1, time). + Returns: + Tensor: Output tensor (#batch, time, attention_dim). + Tensor: Mask tensor (#batch, 1, time). + """ + if isinstance(self.embed, (Conv2dSubsampling)): + xs, masks = self.embed(xs, masks) + else: + xs = self.embed(xs) + + if self.intermediate_layers is None: + xs, masks = self.encoders(xs, masks) + else: + intermediate_outputs = [] + for layer_idx, encoder_layer in enumerate(self.encoders): + xs, masks = encoder_layer(xs, masks) + + if (self.intermediate_layers is not None and + layer_idx + 1 in self.intermediate_layers): + # intermediate branches also require normalization. + encoder_output = xs + if isinstance(encoder_output, tuple): + encoder_output = encoder_output[0] + if self.normalize_before: + encoder_output = self.after_norm(encoder_output) + intermediate_outputs.append(encoder_output) + + if isinstance(xs, tuple): + xs = xs[0] + + if self.normalize_before: + xs = self.after_norm(xs) + + if self.intermediate_layers is not None: + return xs, masks, intermediate_outputs + return xs, masks + + +class Conv1dResidualBlock(nn.Layer): + """ + Special module for simplified version of Encoder class. + """ + + def __init__(self, + idim: int=256, + odim: int=256, + kernel_size: int=5, + dropout_rate: float=0.2): + super().__init__() + self.main_block = nn.Sequential( + nn.Conv1D( + idim, odim, kernel_size=kernel_size, padding=kernel_size // 2), + nn.ReLU(), + nn.BatchNorm1D(odim), + nn.Dropout(p=dropout_rate)) + self.conv1d_residual = nn.Conv1D(idim, odim, kernel_size=1) + + def forward(self, xs): + """Encode input sequence. + Args: + xs (Tensor): Input tensor (#batch, idim, T). + Returns: + Tensor: Output tensor (#batch, odim, T). + """ + outputs = self.main_block(xs) + outputs = self.conv1d_residual(xs) + outputs + return outputs + + +class CNNDecoder(nn.Layer): + """ + Much simplified decoder than the original one with Prenet. + """ + + def __init__( + self, + emb_dim: int=256, + odim: int=80, + kernel_size: int=5, + dropout_rate: float=0.2, + resblock_kernel_sizes: List[int]=[256, 256], ): + + super().__init__() + + input_shape = emb_dim + out_sizes = resblock_kernel_sizes + out_sizes.append(out_sizes[-1]) + + in_sizes = [input_shape] + out_sizes[:-1] + self.residual_blocks = nn.LayerList([ + Conv1dResidualBlock( + idim=in_channels, + odim=out_channels, + kernel_size=kernel_size, + dropout_rate=dropout_rate, ) + for in_channels, out_channels in zip(in_sizes, out_sizes) + ]) + self.conv1d = nn.Conv1D( + in_channels=out_sizes[-1], out_channels=odim, kernel_size=1) + + def forward(self, xs, masks=None): + """Encode input sequence. + Args: + xs (Tensor): Input tensor (#batch, time, idim). + masks (Tensor): Mask tensor (#batch, 1, time). + Returns: + Tensor: Output tensor (#batch, time, odim). + """ + # exchange the temporal dimension and the feature dimension + xs = xs.transpose([0, 2, 1]) + if masks is not None: + xs = xs * masks + + for layer in self.residual_blocks: + outputs = layer(xs) + if masks is not None: + # input_mask B * 1 * T + outputs = outputs * masks + xs = outputs + outputs = self.conv1d(outputs) + if masks is not None: + outputs = outputs * masks + outputs = outputs.transpose([0, 2, 1]) + return outputs, masks + + +class CNNPostnet(nn.Layer): + def __init__( + self, + odim: int=80, + kernel_size: int=5, + dropout_rate: float=0.2, + resblock_kernel_sizes: List[int]=[256, 256], ): + super().__init__() + out_sizes = resblock_kernel_sizes + in_sizes = [odim] + out_sizes[:-1] + self.residual_blocks = nn.LayerList([ + Conv1dResidualBlock( + idim=in_channels, + odim=out_channels, + kernel_size=kernel_size, + dropout_rate=dropout_rate) + for in_channels, out_channels in zip(in_sizes, out_sizes) + ]) + self.conv1d = nn.Conv1D( + in_channels=out_sizes[-1], out_channels=odim, kernel_size=1) + + def forward(self, xs, masks=None): + """Encode input sequence. + Args: + xs (Tensor): Input tensor (#batch, odim, time). + masks (Tensor): Mask tensor (#batch, 1, time). + Returns: + Tensor: Output tensor (#batch, odim, time). + """ + for layer in self.residual_blocks: + outputs = layer(xs) + if masks is not None: + # input_mask B * 1 * T + outputs = outputs * masks + xs = outputs + outputs = self.conv1d(outputs) + if masks is not None: + outputs = outputs * masks + return outputs diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/encoder_layer.py b/ernie-sat/paddlespeech/t2s/modules/transformer/encoder_layer.py new file mode 100644 index 0000000..72372b6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/encoder_layer.py @@ -0,0 +1,102 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Encoder self-attention layer definition.""" +import paddle +from paddle import nn + + +class EncoderLayer(nn.Layer): + """Encoder layer module. + + Args: + size (int): Input dimension. + self_attn (nn.Layer): Self-attention module instance. + `MultiHeadedAttention` instance can be used as the argument. + feed_forward (nn.Layer): Feed-forward module instance. + `PositionwiseFeedForward`, `MultiLayeredConv1d`, or `Conv1dLinear` instance can be used as the argument. + dropout_rate (float): Dropout rate. + normalize_before (bool): Whether to use layer_norm before the first block. + concat_after (bool): Whether to concat attention layer's input and output. + if True, additional linear will be applied. + i.e. x -> x + linear(concat(x, att(x))) + if False, no additional linear will be applied. i.e. x -> x + att(x) + """ + + def __init__( + self, + size, + self_attn, + feed_forward, + dropout_rate, + normalize_before=True, + concat_after=False, ): + """Construct an EncoderLayer object.""" + super().__init__() + self.self_attn = self_attn + self.feed_forward = feed_forward + self.norm1 = nn.LayerNorm(size) + self.norm2 = nn.LayerNorm(size) + self.dropout = nn.Dropout(dropout_rate) + self.size = size + self.normalize_before = normalize_before + self.concat_after = concat_after + if self.concat_after: + self.concat_linear = nn.Linear(size + size, size, bias_attr=True) + + def forward(self, x, mask, cache=None): + """Compute encoded features. + + Args: + x(Tensor): Input tensor (#batch, time, size). + mask(Tensor): Mask tensor for the input (#batch, time). + cache(Tensor, optional): Cache tensor of the input (#batch, time - 1, size). + + Returns: + Tensor: Output tensor (#batch, time, size). + Tensor: Mask tensor (#batch, time). + """ + residual = x + if self.normalize_before: + x = self.norm1(x) + + if cache is None: + x_q = x + else: + assert cache.shape == (x.shape[0], x.shape[1] - 1, self.size) + x_q = x[:, -1:, :] + residual = residual[:, -1:, :] + mask = None if mask is None else mask[:, -1:, :] + + if self.concat_after: + x_concat = paddle.concat( + (x, self.self_attn(x_q, x, x, mask)), axis=-1) + x = residual + self.concat_linear(x_concat) + else: + + x = residual + self.dropout(self.self_attn(x_q, x, x, mask)) + if not self.normalize_before: + x = self.norm1(x) + + residual = x + if self.normalize_before: + x = self.norm2(x) + x = residual + self.dropout(self.feed_forward(x)) + if not self.normalize_before: + x = self.norm2(x) + + if cache is not None: + x = paddle.concat([cache, x], axis=1) + + return x, mask diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/lightconv.py b/ernie-sat/paddlespeech/t2s/modules/transformer/lightconv.py new file mode 100644 index 0000000..9bcc1ac --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/lightconv.py @@ -0,0 +1,141 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Lightweight Convolution Module.""" +import numpy +import paddle +import paddle.nn.functional as F +from paddle import nn + +from paddlespeech.t2s.modules.activation import get_activation +from paddlespeech.t2s.modules.masked_fill import masked_fill + +MIN_VALUE = float(numpy.finfo(numpy.float32).min) + + +class LightweightConvolution(nn.Layer): + """Lightweight Convolution layer. + + This implementation is based on + https://github.com/pytorch/fairseq/tree/master/fairseq + + Args: + wshare (int): the number of kernel of convolution + n_feat (int): the number of features + dropout_rate (float): dropout_rate + kernel_size (int): kernel size (length) + use_kernel_mask (bool): Use causal mask or not for convolution kernel + use_bias (bool): Use bias term or not. + + """ + + def __init__( + self, + wshare, + n_feat, + dropout_rate, + kernel_size, + use_kernel_mask=False, + use_bias=False, ): + """Construct Lightweight Convolution layer.""" + super().__init__() + + assert n_feat % wshare == 0 + self.wshare = wshare + self.use_kernel_mask = use_kernel_mask + self.dropout_rate = dropout_rate + self.kernel_size = kernel_size + self.padding_size = int(kernel_size / 2) + + # linear -> GLU -> lightconv -> linear + self.linear1 = nn.Linear(n_feat, n_feat * 2) + self.linear2 = nn.Linear(n_feat, n_feat) + self.act = get_activation("glu") + + # lightconv related + self.uniform_ = nn.initializer.Uniform() + self.weight = paddle.to_tensor( + numpy.random.uniform(0, 1, size=[self.wshare, 1, kernel_size]), + dtype="float32") + self.uniform_(self.weight) + self.weight = paddle.create_parameter( + shape=self.weight.shape, + dtype=str(self.weight.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign(self.weight)) + self.use_bias = use_bias + if self.use_bias: + self.bias = paddle.Tensor(n_feat) + self.bias = paddle.create_parameter( + shape=self.bias.shape, + dtype=str(self.bias.numpy().dtype), + default_initializer=paddle.nn.initializer.Assign(self.bias)) + + # mask of kernel + kernel_mask0 = paddle.zeros([self.wshare, int(kernel_size / 2)]) + kernel_mask1 = paddle.ones([self.wshare, int(kernel_size / 2 + 1)]) + self.kernel_mask = paddle.concat( + (kernel_mask1, kernel_mask0), axis=-1).unsqueeze(1) + + def forward(self, query, key, value, mask): + """Forward of 'Lightweight Convolution'. + + This function takes query, key and value but uses only query. + This is just for compatibility with self-attention layer (attention.py) + + Args: + query (Tensor): input tensor. (batch, time1, d_model) + key (Tensor): NOT USED. (batch, time2, d_model) + value (Tensor): NOT USED. (batch, time2, d_model) + mask : (Tensor): (batch, time1, time2) mask + + Return: + Tensor: ouput. (batch, time1, d_model) + + """ + # linear -> GLU -> lightconv -> linear + x = query + B, T, C = x.shape + H = self.wshare + + # first liner layer + x = self.linear1(x) + + # GLU activation + x = self.act(x) + + # lightconv + # B x C x T + x = x.transpose([0, 2, 1]).reshape([-1, H, T]) + weight = F.dropout( + self.weight, self.dropout_rate, training=self.training) + if self.use_kernel_mask: + weight = masked_fill(weight, self.kernel_mask == 0.0, float("-inf")) + # weight = weight.masked_fill(self.kernel_mask == 0.0, float("-inf")) + weight = F.softmax(weight, axis=-1) + x = F.conv1d( + x, weight, padding=self.padding_size, + groups=self.wshare).reshape([B, C, T]) + if self.use_bias: + x = x + self.bias.reshape([1, -1, 1]) + # B x T x C + x = x.transpose([0, 2, 1]) + + if mask is not None and not self.use_kernel_mask: + mask = mask.transpose([0, 2, 1]) + # x = x.masked_fill(mask == 0, 0.0) + x = masked_fill(x, mask == 0, 0.0) + + # second linear layer + x = self.linear2(x) + return x diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/mask.py b/ernie-sat/paddlespeech/t2s/modules/transformer/mask.py new file mode 100644 index 0000000..c10e6ad --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/mask.py @@ -0,0 +1,47 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Mask module.""" +import paddle + + +def subsequent_mask(size, dtype=paddle.bool): + """Create mask for subsequent steps (size, size). + + Args: + size (int): size of mask + dtype (paddle.dtype): result dtype + Return: + Tensor: + >>> subsequent_mask(3) + [[1, 0, 0], + [1, 1, 0], + [1, 1, 1]] + """ + ret = paddle.ones([size, size], dtype=dtype) + return paddle.tril(ret) + + +def target_mask(ys_in_pad, ignore_id, dtype=paddle.bool): + """Create mask for decoder self-attention. + + Args: + ys_pad (Tensor): batch of padded target sequences (B, Lmax) + ignore_id (int): index of padding + dtype (paddle.dtype): result dtype + Return: + Tensor: (B, Lmax, Lmax) + """ + ys_mask = ys_in_pad != ignore_id + m = subsequent_mask(ys_mask.shape[-1]).unsqueeze(0) + return ys_mask.unsqueeze(-2) & m diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/multi_layer_conv.py b/ernie-sat/paddlespeech/t2s/modules/transformer/multi_layer_conv.py new file mode 100644 index 0000000..d3285b6 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/multi_layer_conv.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Layer modules for FFT block in FastSpeech (Feed-forward Transformer).""" +from paddle import nn + + +class MultiLayeredConv1d(nn.Layer): + """Multi-layered conv1d for Transformer block. + + This is a module of multi-leyered conv1d designed + to replace positionwise feed-forward network + in Transforner block, which is introduced in + `FastSpeech: Fast, Robust and Controllable Text to Speech`_. + + .. _`FastSpeech: Fast, Robust and Controllable Text to Speech`: + https://arxiv.org/pdf/1905.09263.pdf + + """ + + def __init__(self, in_chans, hidden_chans, kernel_size, dropout_rate): + """Initialize MultiLayeredConv1d module. + + Args: + in_chans (int): Number of input channels. + hidden_chans (int): Number of hidden channels. + kernel_size (int): Kernel size of conv1d. + dropout_rate (float): Dropout rate. + + """ + super().__init__() + self.w_1 = nn.Conv1D( + in_chans, + hidden_chans, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, ) + self.w_2 = nn.Conv1D( + hidden_chans, + in_chans, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, ) + self.dropout = nn.Dropout(dropout_rate) + self.relu = nn.ReLU() + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Batch of input tensors (B, T, in_chans). + + Returns: + Tensor: Batch of output tensors (B, T, in_chans). + """ + x = self.relu(self.w_1(x.transpose([0, 2, 1]))).transpose([0, 2, 1]) + return self.w_2(self.dropout(x).transpose([0, 2, 1])).transpose( + [0, 2, 1]) + + +class Conv1dLinear(nn.Layer): + """Conv1D + Linear for Transformer block. + + A variant of MultiLayeredConv1d, which replaces second conv-layer to linear. + + """ + + def __init__(self, in_chans, hidden_chans, kernel_size, dropout_rate): + """Initialize Conv1dLinear module. + + Args: + in_chans (int): Number of input channels. + hidden_chans (int): Number of hidden channels. + kernel_size (int): Kernel size of conv1d. + dropout_rate (float): Dropout rate. + """ + super().__init__() + self.w_1 = nn.Conv1D( + in_chans, + hidden_chans, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, ) + self.w_2 = nn.Linear(hidden_chans, in_chans, bias_attr=True) + self.dropout = nn.Dropout(dropout_rate) + self.relu = nn.ReLU() + + def forward(self, x): + """Calculate forward propagation. + + Args: + x (Tensor): Batch of input tensors (B, T, in_chans). + + Returns: + Tensor: Batch of output tensors (B, T, in_chans). + + """ + x = self.relu(self.w_1(x.transpose([0, 2, 1]))).transpose([0, 2, 1]) + + return self.w_2(self.dropout(x)) diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/positionwise_feed_forward.py b/ernie-sat/paddlespeech/t2s/modules/transformer/positionwise_feed_forward.py new file mode 100644 index 0000000..92af685 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/positionwise_feed_forward.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Positionwise feed forward layer definition.""" +import paddle +from paddle import nn + + +class PositionwiseFeedForward(nn.Layer): + """Positionwise feed forward layer. + + Args: + idim (int): Input dimenstion. + hidden_units (int): The number of hidden units. + dropout_rate (float): Dropout rate. + """ + + def __init__(self, + idim, + hidden_units, + dropout_rate, + activation=paddle.nn.ReLU()): + """Construct an PositionwiseFeedForward object.""" + super().__init__() + self.w_1 = paddle.nn.Linear(idim, hidden_units, bias_attr=True) + self.w_2 = paddle.nn.Linear(hidden_units, idim, bias_attr=True) + self.dropout = paddle.nn.Dropout(dropout_rate) + self.activation = activation + + def forward(self, x): + """Forward funciton.""" + return self.w_2(self.dropout(self.activation(self.w_1(x)))) diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/repeat.py b/ernie-sat/paddlespeech/t2s/modules/transformer/repeat.py new file mode 100644 index 0000000..1e946ad --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/repeat.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Repeat the same layer definition.""" +import paddle + + +class MultiSequential(paddle.nn.Sequential): + """Multi-input multi-output paddle.nn.Sequential.""" + + def forward(self, *args): + """Repeat.""" + for m in self: + args = m(*args) + return args + + +def repeat(N, fn): + """Repeat module N times. + + Args: + N (int): Number of repeat time. + fn (Callable): Function to generate module. + + Returns: + MultiSequential: Repeated model instance. + """ + return MultiSequential(* [fn(n) for n in range(N)]) diff --git a/ernie-sat/paddlespeech/t2s/modules/transformer/subsampling.py b/ernie-sat/paddlespeech/t2s/modules/transformer/subsampling.py new file mode 100644 index 0000000..0743970 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/transformer/subsampling.py @@ -0,0 +1,71 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +"""Subsampling layer definition.""" +import paddle +from paddle import nn + +from paddlespeech.t2s.modules.transformer.embedding import PositionalEncoding + + +class Conv2dSubsampling(nn.Layer): + """Convolutional 2D subsampling (to 1/4 length). + + Args: + idim (int): Input dimension. + odim (int): Output dimension. + dropout_rate (float): Dropout rate. + pos_enc (nn.Layer): Custom position encoding layer. + """ + + def __init__(self, idim, odim, dropout_rate, pos_enc=None): + """Construct an Conv2dSubsampling object.""" + super().__init__() + self.conv = nn.Sequential( + nn.Conv2D(1, odim, 3, 2), + nn.ReLU(), + nn.Conv2D(odim, odim, 3, 2), + nn.ReLU(), ) + self.out = nn.Sequential( + nn.Linear(odim * (((idim - 1) // 2 - 1) // 2), odim), + pos_enc if pos_enc is not None else + PositionalEncoding(odim, dropout_rate), ) + + def forward(self, x, x_mask): + """Subsample x. + Args: + x (Tensor): Input tensor (#batch, time, idim). + x_mask (Tensor): Input mask (#batch, 1, time). + Returns: + Tensor: Subsampled tensor (#batch, time', odim), where time' = time // 4. + Tensor: Subsampled mask (#batch, 1, time'), where time' = time // 4. + """ + # (b, c, t, f) + x = x.unsqueeze(1) + x = self.conv(x) + b, c, t, f = paddle.shape(x) + x = self.out(x.transpose([0, 2, 1, 3]).reshape([b, t, c * f])) + if x_mask is None: + return x, None + return x, x_mask[:, :, :-2:2][:, :, :-2:2] + + def __getitem__(self, key): + """Get item. + When reset_parameters() is called, if use_scaled_pos_enc is used, + return the positioning encoding. + """ + if key != -1: + raise NotImplementedError( + "Support only `-1` (for `reset_parameters`).") + return self.out[key] diff --git a/ernie-sat/paddlespeech/t2s/modules/upsample.py b/ernie-sat/paddlespeech/t2s/modules/upsample.py new file mode 100644 index 0000000..65e78a8 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/modules/upsample.py @@ -0,0 +1,181 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from espnet(https://github.com/espnet/espnet) +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from paddle import nn +from paddle.nn import functional as F + +from paddlespeech.t2s.modules.activation import get_activation + + +class Stretch2D(nn.Layer): + def __init__(self, w_scale: int, h_scale: int, mode: str="nearest"): + """Strech an image (or image-like object) with some interpolation. + + Args: + w_scale (int): Scalar of width. + h_scale (int): Scalar of the height. + mode (str, optional): Interpolation mode, modes suppored are "nearest", "bilinear", + "trilinear", "bicubic", "linear" and "area",by default "nearest" + For more details about interpolation, see + `paddle.nn.functional.interpolate `_. + """ + super().__init__() + self.w_scale = w_scale + self.h_scale = h_scale + self.mode = mode + + def forward(self, x): + """ + + Args: + x (Tensor): Shape (N, C, H, W) + + Returns: + Tensor: The stretched image. + Shape (N, C, H', W'), where ``H'=h_scale * H``, ``W'=w_scale * W``. + + """ + out = F.interpolate( + x, scale_factor=(self.h_scale, self.w_scale), mode=self.mode) + return out + + +class UpsampleNet(nn.Layer): + """A Layer to upsample spectrogram by applying consecutive stretch and + convolutions. + + Args: + upsample_scales (List[int]): Upsampling factors for each strech. + nonlinear_activation (Optional[str], optional): Activation after each convolution, by default None + nonlinear_activation_params (Dict[str, Any], optional): Parameters passed to construct the activation, by default {} + interpolate_mode (str, optional): Interpolation mode of the strech, by default "nearest" + freq_axis_kernel_size (int, optional): Convolution kernel size along the frequency axis, by default 1 + use_causal_conv (bool, optional): Whether to use causal padding before convolution, by default False + If True, Causal padding is used along the time axis, + i.e. padding amount is ``receptive field - 1`` and 0 for before and after, respectively. + If False, "same" padding is used along the time axis. + """ + + def __init__(self, + upsample_scales: List[int], + nonlinear_activation: Optional[str]=None, + nonlinear_activation_params: Dict[str, Any]={}, + interpolate_mode: str="nearest", + freq_axis_kernel_size: int=1, + use_causal_conv: bool=False): + super().__init__() + self.use_causal_conv = use_causal_conv + self.up_layers = nn.LayerList() + + for scale in upsample_scales: + stretch = Stretch2D(scale, 1, interpolate_mode) + assert freq_axis_kernel_size % 2 == 1 + freq_axis_padding = (freq_axis_kernel_size - 1) // 2 + kernel_size = (freq_axis_kernel_size, scale * 2 + 1) + if use_causal_conv: + padding = (freq_axis_padding, scale * 2) + else: + padding = (freq_axis_padding, scale) + conv = nn.Conv2D( + 1, 1, kernel_size, padding=padding, bias_attr=False) + self.up_layers.extend([stretch, conv]) + if nonlinear_activation is not None: + # for compatibility + nonlinear_activation = nonlinear_activation.lower() + + nonlinear = get_activation(nonlinear_activation, + **nonlinear_activation_params) + self.up_layers.append(nonlinear) + + def forward(self, c): + """ + Args: + c (Tensor): spectrogram. Shape (N, F, T) + + Returns: + Tensor: upsampled spectrogram. + Shape (N, F, T'), where ``T' = upsample_factor * T``, + """ + c = c.unsqueeze(1) + for f in self.up_layers: + if self.use_causal_conv and isinstance(f, nn.Conv2D): + c = f(c)[:, :, :, c.shape[-1]] + else: + c = f(c) + return c.squeeze(1) + + +class ConvInUpsampleNet(nn.Layer): + """A Layer to upsample spectrogram composed of a convolution and an + UpsampleNet. + + Args: + upsample_scales (List[int]): Upsampling factors for each strech. + nonlinear_activation (Optional[str], optional): Activation after each convolution, by default None + nonlinear_activation_params (Dict[str, Any], optional): Parameters passed to construct the activation, by default {} + interpolate_mode (str, optional): Interpolation mode of the strech, by default "nearest" + freq_axis_kernel_size (int, optional): Convolution kernel size along the frequency axis, by default 1 + aux_channels (int, optional): Feature size of the input, by default 80 + aux_context_window (int, optional): Context window of the first 1D convolution applied to the input. It + related to the kernel size of the convolution, by default 0 + If use causal convolution, the kernel size is ``window + 1``, + else the kernel size is ``2 * window + 1``. + use_causal_conv (bool, optional): Whether to use causal padding before convolution, by default False + If True, Causal padding is used along the time axis, i.e. padding + amount is ``receptive field - 1`` and 0 for before and after, respectively. + If False, "same" padding is used along the time axis. + """ + + def __init__(self, + upsample_scales: List[int], + nonlinear_activation: Optional[str]=None, + nonlinear_activation_params: Dict[str, Any]={}, + interpolate_mode: str="nearest", + freq_axis_kernel_size: int=1, + aux_channels: int=80, + aux_context_window: int=0, + use_causal_conv: bool=False): + super().__init__() + self.aux_context_window = aux_context_window + self.use_causal_conv = use_causal_conv and aux_context_window > 0 + kernel_size = aux_context_window + 1 if use_causal_conv else 2 * aux_context_window + 1 + self.conv_in = nn.Conv1D( + aux_channels, + aux_channels, + kernel_size=kernel_size, + bias_attr=False) + self.upsample = UpsampleNet( + upsample_scales=upsample_scales, + nonlinear_activation=nonlinear_activation, + nonlinear_activation_params=nonlinear_activation_params, + interpolate_mode=interpolate_mode, + freq_axis_kernel_size=freq_axis_kernel_size, + use_causal_conv=use_causal_conv) + + def forward(self, c): + """ + Args: + c (Tensor): spectrogram. Shape (N, F, T) + + Returns: + Tensors: upsampled spectrogram. Shape (N, F, T'), where ``T' = upsample_factor * T``, + """ + c_ = self.conv_in(c) + c = c_[:, :, :-self.aux_context_window] if self.use_causal_conv else c_ + return self.upsample(c) diff --git a/ernie-sat/paddlespeech/t2s/training/__init__.py b/ernie-sat/paddlespeech/t2s/training/__init__.py new file mode 100644 index 0000000..719e844 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .cli import * +from .experiment import * diff --git a/ernie-sat/paddlespeech/t2s/training/cli.py b/ernie-sat/paddlespeech/t2s/training/cli.py new file mode 100644 index 0000000..83dae11 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/cli.py @@ -0,0 +1,62 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse + + +def default_argument_parser(): + r"""A simple yet genral argument parser for experiments with t2s. + + This is used in examples with t2s. And it is intended to be used by + other experiments with t2s. It requires a minimal set of command line + arguments to start a training script. + + The ``--config`` and ``--opts`` are used for overwrite the deault + configuration. + + The ``--data`` and ``--output`` specifies the data path and output path. + Resuming training from existing progress at the output directory is the + intended default behavior. + + The ``--checkpoint_path`` specifies the checkpoint to load from. + + The ``--ngpu`` specifies how to run the training. + + See Also + -------- + paddlespeech.t2s.training.experiment + + Returns + ------- + argparse.ArgumentParser + the parser + """ + parser = argparse.ArgumentParser() + + # yapf: disable + # data and outpu + parser.add_argument("--config", metavar="FILE", help="path of the config file to overwrite to default config with.") + parser.add_argument("--data", metavar="DATA_DIR", help="path to the datatset.") + parser.add_argument("--output", metavar="OUTPUT_DIR", help="path to save checkpoint and logs.") + + # load from saved checkpoint + parser.add_argument("--checkpoint_path", type=str, help="path of the checkpoint to load") + + # running + parser.add_argument("--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + + # overwrite extra config and default config + parser.add_argument("--opts", nargs=argparse.REMAINDER, help="options to overwrite --config file and the default config, passing in KEY VALUE pairs") + # yapd: enable + + return parser diff --git a/ernie-sat/paddlespeech/t2s/training/default_config.py b/ernie-sat/paddlespeech/t2s/training/default_config.py new file mode 100644 index 0000000..7deb795 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/default_config.py @@ -0,0 +1,25 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from yacs.config import CfgNode + +_C = CfgNode( + dict( + valid_interval=1000, # validation + save_interval=10000, # checkpoint + max_iteration=900000, # max iteration to train + )) + + +def get_default_training_config(): + return _C.clone() diff --git a/ernie-sat/paddlespeech/t2s/training/experiment.py b/ernie-sat/paddlespeech/t2s/training/experiment.py new file mode 100644 index 0000000..05a363f --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/experiment.py @@ -0,0 +1,303 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import sys +from pathlib import Path + +import paddle +from paddle import distributed as dist +from paddle.io import DistributedBatchSampler +from visualdl import LogWriter + +from paddlespeech.t2s.utils import checkpoint +from paddlespeech.t2s.utils import mp_tools + +__all__ = ["ExperimentBase"] + + +class ExperimentBase(object): + """ + An experiment template in order to structure the training code and take + care of saving, loading, logging, visualization stuffs. It's intended to + be flexible and simple. + + So it only handles output directory (create directory for the output, + create a checkpoint directory, dump the config in use and create + visualizer and logger) in a standard way without enforcing any + input-output protocols to the model and dataloader. It leaves the main + part for the user to implement their own (setup the model, criterion, + optimizer, define a training step, define a validation function and + customize all the text and visual logs). + + It does not save too much boilerplate code. The users still have to write + the forward/backward/update mannually, but they are free to add + non-standard behaviors if needed. + + We have some conventions to follow. + 1. Experiment should have ``model``, ``optimizer``, ``train_loader`` and + ``valid_loader``, ``config`` and ``args`` attributes. + 2. The config should have a ``training`` field, which has + ``valid_interval``, ``save_interval`` and ``max_iteration`` keys. It is + used as the trigger to invoke validation, checkpointing and stop of the + experiment. + 3. There are four methods, namely ``train_batch``, ``valid``, + ``setup_model`` and ``setup_dataloader`` that should be implemented. + + Feel free to add/overwrite other methods and standalone functions if you + need. + + Args: + config (yacs.config.CfgNode): The configuration used for the experiment. + args (argparse.Namespace): The parsed command line arguments. + + Examples: + >>> def main_sp(config, args): + >>> exp = Experiment(config, args) + >>> exp.setup() + >>> exe.resume_or_load() + >>> exp.run() + >>> + >>> config = get_cfg_defaults() + >>> parser = default_argument_parser() + >>> args = parser.parse_args() + >>> if args.config: + >>> config.merge_from_file(args.config) + >>> if args.opts: + >>> config.merge_from_list(args.opts) + >>> config.freeze() + >>> + >>> if args.ngpu > 1: + >>> dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + >>> else: + >>> main_sp(config, args) + """ + + def __init__(self, config, args): + self.config = config + self.args = args + + self.model = None + self.optimizer = None + self.iteration = 0 + self.epoch = 0 + self.train_loader = None + self.valid_loader = None + self.iterator = None + self.logger = None + self.visualizer = None + self.output_dir = None + self.checkpoint_dir = None + + def setup(self): + """Setup the experiment. + """ + if self.args.ngpu == 0: + paddle.set_device("cpu") + elif self.args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + if self.parallel: + self.init_parallel() + + self.setup_output_dir() + self.dump_config() + self.setup_visualizer() + self.setup_logger() + self.setup_checkpointer() + + self.setup_dataloader() + self.setup_model() + + self.iteration = 0 + self.epoch = 0 + + @property + def parallel(self): + """A flag indicating whether the experiment should run with + multiprocessing. + """ + return self.args.ngpu > 1 + + def init_parallel(self): + """Init environment for multiprocess training. + """ + dist.init_parallel_env() + + @mp_tools.rank_zero_only + def save(self): + """Save checkpoint (model parameters and optimizer states). + """ + checkpoint.save_parameters(self.checkpoint_dir, self.iteration, + self.model, self.optimizer) + + def resume_or_load(self): + """Resume from latest checkpoint at checkpoints in the output + directory or load a specified checkpoint. + + If ``args.checkpoint_path`` is not None, load the checkpoint, else + resume training. + """ + iteration = checkpoint.load_parameters( + self.model, + self.optimizer, + checkpoint_dir=self.checkpoint_dir, + checkpoint_path=self.args.checkpoint_path) + self.iteration = iteration + + def read_batch(self): + """Read a batch from the train_loader. + + Returns + ------- + List[Tensor] + A batch. + """ + try: + batch = next(self.iterator) + except StopIteration: + self.new_epoch() + batch = next(self.iterator) + return batch + + def new_epoch(self): + """Reset the train loader and increment ``epoch``. + """ + self.epoch += 1 + if self.parallel and isinstance(self.train_loader.batch_sampler, + DistributedBatchSampler): + self.train_loader.batch_sampler.set_epoch(self.epoch) + self.iterator = iter(self.train_loader) + + def train(self): + """The training process. + + It includes forward/backward/update and periodical validation and + saving. + """ + self.new_epoch() + while self.iteration < self.config.training.max_iteration: + self.iteration += 1 + self.train_batch() + + if self.iteration % self.config.training.valid_interval == 0: + self.valid() + + if self.iteration % self.config.training.save_interval == 0: + self.save() + + def run(self): + """The routine of the experiment after setup. This method is intended + to be used by the user. + """ + try: + self.train() + except KeyboardInterrupt as exception: + # delete this, because it can not save a complete model + # self.save() + self.close() + sys.exit(exception) + finally: + self.close() + + def setup_output_dir(self): + """Create a directory used for output. + """ + # output dir + output_dir = Path(self.args.output).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + + self.output_dir = output_dir + + def setup_checkpointer(self): + """Create a directory used to save checkpoints into. + + It is "checkpoints" inside the output directory. + """ + # checkpoint dir + checkpoint_dir = self.output_dir / "checkpoints" + checkpoint_dir.mkdir(exist_ok=True) + + self.checkpoint_dir = checkpoint_dir + + @mp_tools.rank_zero_only + def close(self): + """Close visualizer to avoid hanging after training""" + # https://github.com/pytorch/fairseq/issues/2357 + self.visualizer.close() + + @mp_tools.rank_zero_only + def setup_visualizer(self): + """Initialize a visualizer to log the experiment. + + The visual log is saved in the output directory. + + Notes + ------ + Only the main process has a visualizer with it. Use multiple + visualizers in multiprocess to write to a same log file may cause + unexpected behaviors. + """ + # visualizer + visualizer = LogWriter(logdir=str(self.output_dir)) + + self.visualizer = visualizer + + def setup_logger(self): + """Initialize a text logger to log the experiment. + + Each process has its own text logger. The logging message is write to + the standard output and a text file named ``worker_n.log`` in the + output directory, where ``n`` means the rank of the process. + """ + logger = logging.getLogger(__name__) + logger.setLevel("INFO") + log_file = self.output_dir / 'worker_{}.log'.format(dist.get_rank()) + logger.addHandler(logging.FileHandler(str(log_file))) + + self.logger = logger + + @mp_tools.rank_zero_only + def dump_config(self): + """Save the configuration used for this experiment. + + It is saved in to ``config.yaml`` in the output directory at the + beginning of the experiment. + """ + with open(self.output_dir / "config.yaml", 'wt') as f: + print(self.config, file=f) + + def train_batch(self): + """The training loop. A subclass should implement this method. + """ + raise NotImplementedError("train_batch should be implemented.") + + @mp_tools.rank_zero_only + @paddle.no_grad() + def valid(self): + """The validation. A subclass should implement this method. + """ + raise NotImplementedError("valid should be implemented.") + + def setup_model(self): + """Setup model, criterion and optimizer, etc. A subclass should + implement this method. + """ + raise NotImplementedError("setup_model should be implemented.") + + def setup_dataloader(self): + """Setup training dataloader and validation dataloader. A subclass + should implement this method. + """ + raise NotImplementedError("setup_dataloader should be implemented.") diff --git a/ernie-sat/paddlespeech/t2s/training/extension.py b/ernie-sat/paddlespeech/t2s/training/extension.py new file mode 100644 index 0000000..3f755a7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/extension.py @@ -0,0 +1,80 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from typing import Callable + +PRIORITY_WRITER = 300 +PRIORITY_EDITOR = 200 +PRIORITY_READER = 100 + + +class Extension(object): + """Extension to customize the behavior of Trainer.""" + trigger = (1, 'iteration') + priority = PRIORITY_READER + name = None + + @property + def default_name(self): + """Default name of the extension, class name by default.""" + return type(self).__name__ + + def __call__(self, trainer): + """Main action of the extention. After each update, it is executed + when the trigger fires.""" + raise NotImplementedError( + 'Extension implementation must override __call__.') + + def initialize(self, trainer): + """Action that is executed once to get the corect trainer state. + It is called before training normally, but if the trainer restores + states with an Snapshot extension, this method should also be called.g + """ + pass + + def on_error(self, trainer, exc, tb): + """Handles the error raised during training before finalization. + """ + pass + + def finalize(self, trainer): + """Action that is executed when training is done. + For example, visualizers would need to be closed. + """ + pass + + +def make_extension(trigger: Callable=None, + default_name: str=None, + priority: int=None, + finalizer: Callable=None, + initializer: Callable=None, + on_error: Callable=None): + """Make an Extension-like object by injecting required attributes to it. + """ + if trigger is None: + trigger = Extension.trigger + if priority is None: + priority = Extension.priority + + def decorator(ext): + ext.trigger = trigger + ext.default_name = default_name or ext.__name__ + ext.priority = priority + ext.finalize = finalizer + ext.on_error = on_error + ext.initialize = initializer + return ext + + return decorator diff --git a/ernie-sat/paddlespeech/t2s/training/extensions/__init__.py b/ernie-sat/paddlespeech/t2s/training/extensions/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/extensions/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/training/extensions/evaluator.py b/ernie-sat/paddlespeech/t2s/training/extensions/evaluator.py new file mode 100644 index 0000000..3940dff --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/extensions/evaluator.py @@ -0,0 +1,72 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +from typing import Dict + +import paddle +from paddle.io import DataLoader +from paddle.nn import Layer + +from paddlespeech.t2s.training import extension +from paddlespeech.t2s.training.reporter import DictSummary +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.reporter import scope + + +class StandardEvaluator(extension.Extension): + + trigger = (1, 'epoch') + default_name = 'validation' + priority = extension.PRIORITY_WRITER + + name = None + + def __init__(self, model: Layer, dataloader: DataLoader): + # it is designed to hold multiple models + models = {"main": model} + self.models: Dict[str, Layer] = models + self.model = model + + # dataloaders + self.dataloader = dataloader + + def evaluate_core(self, batch): + # compute + self.model(batch) # you may report here + + def evaluate(self): + # switch to eval mode + for layer in self.models.values(): + layer.eval() + + # to average evaluation metrics + summary = DictSummary() + for batch in self.dataloader: + observation = {} + with scope(observation): + # main evaluation computation here. + with paddle.no_grad(): + self.evaluate_core(batch) + summary.add(observation) + summary = summary.compute_mean() + return summary + + def __call__(self, trainer=None): + # evaluate and report the averaged metric to current observation + # if it is used to extend a trainer, the metrics is reported to + # to observation of the trainer + # or otherwise, you can use your own observation + summary = self.evaluate() + for k, v in summary.items(): + report(k, v) diff --git a/ernie-sat/paddlespeech/t2s/training/extensions/snapshot.py b/ernie-sat/paddlespeech/t2s/training/extensions/snapshot.py new file mode 100644 index 0000000..5f8d3c4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/extensions/snapshot.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import logging +import os +from datetime import datetime +from pathlib import Path +from typing import Any +from typing import Dict +from typing import List + +import jsonlines + +from paddlespeech.t2s.training import extension +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.t2s.utils.mp_tools import rank_zero_only + + +def load_records(records_fp): + """Load record files (json lines.)""" + with jsonlines.open(records_fp, 'r') as reader: + records = list(reader) + return records + + +class Snapshot(extension.Extension): + """An extension to make snapshot of the updater object inside + the trainer. It is done by calling the updater's `save` method. + + An Updater save its state_dict by default, which contains the + updater state, (i.e. epoch and iteration) and all the model + parameters and optimizer states. If the updater inside the trainer + subclasses StandardUpdater, everything is good to go. + + Arsg: + checkpoint_dir (Union[str, Path]): The directory to save checkpoints into. + """ + + trigger = (1, 'epoch') + priority = -100 + default_name = "snapshot" + + def __init__(self, max_size: int=5, snapshot_on_error: bool=False): + self.records: List[Dict[str, Any]] = [] + self.max_size = max_size + self._snapshot_on_error = snapshot_on_error + self._save_all = (max_size == -1) + self.checkpoint_dir = None + + def initialize(self, trainer: Trainer): + """Setting up this extention.""" + self.checkpoint_dir = trainer.out / "checkpoints" + + # load existing records + record_path: Path = self.checkpoint_dir / "records.jsonl" + if record_path.exists(): + logging.debug("Loading from an existing checkpoint dir") + self.records = load_records(record_path) + trainer.updater.load(self.records[-1]['path']) + + def on_error(self, trainer, exc, tb): + if self._snapshot_on_error: + self.save_checkpoint_and_update(trainer) + + def __call__(self, trainer: Trainer): + self.save_checkpoint_and_update(trainer) + + def full(self): + """Whether the number of snapshots it keeps track of is greater + than the max_size.""" + return (not self._save_all) and len(self.records) > self.max_size + + @rank_zero_only + def save_checkpoint_and_update(self, trainer: Trainer): + """Saving new snapshot and remove the oldest snapshot if needed.""" + iteration = trainer.updater.state.iteration + path = self.checkpoint_dir / f"snapshot_iter_{iteration}.pdz" + + # add the new one + trainer.updater.save(path) + record = { + "time": str(datetime.now()), + 'path': str(path.resolve()), # use absolute path + 'iteration': iteration + } + self.records.append(record) + + # remove the earist + if self.full(): + eariest_record = self.records[0] + os.remove(eariest_record["path"]) + self.records.pop(0) + + # update the record file + record_path = self.checkpoint_dir / "records.jsonl" + with jsonlines.open(record_path, 'w') as writer: + for record in self.records: + # jsonlines.open may return a Writer or a Reader + writer.write(record) # pylint: disable=no-member diff --git a/ernie-sat/paddlespeech/t2s/training/extensions/visualizer.py b/ernie-sat/paddlespeech/t2s/training/extensions/visualizer.py new file mode 100644 index 0000000..748a7c4 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/extensions/visualizer.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from visualdl import LogWriter + +from paddlespeech.t2s.training import extension +from paddlespeech.t2s.training.trainer import Trainer + + +class VisualDL(extension.Extension): + """A wrapper of visualdl log writer. It assumes that the metrics to be visualized + are all scalars which are recorded into the `.observation` dictionary of the + trainer object. The dictionary is created for each step, thus the visualdl log + writer uses the iteration from the updater's `iteration` as the global step to + add records. + """ + trigger = (1, 'iteration') + default_name = 'visualdl' + priority = extension.PRIORITY_READER + + def __init__(self, logdir): + self.writer = LogWriter(str(logdir)) + + def __call__(self, trainer: Trainer): + for k, v in trainer.observation.items(): + self.writer.add_scalar(k, v, step=trainer.updater.state.iteration) + + def finalize(self, trainer): + self.writer.close() diff --git a/ernie-sat/paddlespeech/t2s/training/optimizer.py b/ernie-sat/paddlespeech/t2s/training/optimizer.py new file mode 100644 index 0000000..64274d5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/optimizer.py @@ -0,0 +1,52 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +from paddle import nn + +optim_classes = dict( + adadelta=paddle.optimizer.Adadelta, + adagrad=paddle.optimizer.Adagrad, + adam=paddle.optimizer.Adam, + adamax=paddle.optimizer.Adamax, + adamw=paddle.optimizer.AdamW, + lamb=paddle.optimizer.Lamb, + momentum=paddle.optimizer.Momentum, + rmsprop=paddle.optimizer.RMSProp, + sgd=paddle.optimizer.SGD, ) + + +def build_optimizers( + model: nn.Layer, + optim='adadelta', + max_grad_norm=None, + learning_rate=0.01, + weight_decay=None, + epsilon=1.0e-6, ) -> paddle.optimizer: + optim_class = optim_classes.get(optim) + if optim_class is None: + raise ValueError(f"must be one of {list(optim_classes)}: {optim}") + else: + grad_clip = None + if max_grad_norm: + grad_clip = paddle.nn.ClipGradByGlobalNorm(max_grad_norm) + optim_dict = {} + optim_dict['parameters'] = model.parameters() + optim_dict['learning_rate'] = learning_rate + optim_dict['grad_clip'] = grad_clip + optim_dict['weight_decay'] = weight_decay + if optim_class not in {'momentum', 'sgd'}: + optim_dict['epsilon'] = epsilon + optimizers = optim_class(**optim_dict) + + return optimizers diff --git a/ernie-sat/paddlespeech/t2s/training/reporter.py b/ernie-sat/paddlespeech/t2s/training/reporter.py new file mode 100644 index 0000000..a61506d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/reporter.py @@ -0,0 +1,159 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import contextlib +import math +from collections import defaultdict + +OBSERVATIONS = None + + +@contextlib.contextmanager +def scope(observations): + # make `observation` the target to report to. + # it is basically a dictionary that stores temporary observations + global OBSERVATIONS + old = OBSERVATIONS + OBSERVATIONS = observations + + try: + yield + finally: + OBSERVATIONS = old + + +def get_observations(): + global OBSERVATIONS + return OBSERVATIONS + + +def report(name, value): + # a simple function to report named value + # you can use it everywhere, it will get the default target and writ to it + # you can think of it as std.out + observations = get_observations() + if observations is None: + return + else: + observations[name] = value + + +class Summary(object): + """Online summarization of a sequence of scalars. + Summary computes the statistics of given scalars online. + """ + + def __init__(self): + self._x = 0.0 + self._x2 = 0.0 + self._n = 0 + + def add(self, value, weight=1): + """Adds a scalar value. + + Args: + value: Scalar value to accumulate. It is either a NumPy scalar or + a zero-dimensional array (on CPU or GPU). + weight: An optional weight for the value. It is a NumPy scalar or + a zero-dimensional array (on CPU or GPU). + Default is 1 (integer). + + """ + self._x += weight * value + self._x2 += weight * value * value + self._n += weight + + def compute_mean(self): + """Computes the mean.""" + x, n = self._x, self._n + return x / n + + def make_statistics(self): + """Computes and returns the mean and standard deviation values. + + Returns: + tuple: Mean and standard deviation values. + + """ + x, n = self._x, self._n + mean = x / n + var = self._x2 / n - mean * mean + std = math.sqrt(var) + return mean, std + + +class DictSummary(object): + """Online summarization of a sequence of dictionaries. + + ``DictSummary`` computes the statistics of a given set of scalars online. + It only computes the statistics for scalar values and variables of scalar + values in the dictionaries. + + """ + + def __init__(self): + self._summaries = defaultdict(Summary) + + def add(self, d): + """Adds a dictionary of scalars. + + Args: + d (dict): Dictionary of scalars to accumulate. Only elements of + scalars, zero-dimensional arrays, and variables of + zero-dimensional arrays are accumulated. When the value + is a tuple, the second element is interpreted as a weight. + + """ + summaries = self._summaries + for k, v in d.items(): + w = 1 + if isinstance(v, tuple): + w = v[1] + v = v[0] + summaries[k].add(v, weight=w) + + def compute_mean(self): + """Creates a dictionary of mean values. + + It returns a single dictionary that holds a mean value for each entry + added to the summary. + + Returns: + dict: Dictionary of mean values. + + """ + return { + name: summary.compute_mean() + for name, summary in self._summaries.items() + } + + def make_statistics(self): + """Creates a dictionary of statistics. + + It returns a single dictionary that holds mean and standard deviation + values for every entry added to the summary. For an entry of name + ``'key'``, these values are added to the dictionary by names ``'key'`` + and ``'key.std'``, respectively. + + Returns: + dict: Dictionary of statistics of all entries. + + """ + stats = {} + for name, summary in self._summaries.items(): + mean, std = summary.make_statistics() + stats[name] = mean + stats[name + '.std'] = std + + return stats diff --git a/ernie-sat/paddlespeech/t2s/training/seeding.py b/ernie-sat/paddlespeech/t2s/training/seeding.py new file mode 100644 index 0000000..8ca30fd --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/seeding.py @@ -0,0 +1,26 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import random + +import numpy as np +import paddle + + +def seed_everything(seed: int): + """Seed paddle, random and np.random to help reproductivity.""" + paddle.seed(seed) + random.seed(seed) + np.random.seed(seed) + logging.debug(f"Set the seed of paddle, random, np.random to {seed}.") diff --git a/ernie-sat/paddlespeech/t2s/training/trainer.py b/ernie-sat/paddlespeech/t2s/training/trainer.py new file mode 100644 index 0000000..9a32bca --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/trainer.py @@ -0,0 +1,202 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import traceback +from collections import OrderedDict +from pathlib import Path +from typing import Callable +from typing import List +from typing import Union + +import six + +from paddlespeech.t2s.training.extension import Extension +from paddlespeech.t2s.training.extension import PRIORITY_READER +from paddlespeech.t2s.training.reporter import scope +from paddlespeech.t2s.training.trigger import get_trigger +from paddlespeech.t2s.training.triggers.limit_trigger import LimitTrigger +from paddlespeech.t2s.training.updater import UpdaterBase +from paddlespeech.t2s.utils import profiler + + +class _ExtensionEntry(object): + def __init__(self, extension, trigger, priority): + self.extension = extension + self.trigger = trigger + self.priority = priority + + +class Trainer(object): + def __init__(self, + updater: UpdaterBase, + stop_trigger: Callable=None, + out: Union[str, Path]='result', + extensions: List[Extension]=None, + profiler_options: str=None): + self.updater = updater + self.extensions = OrderedDict() + self.stop_trigger = LimitTrigger(*stop_trigger) + self.out = Path(out) + self.observation = None + self.profiler_options = profiler_options + self._done = False + if extensions: + for ext in extensions: + self.extend(ext) + + @property + def is_before_training(self): + return self.updater.state.iteration == 0 + + def extend(self, extension, name=None, trigger=None, priority=None): + # get name for the extension + # argument \ + # -> extention's name \ + # -> default_name (class name, when it is an object) \ + # -> function name when it is a function \ + # -> error + + if name is None: + name = getattr(extension, 'name', None) + if name is None: + name = getattr(extension, 'default_name', None) + if name is None: + name = getattr(extension, '__name__', None) + if name is None: + raise ValueError("Name is not given for the extension.") + if name == 'training': + raise ValueError("training is a reserved name.") + + if trigger is None: + trigger = getattr(extension, 'trigger', (1, 'iteration')) + trigger = get_trigger(trigger) + + if priority is None: + priority = getattr(extension, 'priority', PRIORITY_READER) + + # add suffix to avoid nameing conflict + ordinal = 0 + modified_name = name + while modified_name in self.extensions: + ordinal += 1 + modified_name = f"{name}_{ordinal}" + extension.name = modified_name + + self.extensions[modified_name] = _ExtensionEntry(extension, trigger, + priority) + + def get_extension(self, name): + """get extension by name.""" + extensions = self.extensions + if name in extensions: + return extensions[name].extension + else: + raise ValueError(f'extension {name} not found') + + def run(self): + if self._done: + raise RuntimeError("Training is already done!.") + + self.out.mkdir(parents=True, exist_ok=True) + + # sort extensions by priorities once + extension_order = sorted( + self.extensions.keys(), + key=lambda name: self.extensions[name].priority, + reverse=True) + extensions = [(name, self.extensions[name]) for name in extension_order] + + # initializing all extensions + for name, entry in extensions: + if hasattr(entry.extension, "initialize"): + entry.extension.initialize(self) + + update = self.updater.update # training step + + stop_trigger = self.stop_trigger + + # display only one progress bar + max_iteration = None + if isinstance(stop_trigger, LimitTrigger): + if stop_trigger.unit == 'epoch': + max_epoch = self.stop_trigger.limit + updates_per_epoch = getattr(self.updater, "updates_per_epoch", + None) + max_iteration = max_epoch * updates_per_epoch if updates_per_epoch else None + else: + max_iteration = self.stop_trigger.limit + + try: + while not stop_trigger(self): + self.observation = {} + # set observation as the report target + # you can use report freely in Updater.update() + + # updating parameters and state + with scope(self.observation): + + update() + if self.profiler_options: + profiler.add_profiler_step(self.profiler_options) + batch_read_time = self.updater.batch_read_time + batch_time = self.updater.batch_time + avg_batch_cost = batch_read_time + batch_time + logger = self.updater.logger + logger.removeHandler(self.updater.filehandler) + msg = self.updater.msg + msg = " iter: {}/{}, ".format(self.updater.state.iteration, + max_iteration) + msg + msg += ", avg_reader_cost: {:.5f} sec, ".format( + batch_read_time + ) + "avg_batch_cost: {:.5f} sec, ".format(avg_batch_cost) + msg += "avg_samples: {}, ".format( + self.updater. + batch_size) + "avg_ips: {:.5f} sequences/sec".format( + self.updater.batch_size / avg_batch_cost) + logger.info(msg) + + # execute extension when necessary + for name, entry in extensions: + if entry.trigger(self): + entry.extension(self) + + # print("###", self.observation) + except Exception as e: + f = sys.stderr + f.write(f"Exception in main training loop: {e}\n") + f.write("Traceback (most recent call last):\n") + traceback.print_tb(sys.exc_info()[2]) + f.write( + "Trainer extensions will try to handle the extension. Then all extensions will finalize." + ) + + # capture the exception in the mian training loop + exc_info = sys.exc_info() + + # try to handle it + for name, entry in extensions: + if hasattr(entry.extension, "on_error"): + try: + entry.extension.on_error(self, e, sys.exc_info()[2]) + except Exception as ee: + f.write(f"Exception in error handler: {ee}\n") + f.write('Traceback (most recent call last):\n') + traceback.print_tb(sys.exc_info()[2]) + + # raise exception in main training loop + six.reraise(*exc_info) + finally: + for name, entry in extensions: + if hasattr(entry.extension, "finalize"): + entry.extension.finalize(self) diff --git a/ernie-sat/paddlespeech/t2s/training/trigger.py b/ernie-sat/paddlespeech/t2s/training/trigger.py new file mode 100644 index 0000000..2899562 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/trigger.py @@ -0,0 +1,28 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.t2s.training.triggers.interval_trigger import IntervalTrigger + + +def never_fail_trigger(trainer): + return False + + +def get_trigger(trigger): + if trigger is None: + return never_fail_trigger + if callable(trigger): + return trigger + else: + trigger = IntervalTrigger(*trigger) + return trigger diff --git a/ernie-sat/paddlespeech/t2s/training/triggers/__init__.py b/ernie-sat/paddlespeech/t2s/training/triggers/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/triggers/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/training/triggers/interval_trigger.py b/ernie-sat/paddlespeech/t2s/training/triggers/interval_trigger.py new file mode 100644 index 0000000..a83139b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/triggers/interval_trigger.py @@ -0,0 +1,39 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class IntervalTrigger(object): + """A Predicate to do something every N cycle.""" + + def __init__(self, period: int, unit: str): + if unit not in ("iteration", "epoch"): + raise ValueError("unit should be 'iteration' or 'epoch'") + if period <= 0: + raise ValueError("period should be a positive integer.") + self.period = period + self.unit = unit + self.last_index = None + + def __call__(self, trainer): + if self.last_index is None: + last_index = getattr(trainer.updater.state, self.unit) + self.last_index = last_index + + last_index = self.last_index + index = getattr(trainer.updater.state, self.unit) + fire = index // self.period != last_index // self.period + + self.last_index = index + return fire diff --git a/ernie-sat/paddlespeech/t2s/training/triggers/limit_trigger.py b/ernie-sat/paddlespeech/t2s/training/triggers/limit_trigger.py new file mode 100644 index 0000000..db1db77 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/triggers/limit_trigger.py @@ -0,0 +1,32 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class LimitTrigger(object): + """A Predicate to decide whether to stop.""" + + def __init__(self, limit: int, unit: str): + if unit not in ("iteration", "epoch"): + raise ValueError("unit should be 'iteration' or 'epoch'") + if limit <= 0: + raise ValueError("limit should be a positive integer.") + self.limit = limit + self.unit = unit + + def __call__(self, trainer): + state = trainer.updater.state + index = getattr(state, self.unit) + fire = index >= self.limit + return fire diff --git a/ernie-sat/paddlespeech/t2s/training/triggers/time_trigger.py b/ernie-sat/paddlespeech/t2s/training/triggers/time_trigger.py new file mode 100644 index 0000000..d712352 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/triggers/time_trigger.py @@ -0,0 +1,36 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Reference chainer MIT (https://opensource.org/licenses/MIT) + + +class TimeTrigger(object): + """Trigger based on a fixed time interval. + + This trigger accepts iterations with a given interval time. + + Args: + period (float): Interval time. It is given in seconds. + + """ + + def __init__(self, period): + self._period = period + self._next_time = self._period + + def __call__(self, trainer): + if self._next_time < trainer.elapsed_time: + self._next_time += self._period + return True + else: + return False diff --git a/ernie-sat/paddlespeech/t2s/training/updater.py b/ernie-sat/paddlespeech/t2s/training/updater.py new file mode 100644 index 0000000..a705503 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/updater.py @@ -0,0 +1,86 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import logging +from dataclasses import dataclass + +import paddle + + +@dataclass +class UpdaterState: + iteration: int = 0 + epoch: int = 0 + + +class UpdaterBase(object): + """An updater is the abstraction of how a model is trained given the + dataloader and the optimizer. + + The `update_core` method is a step in the training loop with only necessary + operations (get a batch, forward and backward, update the parameters). + + Other stuffs are made extensions. Visualization, saving, loading and + periodical validation and evaluation are not considered here. + + But even in such simplist case, things are not that simple. There is an + attempt to standardize this process and requires only the model and + dataset and do all the stuffs automatically. But this may hurt flexibility. + + If we assume a batch yield from the dataloader is just the input to the + model, we will find that some model requires more arguments, or just some + keyword arguments. But this prevents us from over-simplifying it. + + From another perspective, the batch may includes not just the input, but + also the target. But the model's forward method may just need the input. + We can pass a dict or a super-long tuple to the model and let it pick what + it really needs. But this is an abuse of lazy interface. + + After all, we care about how a model is trained. But just how the model is + used for inference. We want to control how a model is trained. We just + don't want to be messed up with other auxiliary code. + + So the best practice is to define a model and define a updater for it. + """ + + def __init__(self, init_state=None): + if init_state is None: + self.state = UpdaterState() + else: + self.state = init_state + + def update(self, batch): + raise NotImplementedError( + "Implement your own `update` method for training a step.") + + def state_dict(self): + state_dict = { + "epoch": self.state.epoch, + "iteration": self.state.iteration, + } + return state_dict + + def set_state_dict(self, state_dict): + self.state.epoch = state_dict["epoch"] + self.state.iteration = state_dict["iteration"] + + def save(self, path): + logging.debug(f"Saving to {path}.") + archive = self.state_dict() + paddle.save(archive, str(path)) + + def load(self, path): + logging.debug(f"Loading from {path}.") + archive = paddle.load(str(path)) + self.set_state_dict(archive) diff --git a/ernie-sat/paddlespeech/t2s/training/updaters/__init__.py b/ernie-sat/paddlespeech/t2s/training/updaters/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/updaters/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/t2s/training/updaters/standard_updater.py b/ernie-sat/paddlespeech/t2s/training/updaters/standard_updater.py new file mode 100644 index 0000000..b1c4862 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/training/updaters/standard_updater.py @@ -0,0 +1,200 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Modified from chainer(https://github.com/chainer/chainer) +import logging +import time +from typing import Dict +from typing import Optional + +from paddle import Tensor +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from timer import timer + +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updater import UpdaterBase +from paddlespeech.t2s.training.updater import UpdaterState + + +class StandardUpdater(UpdaterBase): + """An example of over-simplification. Things may not be that simple, but + you can subclass it to fit your need. + """ + + def __init__(self, + model: Layer, + optimizer: Optimizer, + dataloader: DataLoader, + init_state: Optional[UpdaterState]=None): + # it is designed to hold multiple models + models = {"main": model} + self.models: Dict[str, Layer] = models + self.model = model + + # it is designed to hold multiple optimizers + optimizers = {"main": optimizer} + self.optimizer = optimizer + self.optimizers: Dict[str, Optimizer] = optimizers + + # dataloaders + self.dataloader = dataloader + + # init state + if init_state is None: + self.state = UpdaterState() + else: + self.state = init_state + + self.train_iterator = iter(dataloader) + self.batch_read_time = 0 + self.batch_time = 0 + + def update(self): + # We increase the iteration index after updating and before extension. + # Here are the reasons. + + # 0. Snapshotting(as well as other extensions, like visualizer) is + # executed after a step of updating; + # 1. We decide to increase the iteration index after updating and + # before any all extension is executed. + # 3. We do not increase the iteration after extension because we + # prefer a consistent resume behavior, when load from a + # `snapshot_iter_100.pdz` then the next step to train is `101`, + # naturally. But if iteration is increased increased after + # extension(including snapshot), then, a `snapshot_iter_99` is + # loaded. You would need a extra increasing of the iteration idex + # before training to avoid another iteration `99`, which has been + # done before snapshotting. + # 4. Thus iteration index represrnts "currently how mant epochs has + # been done." + # NOTE: use report to capture the correctly value. If you want to + # report the learning rate used for a step, you must report it before + # the learning rate scheduler's step() has been called. In paddle's + # convention, we do not use an extension to change the learning rate. + # so if you want to report it, do it in the updater. + + # Then here comes the next question. When is the proper time to + # increase the epoch index? Since all extensions are executed after + # updating, it is the time that after updating is the proper time to + # increase epoch index. + # 1. If we increase the epoch index before updating, then an extension + # based ot epoch would miss the correct timing. It could only be + # triggerd after an extra updating. + # 2. Theoretically, when an epoch is done, the epoch index should be + # increased. So it would be increase after updating. + # 3. Thus, eppoch index represents "currently how many epochs has been + # done." So it starts from 0. + + # switch to training mode + for layer in self.models.values(): + layer.train() + + # training for a step is implemented here + time_before_read = time.time() + batch = self.read_batch() + time_before_core = time.time() + self.update_core(batch) + self.batch_time = time.time() - time_before_core + self.batch_read_time = time_before_core - time_before_read + if isinstance(batch, dict): + self.batch_size = len(list(batch.items())[0][-1]) + # for pwg + elif isinstance(batch, list): + self.batch_size = batch[0].shape[0] + + self.state.iteration += 1 + if self.updates_per_epoch is not None: + if self.state.iteration % self.updates_per_epoch == 0: + self.state.epoch += 1 + + def update_core(self, batch): + """A simple case for a training step. Basic assumptions are: + Single model; + Single optimizer; + A batch from the dataloader is just the input of the model; + The model return a single loss, or a dict containing serval losses. + Parameters updates at every batch, no gradient accumulation. + """ + loss = self.model(*batch) + + if isinstance(loss, Tensor): + loss_dict = {"main": loss} + else: + # Dict[str, Tensor] + loss_dict = loss + if "main" not in loss_dict: + main_loss = 0 + for loss_item in loss.values(): + main_loss += loss_item + loss_dict["main"] = main_loss + + for name, loss_item in loss_dict.items(): + report(name, float(loss_item)) + + self.optimizer.clear_gradient() + loss_dict["main"].backward() + self.optimizer.update() + + @property + def updates_per_epoch(self): + """Number of updater per epoch, determined by the length of the + dataloader.""" + length_of_dataloader = None + try: + length_of_dataloader = len(self.dataloader) + except TypeError: + logging.debug("This dataloader has no __len__.") + finally: + return length_of_dataloader + + def new_epoch(self): + """Start a new epoch.""" + # NOTE: all batch sampler for distributed training should + # subclass DistributedBatchSampler and implement `set_epoch` method + batch_sampler = self.dataloader.batch_sampler + if isinstance(batch_sampler, DistributedBatchSampler): + batch_sampler.set_epoch(self.state.epoch) + self.train_iterator = iter(self.dataloader) + + def read_batch(self): + """Read a batch from the data loader, auto renew when data is exhausted.""" + with timer() as t: + try: + batch = next(self.train_iterator) + except StopIteration: + self.new_epoch() + batch = next(self.train_iterator) + logging.debug( + f"Read a batch takes {t.elapse}s.") # replace it with logging + return batch + + def state_dict(self): + """State dict of a Updater, model, optimizer and updater state are included.""" + state_dict = super().state_dict() + for name, layer in self.models.items(): + state_dict[f"{name}_params"] = layer.state_dict() + for name, optim in self.optimizers.items(): + state_dict[f"{name}_optimizer"] = optim.state_dict() + return state_dict + + def set_state_dict(self, state_dict): + """Set state dict for a Updater. Parameters of models, states for + optimizers and UpdaterState are restored.""" + for name, layer in self.models.items(): + layer.set_state_dict(state_dict[f"{name}_params"]) + for name, optim in self.optimizers.items(): + optim.set_state_dict(state_dict[f"{name}_optimizer"]) + super().set_state_dict(state_dict) diff --git a/ernie-sat/paddlespeech/t2s/utils/__init__.py b/ernie-sat/paddlespeech/t2s/utils/__init__.py new file mode 100644 index 0000000..520c81a --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/__init__.py @@ -0,0 +1,22 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from . import checkpoint +from . import display +from . import layer_tools +from . import mp_tools +from . import scheduler + + +def str2bool(str): + return True if str.lower() == 'true' else False diff --git a/ernie-sat/paddlespeech/t2s/utils/checkpoint.py b/ernie-sat/paddlespeech/t2s/utils/checkpoint.py new file mode 100644 index 0000000..1e222c5 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/checkpoint.py @@ -0,0 +1,138 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import paddle +from paddle import distributed as dist + +from paddlespeech.t2s.utils import mp_tools + +__all__ = ["load_parameters", "save_parameters"] + + +def _load_latest_checkpoint(checkpoint_dir: str) -> int: + """Get the iteration number corresponding to the latest saved checkpoint. + + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + + Returns: + int: the latest iteration number. + """ + checkpoint_record = os.path.join(checkpoint_dir, "checkpoint") + if (not os.path.isfile(checkpoint_record)): + return 0 + + # Fetch the latest checkpoint index. + with open(checkpoint_record, "rt") as handle: + latest_checkpoint = handle.readline().split()[-1] + iteration = int(latest_checkpoint.split("-")[-1]) + + return iteration + + +def _save_checkpoint(checkpoint_dir: str, iteration: int): + """Save the iteration number of the latest model to be checkpointed. + + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + iteration (int): the latest iteration number. + + Returns: + None + """ + checkpoint_record = os.path.join(checkpoint_dir, "checkpoint") + # Update the latest checkpoint index. + with open(checkpoint_record, "wt") as handle: + handle.write("model_checkpoint_path: step-{}".format(iteration)) + + +def load_parameters(model, + optimizer=None, + checkpoint_dir=None, + checkpoint_path=None): + """Load a specific model checkpoint from disk. + + Args: + model (Layer): model to load parameters. + optimizer (Optimizer, optional): optimizer to load states if needed. + Defaults to None. + checkpoint_dir (str, optional): the directory where checkpoint is saved. + checkpoint_path (str, optional): if specified, load the checkpoint + stored in the checkpoint_path and the argument 'checkpoint_dir' will + be ignored. Defaults to None. + + Returns: + iteration (int): number of iterations that the loaded checkpoint has + been trained. + """ + if checkpoint_path is not None: + iteration = int(os.path.basename(checkpoint_path).split("-")[-1]) + elif checkpoint_dir is not None: + iteration = _load_latest_checkpoint(checkpoint_dir) + if iteration == 0: + return iteration + checkpoint_path = os.path.join(checkpoint_dir, + "step-{}".format(iteration)) + else: + raise ValueError( + "At least one of 'checkpoint_dir' and 'checkpoint_path' should be specified!" + ) + + local_rank = dist.get_rank() + + params_path = checkpoint_path + ".pdparams" + model_dict = paddle.load(params_path) + model.set_state_dict(model_dict) + print("[checkpoint] Rank {}: loaded model from {}".format(local_rank, + params_path)) + + optimizer_path = checkpoint_path + ".pdopt" + if optimizer and os.path.isfile(optimizer_path): + optimizer_dict = paddle.load(optimizer_path) + optimizer.set_state_dict(optimizer_dict) + print("[checkpoint] Rank {}: loaded optimizer state from {}".format( + local_rank, optimizer_path)) + + return iteration + + +@mp_tools.rank_zero_only +def save_parameters(checkpoint_dir, iteration, model, optimizer=None): + """Checkpoint the latest trained model parameters. + + Args: + checkpoint_dir (str): the directory where checkpoint is saved. + iteration (int): the latest iteration number. + model (Layer): model to be checkpointed. + optimizer (Optimizer, optional): optimizer to be checkpointed. + Defaults to None. + + Returns: + None + """ + checkpoint_path = os.path.join(checkpoint_dir, "step-{}".format(iteration)) + + model_dict = model.state_dict() + params_path = checkpoint_path + ".pdparams" + paddle.save(model_dict, params_path) + print("[checkpoint] Saved model to {}".format(params_path)) + + if optimizer: + opt_dict = optimizer.state_dict() + optimizer_path = checkpoint_path + ".pdopt" + paddle.save(opt_dict, optimizer_path) + print("[checkpoint] Saved optimzier state to {}".format(optimizer_path)) + + _save_checkpoint(checkpoint_dir, iteration) diff --git a/ernie-sat/paddlespeech/t2s/utils/display.py b/ernie-sat/paddlespeech/t2s/utils/display.py new file mode 100644 index 0000000..af7d44e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/display.py @@ -0,0 +1,110 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import librosa.display +import matplotlib.pylab as plt + +__all__ = [ + "plot_alignment", + "plot_spectrogram", + "plot_waveform", + "plot_multihead_alignments", + "plot_multilayer_multihead_alignments", +] + + +def plot_alignment(alignment, title=None): + # alignment: [encoder_steps, decoder_steps) + fig, ax = plt.subplots(figsize=(6, 4)) + im = ax.imshow( + alignment, aspect='auto', origin='lower', interpolation='none') + fig.colorbar(im, ax=ax) + xlabel = 'Decoder timestep' + if title is not None: + xlabel += '\n\n' + title + plt.xlabel(xlabel) + plt.ylabel('Encoder timestep') + plt.tight_layout() + return fig + + +def plot_multihead_alignments(alignments, title=None): + # alignments: [N, encoder_steps, decoder_steps) + num_subplots = alignments.shape[0] + + fig, axes = plt.subplots( + figsize=(6 * num_subplots, 4), + ncols=num_subplots, + sharey=True, + squeeze=True) + for i, ax in enumerate(axes): + im = ax.imshow( + alignments[i], aspect='auto', origin='lower', interpolation='none') + fig.colorbar(im, ax=ax) + xlabel = 'Decoder timestep' + if title is not None: + xlabel += '\n\n' + title + ax.set_xlabel(xlabel) + if i == 0: + ax.set_ylabel('Encoder timestep') + plt.tight_layout() + return fig + + +def plot_multilayer_multihead_alignments(alignments, title=None): + # alignments: [num_layers, num_heads, encoder_steps, decoder_steps) + num_layers, num_heads, *_ = alignments.shape + + fig, axes = plt.subplots( + figsize=(6 * num_heads, 4 * num_layers), + nrows=num_layers, + ncols=num_heads, + sharex=True, + sharey=True, + squeeze=True) + for i, row in enumerate(axes): + for j, ax in enumerate(row): + im = ax.imshow( + alignments[i, j], + aspect='auto', + origin='lower', + interpolation='none') + fig.colorbar(im, ax=ax) + xlabel = 'Decoder timestep' + if title is not None: + xlabel += '\n\n' + title + if i == num_layers - 1: + ax.set_xlabel(xlabel) + if j == 0: + ax.set_ylabel('Encoder timestep') + plt.tight_layout() + return fig + + +def plot_spectrogram(spec): + # spec: [C, T] librosa convention + fig, ax = plt.subplots(figsize=(12, 3)) + im = ax.imshow(spec, aspect="auto", origin="lower", interpolation='none') + plt.colorbar(im, ax=ax) + plt.xlabel("Frames") + plt.ylabel("Channels") + plt.tight_layout() + return fig + + +def plot_waveform(wav, sr=22050): + fig, ax = plt.subplots(figsize=(12, 3)) + im = librosa.display.waveplot(wav, sr=22050) + plt.colorbar(im, ax=ax) + plt.tight_layout() + return fig diff --git a/ernie-sat/paddlespeech/t2s/utils/error_rate.py b/ernie-sat/paddlespeech/t2s/utils/error_rate.py new file mode 100644 index 0000000..41b13b7 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/error_rate.py @@ -0,0 +1,206 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module provides functions to calculate error rate in different level. +e.g. wer for word-level, cer for char-level. +""" +import numpy as np + +__all__ = ['word_errors', 'char_errors', 'wer', 'cer'] + + +def _levenshtein_distance(ref, hyp): + """Levenshtein distance is a string metric for measuring the difference + between two sequences. Informally, the levenshtein disctance is defined as + the minimum number of single-character edits (substitutions, insertions or + deletions) required to change one word into the other. We can naturally + extend the edits to word level when calculate levenshtein disctance for + two sentences. + """ + m = len(ref) + n = len(hyp) + + # special case + if ref == hyp: + return 0 + if m == 0: + return n + if n == 0: + return m + + if m < n: + ref, hyp = hyp, ref + m, n = n, m + + # use O(min(m, n)) space + distance = np.zeros((2, n + 1), dtype=np.int32) + + # initialize distance matrix + for j in range(n + 1): + distance[0][j] = j + + # calculate levenshtein distance + for i in range(1, m + 1): + prev_row_idx = (i - 1) % 2 + cur_row_idx = i % 2 + distance[cur_row_idx][0] = i + for j in range(1, n + 1): + if ref[i - 1] == hyp[j - 1]: + distance[cur_row_idx][j] = distance[prev_row_idx][j - 1] + else: + s_num = distance[prev_row_idx][j - 1] + 1 + i_num = distance[cur_row_idx][j - 1] + 1 + d_num = distance[prev_row_idx][j] + 1 + distance[cur_row_idx][j] = min(s_num, i_num, d_num) + + return distance[m % 2][n] + + +def word_errors(reference, hypothesis, ignore_case=False, delimiter=' '): + """Compute the levenshtein distance between reference sequence and + hypothesis sequence in word-level. + + Args: + reference (str): The reference sentence. + hypothesis (str): The hypothesis sentence. + ignore_case (bool): Whether case-sensitive or not. + delimiter (char(str)): Delimiter of input sentences. + + Returns: + list: Levenshtein distance and word number of reference sentence. + """ + if ignore_case: + reference = reference.lower() + hypothesis = hypothesis.lower() + + ref_words = list(filter(None, reference.split(delimiter))) + hyp_words = list(filter(None, hypothesis.split(delimiter))) + + edit_distance = _levenshtein_distance(ref_words, hyp_words) + return float(edit_distance), len(ref_words) + + +def char_errors(reference, hypothesis, ignore_case=False, remove_space=False): + """Compute the levenshtein distance between reference sequence and + hypothesis sequence in char-level. + + Args: + reference (str): The reference sentence. + hypothesis (str): The hypothesis sentence. + ignore_case (bool): Whether case-sensitive or not. + remove_space (bool): Whether remove internal space characters + + Returns: + list: Levenshtein distance and length of reference sentence. + """ + if ignore_case: + reference = reference.lower() + hypothesis = hypothesis.lower() + + join_char = ' ' + if remove_space: + join_char = '' + + reference = join_char.join(list(filter(None, reference.split(' ')))) + hypothesis = join_char.join(list(filter(None, hypothesis.split(' ')))) + + edit_distance = _levenshtein_distance(reference, hypothesis) + return float(edit_distance), len(reference) + + +def wer(reference, hypothesis, ignore_case=False, delimiter=' '): + """Calculate word error rate (WER). WER compares reference text and + hypothesis text in word-level. WER is defined as: + .. math:: + WER = (Sw + Dw + Iw) / Nw + where + .. code-block:: text + Sw is the number of words subsituted, + Dw is the number of words deleted, + Iw is the number of words inserted, + Nw is the number of words in the reference + We can use levenshtein distance to calculate WER. Please draw an attention + that empty items will be removed when splitting sentences by delimiter. + + Args: + reference (str): The reference sentence. + hypothesis (str): The hypothesis sentence. + ignore_case (bool): Whether case-sensitive or not. + delimiter (char): Delimiter of input sentences. + + Returns: + float: Word error rate. + + Raises: + ValueError: If word number of reference is zero. + """ + edit_distance, ref_len = word_errors(reference, hypothesis, ignore_case, + delimiter) + + if ref_len == 0: + raise ValueError("Reference's word number should be greater than 0.") + + wer = float(edit_distance) / ref_len + return wer + + +def cer(reference, hypothesis, ignore_case=False, remove_space=False): + """Calculate charactor error rate (CER). CER compares reference text and + hypothesis text in char-level. CER is defined as: + .. math:: + CER = (Sc + Dc + Ic) / Nc + where + .. code-block:: text + Sc is the number of characters substituted, + Dc is the number of characters deleted, + Ic is the number of characters inserted + Nc is the number of characters in the reference + We can use levenshtein distance to calculate CER. Chinese input should be + encoded to unicode. Please draw an attention that the leading and tailing + space characters will be truncated and multiple consecutive space + characters in a sentence will be replaced by one space character. + + Args: + reference (str): The reference sentence. + hypothesis (str): The hypothesis sentence. + ignore_case (bool): Whether case-sensitive or not. + remove_space (bool): Whether remove internal space characters + + Returns: + float: Character error rate. + + Raises: + ValueError: If the reference length is zero. + """ + edit_distance, ref_len = char_errors(reference, hypothesis, ignore_case, + remove_space) + + if ref_len == 0: + raise ValueError("Length of reference should be greater than 0.") + + cer = float(edit_distance) / ref_len + return cer + + +if __name__ == "__main__": + reference = [ + 'j', 'iou4', 'zh', 'e4', 'iang5', 'x', 'v2', 'b', 'o1', 'k', 'ai1', + 'sh', 'iii3', 'l', 'e5', 'b', 'ei3', 'p', 'iao1', 'sh', 'eng1', 'ia2' + ] + hypothesis = [ + 'j', 'iou4', 'zh', 'e4', 'iang4', 'x', 'v2', 'b', 'o1', 'k', 'ai1', + 'sh', 'iii3', 'l', 'e5', 'b', 'ei3', 'p', 'iao1', 'sh', 'eng1', 'ia2' + ] + reference = " ".join(reference) + hypothesis = " ".join(hypothesis) + print(wer(reference, hypothesis)) diff --git a/ernie-sat/paddlespeech/t2s/utils/h5_utils.py b/ernie-sat/paddlespeech/t2s/utils/h5_utils.py new file mode 100644 index 0000000..75c2e44 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/h5_utils.py @@ -0,0 +1,93 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import sys +from pathlib import Path +from typing import Any +from typing import Union + +import h5py +import numpy as np + + +def read_hdf5(filename: Union[Path, str], dataset_name: str) -> Any: + """Read a dataset from a HDF5 file. + Args: + filename (Union[Path, str]): Path of the HDF5 file. + dataset_name (str): Name of the dataset to read. + + Returns: + Any: The retrieved dataset. + """ + filename = Path(filename) + + if not filename.exists(): + logging.error(f"There is no such a hdf5 file ({filename}).") + sys.exit(1) + + hdf5_file = h5py.File(filename, "r") + + if dataset_name not in hdf5_file: + logging.error(f"There is no such a data in hdf5 file. ({dataset_name})") + sys.exit(1) + + # [()]: a special syntax of h5py to get the dataset as-is + hdf5_data = hdf5_file[dataset_name][()] + hdf5_file.close() + + return hdf5_data + + +def write_hdf5(filename: Union[Path, str], + dataset_name: str, + write_data: np.ndarray, + is_overwrite: bool=True) -> None: + """Write dataset to HDF5 file. + Args: + filename (Union[Path, str]): Path of the HDF5 file. + dataset_name (str): Name of the dataset to write to. + write_data (np.ndarrays): The data to write. + is_overwrite (bool, optional): Whether to overwrite, by default True + """ + # convert to numpy array + filename = Path(filename) + write_data = np.array(write_data) + + # check folder existence + filename.parent.mkdir(parents=True, exist_ok=True) + + # check hdf5 existence + if filename.exists(): + # if already exists, open with r+ mode + hdf5_file = h5py.File(filename, "r+") + # check dataset existence + if dataset_name in hdf5_file: + if is_overwrite: + logging.warning("Dataset in hdf5 file already exists. " + "recreate dataset in hdf5.") + hdf5_file.__delitem__(dataset_name) + else: + logging.error( + "Dataset in hdf5 file already exists. " + "if you want to overwrite, please set is_overwrite = True.") + hdf5_file.close() + sys.exit(1) + else: + # if not exists, open with w mode + hdf5_file = h5py.File(filename, "w") + + # write data to hdf5 + hdf5_file.create_dataset(dataset_name, data=write_data) + hdf5_file.flush() + hdf5_file.close() diff --git a/ernie-sat/paddlespeech/t2s/utils/internals.py b/ernie-sat/paddlespeech/t2s/utils/internals.py new file mode 100644 index 0000000..6c10bd2 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/internals.py @@ -0,0 +1,52 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from paddle.framework import core + +__all__ = ["convert_dtype_to_np_dtype_"] + + +def convert_dtype_to_np_dtype_(dtype): + """ + Convert paddle's data type to corrsponding numpy data type. + + Args: + dtype(np.dtype): the data type in paddle. + + Returns: + type: the data type in numpy. + + """ + if dtype is core.VarDesc.VarType.FP32: + return np.float32 + elif dtype is core.VarDesc.VarType.FP64: + return np.float64 + elif dtype is core.VarDesc.VarType.FP16: + return np.float16 + elif dtype is core.VarDesc.VarType.BOOL: + return np.bool + elif dtype is core.VarDesc.VarType.INT32: + return np.int32 + elif dtype is core.VarDesc.VarType.INT64: + return np.int64 + elif dtype is core.VarDesc.VarType.INT16: + return np.int16 + elif dtype is core.VarDesc.VarType.INT8: + return np.int8 + elif dtype is core.VarDesc.VarType.UINT8: + return np.uint8 + elif dtype is core.VarDesc.VarType.BF16: + return np.uint16 + else: + raise ValueError("Not supported dtype %s" % dtype) diff --git a/ernie-sat/paddlespeech/t2s/utils/layer_tools.py b/ernie-sat/paddlespeech/t2s/utils/layer_tools.py new file mode 100644 index 0000000..6e971f9 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/layer_tools.py @@ -0,0 +1,56 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from paddle import nn + +__all__ = ["summary", "gradient_norm", "freeze", "unfreeze"] + + +def summary(layer: nn.Layer): + num_params = num_elements = 0 + print("layer summary:") + for name, param in layer.state_dict().items(): + print("{}|{}|{}".format(name, param.shape, np.prod(param.shape))) + num_elements += np.prod(param.shape) + num_params += 1 + print("layer has {} parameters, {} elements.".format(num_params, + num_elements)) + + +def gradient_norm(layer: nn.Layer): + grad_norm_dict = {} + for name, param in layer.state_dict().items(): + if param.trainable: + grad = param.gradient() + grad_norm_dict[name] = np.linalg.norm(grad) / grad.size + return grad_norm_dict + + +def recursively_remove_weight_norm(layer: nn.Layer): + for layer in layer.sublayers(): + try: + nn.utils.remove_weight_norm(layer) + except Exception as e: + # ther is not weight norm hoom in this layer + pass + + +def freeze(layer: nn.Layer): + for param in layer.parameters(): + param.trainable = False + + +def unfreeze(layer: nn.Layer): + for param in layer.parameters(): + param.trainable = True diff --git a/ernie-sat/paddlespeech/t2s/utils/mp_tools.py b/ernie-sat/paddlespeech/t2s/utils/mp_tools.py new file mode 100644 index 0000000..ed8c83e --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/mp_tools.py @@ -0,0 +1,29 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from functools import wraps + +from paddle import distributed as dist + +__all__ = ["rank_zero_only"] + + +def rank_zero_only(func): + @wraps(func) + def wrapper(*args, **kwargs): + if dist.get_rank() != 0: + return + result = func(*args, **kwargs) + return result + + return wrapper diff --git a/ernie-sat/paddlespeech/t2s/utils/profile.py b/ernie-sat/paddlespeech/t2s/utils/profile.py new file mode 100644 index 0000000..5f9b495 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/profile.py @@ -0,0 +1,34 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from contextlib import contextmanager + +import paddle +from paddle.framework import core +from paddle.framework import CUDAPlace + + +def synchronize(): + """Trigger cuda synchronization for better timing.""" + place = paddle.fluid.framework._current_expected_place() + if isinstance(place, CUDAPlace): + paddle.fluid.core._cuda_synchronize(place) + + +@contextmanager +def nvtx_span(name): + try: + core.nvprof_nvtx_push(name) + yield + finally: + core.nvprof_nvtx_pop() diff --git a/ernie-sat/paddlespeech/t2s/utils/profiler.py b/ernie-sat/paddlespeech/t2s/utils/profiler.py new file mode 100644 index 0000000..2bbeb02 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/profiler.py @@ -0,0 +1,110 @@ +# copyright (c) 2021 PaddlePaddle Authors. All Rights Reserve. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys + +import paddle + +# A global variable to record the number of calling times for profiler +# functions. It is used to specify the tracing range of training steps. +_profiler_step_id = 0 + +# A global variable to avoid parsing from string every time. +_profiler_options = None + + +class ProfilerOptions(object): + ''' + Use a string to initialize a ProfilerOptions. + The string should be in the format: "key1=value1;key2=value;key3=value3". + For example: + "profile_path=model.profile" + "batch_range=[50, 60]; profile_path=model.profile" + "batch_range=[50, 60]; tracer_option=OpDetail; profile_path=model.profile" + ProfilerOptions supports following key-value pair: + batch_range - a integer list, e.g. [100, 110]. + state - a string, the optional values are 'CPU', 'GPU' or 'All'. + sorted_key - a string, the optional values are 'calls', 'total', + 'max', 'min' or 'ave. + tracer_option - a string, the optional values are 'Default', 'OpDetail', + 'AllOpDetail'. + profile_path - a string, the path to save the serialized profile data, + which can be used to generate a timeline. + exit_on_finished - a boolean. + ''' + + def __init__(self, options_str): + assert isinstance(options_str, str) + + self._options = { + 'batch_range': [10, 20], + 'state': 'All', + 'sorted_key': 'total', + 'tracer_option': 'Default', + 'profile_path': '/tmp/profile', + 'exit_on_finished': True + } + self._parse_from_string(options_str) + + def _parse_from_string(self, options_str): + for kv in options_str.replace(' ', '').split(';'): + key, value = kv.split('=') + if key == 'batch_range': + value_list = value.replace('[', '').replace(']', '').split(',') + value_list = list(map(int, value_list)) + if len(value_list) >= 2 and value_list[0] >= 0 and value_list[ + 1] > value_list[0]: + self._options[key] = value_list + elif key == 'exit_on_finished': + self._options[key] = value.lower() in ("yes", "true", "t", "1") + elif key in [ + 'state', 'sorted_key', 'tracer_option', 'profile_path' + ]: + self._options[key] = value + + def __getitem__(self, name): + if self._options.get(name, None) is None: + raise ValueError( + "ProfilerOptions does not have an option named %s." % name) + return self._options[name] + + +def add_profiler_step(options_str=None): + ''' + Enable the operator-level timing using PaddlePaddle's profiler. + The profiler uses a independent variable to count the profiler steps. + One call of this function is treated as a profiler step. + + Args: + profiler_options - a string to initialize the ProfilerOptions. + Default is None, and the profiler is disabled. + ''' + if options_str is None: + return + + global _profiler_step_id + global _profiler_options + + if _profiler_options is None: + _profiler_options = ProfilerOptions(options_str) + + if _profiler_step_id == _profiler_options['batch_range'][0]: + paddle.utils.profiler.start_profiler(_profiler_options['state'], + _profiler_options['tracer_option']) + elif _profiler_step_id == _profiler_options['batch_range'][1]: + paddle.utils.profiler.stop_profiler(_profiler_options['sorted_key'], + _profiler_options['profile_path']) + if _profiler_options['exit_on_finished']: + sys.exit(0) + + _profiler_step_id += 1 diff --git a/ernie-sat/paddlespeech/t2s/utils/scheduler.py b/ernie-sat/paddlespeech/t2s/utils/scheduler.py new file mode 100644 index 0000000..9338995 --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/scheduler.py @@ -0,0 +1,73 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__all__ = ["SchedulerBase", "Constant", "PieceWise", "StepWise"] + + +class SchedulerBase(object): + def __call__(self, step): + raise NotImplementedError("You should implement the __call__ method.") + + +class Constant(SchedulerBase): + def __init__(self, value): + self.value = value + + def __call__(self, step): + return self.value + + +class PieceWise(SchedulerBase): + def __init__(self, anchors): + anchors = list(anchors) + anchors = sorted(anchors, key=lambda x: x[0]) + assert anchors[0][0] == 0, "it must start from zero" + self.xs = [item[0] for item in anchors] + self.ys = [item[1] for item in anchors] + self.num_anchors = len(self.xs) + + def __call__(self, step): + i = 0 + for x in self.xs: + if step >= x: + i += 1 + if i == 0: + return self.ys[0] + if i == self.num_anchors: + return self.ys[-1] + k = (self.ys[i] - self.ys[i - 1]) / (self.xs[i] - self.xs[i - 1]) + out = self.ys[i - 1] + (step - self.xs[i - 1]) * k + return out + + +class StepWise(SchedulerBase): + def __init__(self, anchors): + anchors = list(anchors) + anchors = sorted(anchors, key=lambda x: x[0]) + assert anchors[0][0] == 0, "it must start from zero" + self.xs = [item[0] for item in anchors] + self.ys = [item[1] for item in anchors] + self.num_anchors = len(self.xs) + + def __call__(self, step): + i = 0 + for x in self.xs: + if step >= x: + i += 1 + + if i == self.num_anchors: + return self.ys[-1] + if i == 0: + return self.ys[0] + return self.ys[i - 1] diff --git a/ernie-sat/paddlespeech/t2s/utils/timeline.py b/ernie-sat/paddlespeech/t2s/utils/timeline.py new file mode 100644 index 0000000..0a5509d --- /dev/null +++ b/ernie-sat/paddlespeech/t2s/utils/timeline.py @@ -0,0 +1,315 @@ +# Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import json + +import paddle.fluid.proto.profiler.profiler_pb2 as profiler_pb2 +import six + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument( + '--profile_path', + type=str, + default='', + help='Input profile file name. If there are multiple file, the format ' + 'should be trainer1=file1,trainer2=file2,ps=file3') +parser.add_argument( + '--timeline_path', type=str, default='', help='Output timeline file name.') +args = parser.parse_args() + + +class _ChromeTraceFormatter(object): + def __init__(self): + self._events = [] + self._metadata = [] + + def _create_event(self, ph, category, name, pid, tid, timestamp): + """Creates a new Chrome Trace event. + + For details of the file format, see: + https://github.com/catapult-project/catapult/blob/master/tracing/README.md + + Args: + ph: The type of event - usually a single character. + category: The event category as a string. + name: The event name as a string. + pid: Identifier of the process generating this event as an integer. + tid: Identifier of the thread generating this event as an integer. + timestamp: The timestamp of this event as a long integer. + + Returns: + A JSON compatible event object. + """ + event = {} + event['ph'] = ph + event['cat'] = category + event['name'] = name.replace("ParallelExecutor::Run/", "") + event['pid'] = pid + event['tid'] = tid + event['ts'] = timestamp + return event + + def emit_pid(self, name, pid): + """Adds a process metadata event to the trace. + + Args: + name: The process name as a string. + pid: Identifier of the process as an integer. + """ + event = {} + event['name'] = 'process_name' + event['ph'] = 'M' + event['pid'] = pid + event['args'] = {'name': name} + self._metadata.append(event) + + def emit_region(self, timestamp, duration, pid, tid, category, name, args): + """Adds a region event to the trace. + + Args: + timestamp: The start timestamp of this region as a long integer. + duration: The duration of this region as a long integer. + pid: Identifier of the process generating this event as an integer. + tid: Identifier of the thread generating this event as an integer. + category: The event category as a string. + name: The event name as a string. + args: A JSON-compatible dictionary of event arguments. + """ + event = self._create_event('X', category, name, pid, tid, timestamp) + event['dur'] = duration + event['args'] = args + self._events.append(event) + + def emit_counter(self, category, name, pid, timestamp, counter, value): + """Emits a record for a single counter. + + Args: + category: The event category as string + name: The event name as string + pid: Identifier of the process generating this event as integer + timestamp: The timestamps of this event as long integer + counter: Name of the counter as string + value: Value of the counter as integer + tid: Thread id of the allocation as integer + """ + event = self._create_event('C', category, name, pid, 0, timestamp) + event['args'] = {counter: value} + self._events.append(event) + + def format_to_string(self, pretty=False): + """Formats the chrome trace to a string. + + Args: + pretty: (Optional.) If True, produce human-readable JSON output. + + Returns: + A JSON-formatted string in Chrome Trace format. + """ + trace = {} + trace['traceEvents'] = self._metadata + self._events + if pretty: + return json.dumps(trace, indent=4, separators=(',', ': ')) + else: + return json.dumps(trace, separators=(',', ':')) + + +class Timeline(object): + def __init__(self, profile_dict): + self._profile_dict = profile_dict + self._pid = 0 + self._devices = dict() + self._mem_devices = dict() + self._chrome_trace = _ChromeTraceFormatter() + + def _allocate_pid(self): + cur_pid = self._pid + self._pid += 1 + return cur_pid + + def _allocate_pids(self): + for k, profile_pb in six.iteritems(self._profile_dict): + for event in profile_pb.events: + if event.type == profiler_pb2.Event.CPU: + if (k, event.device_id, "CPU") not in self._devices: + pid = self._allocate_pid() + self._devices[(k, event.device_id, "CPU")] = pid + # -1 device id represents CUDA API(RunTime) call.(e.g. cudaLaunch, cudaMemcpy) + if event.device_id == -1: + self._chrome_trace.emit_pid("%s:cuda_api" % k, pid) + else: + self._chrome_trace.emit_pid( + "%s:cpu:block:%d" % (k, event.device_id), pid) + elif event.type == profiler_pb2.Event.GPUKernel: + if (k, event.device_id, "GPUKernel") not in self._devices: + pid = self._allocate_pid() + self._devices[(k, event.device_id, "GPUKernel")] = pid + self._chrome_trace.emit_pid("%s:gpu:%d" % + (k, event.device_id), pid) + if not hasattr(profile_pb, "mem_events"): + continue + for mevent in profile_pb.mem_events: + if mevent.place == profiler_pb2.MemEvent.CUDAPlace: + if (k, mevent.device_id, "GPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, mevent.device_id, "GPU")] = pid + self._chrome_trace.emit_pid( + "memory usage on %s:gpu:%d" % (k, mevent.device_id), + pid) + elif mevent.place == profiler_pb2.MemEvent.CPUPlace: + if (k, mevent.device_id, "CPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, mevent.device_id, "CPU")] = pid + self._chrome_trace.emit_pid( + "memory usage on %s:cpu:%d" % (k, mevent.device_id), + pid) + elif mevent.place == profiler_pb2.MemEvent.CUDAPinnedPlace: + if (k, mevent.device_id, + "CUDAPinnedPlace") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, mevent.device_id, + "CUDAPinnedPlace")] = pid + self._chrome_trace.emit_pid( + "memory usage on %s:cudapinnedplace:%d" % + (k, mevent.device_id), pid) + elif mevent.place == profiler_pb2.MemEvent.NPUPlace: + if (k, mevent.device_id, "NPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, mevent.device_id, "NPU")] = pid + self._chrome_trace.emit_pid( + "memory usage on %s:npu:%d" % (k, mevent.device_id), + pid) + if (k, 0, "CPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, 0, "CPU")] = pid + self._chrome_trace.emit_pid("memory usage on %s:cpu:%d" % + (k, 0), pid) + if (k, 0, "GPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, 0, "GPU")] = pid + self._chrome_trace.emit_pid("memory usage on %s:gpu:%d" % + (k, 0), pid) + if (k, 0, "CUDAPinnedPlace") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, 0, "CUDAPinnedPlace")] = pid + self._chrome_trace.emit_pid( + "memory usage on %s:cudapinnedplace:%d" % (k, 0), pid) + if (k, 0, "NPU") not in self._mem_devices: + pid = self._allocate_pid() + self._mem_devices[(k, 0, "NPU")] = pid + self._chrome_trace.emit_pid("memory usage on %s:npu:%d" % + (k, 0), pid) + + def _allocate_events(self): + for k, profile_pb in six.iteritems(self._profile_dict): + for event in profile_pb.events: + if event.type == profiler_pb2.Event.CPU: + type = "CPU" + elif event.type == profiler_pb2.Event.GPUKernel: + type = "GPUKernel" + pid = self._devices[(k, event.device_id, type)] + args = {'name': event.name} + if event.memcopy.bytes > 0: + args['mem_bytes'] = event.memcopy.bytes + if hasattr(event, "detail_info") and event.detail_info: + args['detail_info'] = event.detail_info + # TODO(panyx0718): Chrome tracing only handles ms. However, some + # ops takes micro-seconds. Hence, we keep the ns here. + self._chrome_trace.emit_region( + event.start_ns, (event.end_ns - event.start_ns) / 1.0, pid, + event.sub_device_id, 'Op', event.name, args) + + def _allocate_memory_event(self): + if not hasattr(profiler_pb2, "MemEvent"): + return + place_to_str = { + profiler_pb2.MemEvent.CPUPlace: "CPU", + profiler_pb2.MemEvent.CUDAPlace: "GPU", + profiler_pb2.MemEvent.CUDAPinnedPlace: "CUDAPinnedPlace", + profiler_pb2.MemEvent.NPUPlace: "NPU" + } + for k, profile_pb in six.iteritems(self._profile_dict): + mem_list = [] + end_profiler = 0 + for mevent in profile_pb.mem_events: + crt_info = dict() + crt_info['time'] = mevent.start_ns + crt_info['size'] = mevent.bytes + if mevent.place in place_to_str: + place = place_to_str[mevent.place] + else: + place = "UnDefine" + crt_info['place'] = place + pid = self._mem_devices[(k, mevent.device_id, place)] + crt_info['pid'] = pid + crt_info['thread_id'] = mevent.thread_id + crt_info['device_id'] = mevent.device_id + mem_list.append(crt_info) + crt_info = dict() + crt_info['place'] = place + crt_info['pid'] = pid + crt_info['thread_id'] = mevent.thread_id + crt_info['device_id'] = mevent.device_id + crt_info['time'] = mevent.end_ns + crt_info['size'] = -mevent.bytes + mem_list.append(crt_info) + end_profiler = max(end_profiler, crt_info['time']) + mem_list.sort(key=lambda tmp: (tmp.get('time', 0))) + i = 0 + total_size = 0 + while i < len(mem_list): + total_size += mem_list[i]['size'] + while i < len(mem_list) - 1 and mem_list[i]['time'] == mem_list[ + i + 1]['time']: + total_size += mem_list[i + 1]['size'] + i += 1 + + self._chrome_trace.emit_counter( + "Memory", "Memory", mem_list[i]['pid'], mem_list[i]['time'], + 0, total_size) + i += 1 + + def generate_chrome_trace(self): + self._allocate_pids() + self._allocate_events() + self._allocate_memory_event() + return self._chrome_trace.format_to_string() + + +profile_path = '/tmp/profile' +if args.profile_path: + profile_path = args.profile_path +timeline_path = '/tmp/timeline' +if args.timeline_path: + timeline_path = args.timeline_path + +profile_paths = profile_path.split(',') +profile_dict = dict() +if len(profile_paths) == 1: + with open(profile_path, 'rb') as f: + profile_s = f.read() + profile_pb = profiler_pb2.Profile() + profile_pb.ParseFromString(profile_s) + profile_dict['trainer'] = profile_pb +else: + for profile_path in profile_paths: + k, v = profile_path.split('=') + with open(v, 'rb') as f: + profile_s = f.read() + profile_pb = profiler_pb2.Profile() + profile_pb.ParseFromString(profile_s) + profile_dict[k] = profile_pb + +tl = Timeline(profile_dict) +with open(timeline_path, 'w') as f: + f.write(tl.generate_chrome_trace()) diff --git a/ernie-sat/paddlespeech/text/__init__.py b/ernie-sat/paddlespeech/text/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/text/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/text/exps/__init__.py b/ernie-sat/paddlespeech/text/exps/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/text/exps/ernie_linear/__init__.py b/ernie-sat/paddlespeech/text/exps/ernie_linear/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/ernie_linear/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/text/exps/ernie_linear/avg_model.py b/ernie-sat/paddlespeech/text/exps/ernie_linear/avg_model.py new file mode 100644 index 0000000..036ca14 --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/ernie_linear/avg_model.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import glob +import json +import os + +import numpy as np +import paddle + + +def main(args): + paddle.set_device('cpu') + + val_scores = [] + beat_val_scores = [] + selected_epochs = [] + if args.val_best: + jsons = glob.glob(f'{args.ckpt_dir}/[!train]*.json') + for y in jsons: + with open(y, 'r') as f: + dict_json = json.load(f) + loss = dict_json['F1'] + epoch = dict_json['epoch'] + if epoch >= args.min_epoch and epoch <= args.max_epoch: + val_scores.append((epoch, loss)) + + val_scores = np.array(val_scores) + sort_idx = np.argsort(-val_scores[:, 1]) + sorted_val_scores = val_scores[sort_idx] + path_list = [ + args.ckpt_dir + '/{}.pdparams'.format(int(epoch)) + for epoch in sorted_val_scores[:args.num, 0] + ] + + beat_val_scores = sorted_val_scores[:args.num, 1] + selected_epochs = sorted_val_scores[:args.num, 0].astype(np.int64) + print("best val scores = " + str(beat_val_scores)) + print("selected epochs = " + str(selected_epochs)) + else: + path_list = glob.glob(f'{args.ckpt_dir}/[!avg][!final]*.pdparams') + path_list = sorted(path_list, key=os.path.getmtime) + path_list = path_list[-args.num:] + + print(path_list) + + avg = None + num = args.num + assert num == len(path_list) + for path in path_list: + print(f'Processing {path}') + states = paddle.load(path) + if avg is None: + avg = states + else: + for k in avg.keys(): + avg[k] += states[k] + # average + for k in avg.keys(): + if avg[k] is not None: + avg[k] /= num + + paddle.save(avg, args.dst_model) + print(f'Saving to {args.dst_model}') + + meta_path = os.path.splitext(args.dst_model)[0] + '.avg.json' + with open(meta_path, 'w') as f: + data = json.dumps({ + "avg_ckpt": args.dst_model, + "ckpt": path_list, + "epoch": selected_epochs.tolist(), + "val_loss": beat_val_scores.tolist(), + }) + f.write(data + "\n") + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='average model') + parser.add_argument('--dst_model', required=True, help='averaged model') + parser.add_argument( + '--ckpt_dir', required=True, help='ckpt model dir for average') + parser.add_argument( + '--val_best', action="store_true", help='averaged model') + parser.add_argument( + '--num', default=5, type=int, help='nums for averaged model') + parser.add_argument( + '--min_epoch', + default=0, + type=int, + help='min epoch used for averaging model') + parser.add_argument( + '--max_epoch', + default=65536, # Big enough + type=int, + help='max epoch used for averaging model') + + args = parser.parse_args() + print(args) + + main(args) diff --git a/ernie-sat/paddlespeech/text/exps/ernie_linear/punc_restore.py b/ernie-sat/paddlespeech/text/exps/ernie_linear/punc_restore.py new file mode 100644 index 0000000..2cb4d07 --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/ernie_linear/punc_restore.py @@ -0,0 +1,110 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import re + +import paddle +import yaml +from paddlenlp.transformers import ErnieTokenizer +from yacs.config import CfgNode + +from paddlespeech.text.models.ernie_linear import ErnieLinear + +DefinedClassifier = { + 'ErnieLinear': ErnieLinear, +} + +tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0') + + +def _clean_text(text, punc_list): + text = text.lower() + text = re.sub('[^A-Za-z0-9\u4e00-\u9fa5]', '', text) + text = re.sub(f'[{"".join([p for p in punc_list][1:])}]', '', text) + return text + + +def preprocess(text, punc_list): + clean_text = _clean_text(text, punc_list) + assert len(clean_text) > 0, f'Invalid input string: {text}' + tokenized_input = tokenizer( + list(clean_text), return_length=True, is_split_into_words=True) + _inputs = dict() + _inputs['input_ids'] = tokenized_input['input_ids'] + _inputs['seg_ids'] = tokenized_input['token_type_ids'] + _inputs['seq_len'] = tokenized_input['seq_len'] + return _inputs + + +def test(args): + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + + punc_list = [] + with open(config["data_params"]["punc_path"], 'r') as f: + for line in f: + punc_list.append(line.strip()) + + model = DefinedClassifier[config["model_type"]](**config["model"]) + state_dict = paddle.load(args.checkpoint) + model.set_state_dict(state_dict["main_params"]) + model.eval() + _inputs = preprocess(args.text, punc_list) + seq_len = _inputs['seq_len'] + input_ids = paddle.to_tensor(_inputs['input_ids']).unsqueeze(0) + seg_ids = paddle.to_tensor(_inputs['seg_ids']).unsqueeze(0) + logits, _ = model(input_ids, seg_ids) + preds = paddle.argmax(logits, axis=-1).squeeze(0) + tokens = tokenizer.convert_ids_to_tokens( + _inputs['input_ids'][1:seq_len - 1]) + labels = preds[1:seq_len - 1].tolist() + assert len(tokens) == len(labels) + # add 0 for non punc + punc_list = [0] + punc_list + text = '' + for t, l in zip(tokens, labels): + text += t + if l != 0: # Non punc. + text += punc_list[l] + print("Punctuation Restoration Result:", text) + return text + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Run Punctuation Restoration.") + parser.add_argument("--config", type=str, help="ErnieLinear config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument("--text", type=str, help="raw text to be restored.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + test(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/text/exps/ernie_linear/test.py b/ernie-sat/paddlespeech/text/exps/ernie_linear/test.py new file mode 100644 index 0000000..4302a1a --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/ernie_linear/test.py @@ -0,0 +1,120 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse + +import numpy as np +import paddle +import pandas as pd +import yaml +from paddle import nn +from paddle.io import DataLoader +from sklearn.metrics import classification_report +from sklearn.metrics import precision_recall_fscore_support +from yacs.config import CfgNode + +from paddlespeech.text.models.ernie_linear import ErnieLinear +from paddlespeech.text.models.ernie_linear import PuncDataset +from paddlespeech.text.models.ernie_linear import PuncDatasetFromErnieTokenizer + +DefinedClassifier = { + 'ErnieLinear': ErnieLinear, +} + +DefinedLoss = { + "ce": nn.CrossEntropyLoss, +} + +DefinedDataset = { + 'Punc': PuncDataset, + 'Ernie': PuncDatasetFromErnieTokenizer, +} + + +def evaluation(y_pred, y_test): + precision, recall, f1, _ = precision_recall_fscore_support( + y_test, y_pred, average=None, labels=[1, 2, 3]) + overall = precision_recall_fscore_support( + y_test, y_pred, average='macro', labels=[1, 2, 3]) + result = pd.DataFrame( + np.array([precision, recall, f1]), + columns=list(['O', 'COMMA', 'PERIOD', 'QUESTION'])[1:], + index=['Precision', 'Recall', 'F1']) + result['OVERALL'] = overall[:3] + return result + + +def test(args): + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + + test_dataset = DefinedDataset[config["dataset_type"]]( + train_path=config["test_path"], **config["data_params"]) + test_loader = DataLoader( + test_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + model = DefinedClassifier[config["model_type"]](**config["model"]) + state_dict = paddle.load(args.checkpoint) + model.set_state_dict(state_dict["main_params"]) + model.eval() + + punc_list = [] + for i in range(len(test_loader.dataset.id2punc)): + punc_list.append(test_loader.dataset.id2punc[i]) + + test_total_label = [] + test_total_predict = [] + + for i, batch in enumerate(test_loader): + input, label = batch + label = paddle.reshape(label, shape=[-1]) + y, logit = model(input) + pred = paddle.argmax(logit, axis=1) + test_total_label.extend(label.numpy().tolist()) + test_total_predict.extend(pred.numpy().tolist()) + t = classification_report( + test_total_label, test_total_predict, target_names=punc_list) + print(t) + t2 = evaluation(test_total_label, test_total_predict) + print('=========================================================') + print(t2) + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Test a ErnieLinear model.") + parser.add_argument("--config", type=str, help="ErnieLinear config file.") + parser.add_argument("--checkpoint", type=str, help="snapshot to load.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + + args = parser.parse_args() + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + test(args) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/text/exps/ernie_linear/train.py b/ernie-sat/paddlespeech/text/exps/ernie_linear/train.py new file mode 100644 index 0000000..22c25e1 --- /dev/null +++ b/ernie-sat/paddlespeech/text/exps/ernie_linear/train.py @@ -0,0 +1,172 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import logging +import os +import shutil +from pathlib import Path + +import paddle +import yaml +from paddle import DataParallel +from paddle import distributed as dist +from paddle import nn +from paddle.io import DataLoader +from paddle.optimizer import Adam +from paddle.optimizer.lr import ExponentialDecay +from yacs.config import CfgNode + +from paddlespeech.t2s.training.extensions.snapshot import Snapshot +from paddlespeech.t2s.training.extensions.visualizer import VisualDL +from paddlespeech.t2s.training.seeding import seed_everything +from paddlespeech.t2s.training.trainer import Trainer +from paddlespeech.text.models.ernie_linear import ErnieLinear +from paddlespeech.text.models.ernie_linear import ErnieLinearEvaluator +from paddlespeech.text.models.ernie_linear import ErnieLinearUpdater +from paddlespeech.text.models.ernie_linear import PuncDataset +from paddlespeech.text.models.ernie_linear import PuncDatasetFromErnieTokenizer + +DefinedClassifier = { + 'ErnieLinear': ErnieLinear, +} + +DefinedLoss = { + "ce": nn.CrossEntropyLoss, +} + +DefinedDataset = { + 'Punc': PuncDataset, + 'Ernie': PuncDatasetFromErnieTokenizer, +} + + +def train_sp(args, config): + # decides device type and whether to run in parallel + # setup running environment correctly + if (not paddle.is_compiled_with_cuda()) or args.ngpu == 0: + paddle.set_device("cpu") + else: + paddle.set_device("gpu") + world_size = paddle.distributed.get_world_size() + if world_size > 1: + paddle.distributed.init_parallel_env() + + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + print( + f"rank: {dist.get_rank()}, pid: {os.getpid()}, parent_pid: {os.getppid()}", + ) + # dataloader has been too verbose + logging.getLogger("DataLoader").disabled = True + train_dataset = DefinedDataset[config["dataset_type"]]( + train_path=config["train_path"], **config["data_params"]) + dev_dataset = DefinedDataset[config["dataset_type"]]( + train_path=config["dev_path"], **config["data_params"]) + train_dataloader = DataLoader( + train_dataset, + shuffle=True, + num_workers=config.num_workers, + batch_size=config.batch_size) + + dev_dataloader = DataLoader( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False, + num_workers=config.num_workers) + + print("dataloaders done!") + + model = DefinedClassifier[config["model_type"]](**config["model"]) + + if world_size > 1: + model = DataParallel(model) + print("model done!") + + criterion = DefinedLoss[config["loss_type"]]( + **config["loss"]) if "loss_type" in config else DefinedLoss["ce"]() + + print("criterions done!") + + lr_schedule = ExponentialDecay(**config["scheduler_params"]) + optimizer = Adam( + learning_rate=lr_schedule, + parameters=model.parameters(), + weight_decay=paddle.regularizer.L2Decay( + config["optimizer_params"]["weight_decay"])) + + print("optimizer done!") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + if dist.get_rank() == 0: + config_name = args.config.split("/")[-1] + # copy conf to output_dir + shutil.copyfile(args.config, output_dir / config_name) + + updater = ErnieLinearUpdater( + model=model, + criterion=criterion, + scheduler=lr_schedule, + optimizer=optimizer, + dataloader=train_dataloader, + output_dir=output_dir) + + trainer = Trainer(updater, (config.max_epoch, 'epoch'), output_dir) + + evaluator = ErnieLinearEvaluator( + model=model, + criterion=criterion, + dataloader=dev_dataloader, + output_dir=output_dir) + + if dist.get_rank() == 0: + trainer.extend(evaluator, trigger=(1, "epoch")) + trainer.extend(VisualDL(output_dir), trigger=(1, "iteration")) + trainer.extend( + Snapshot(max_size=config.num_snapshots), trigger=(1, 'epoch')) + trainer.run() + + +def main(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser(description="Train a ErnieLinear model.") + parser.add_argument("--config", type=str, help="ErnieLinear config file.") + parser.add_argument("--output-dir", type=str, help="output dir.") + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + + args = parser.parse_args() + + with open(args.config) as f: + config = CfgNode(yaml.safe_load(f)) + + print("========Args========") + print(yaml.safe_dump(vars(args))) + print("========Config========") + print(config) + print( + f"master see the word size: {dist.get_world_size()}, from pid: {os.getpid()}" + ) + + # dispatch + if args.ngpu > 1: + dist.spawn(train_sp, (args, config), nprocs=args.ngpu) + else: + train_sp(args, config) + + +if __name__ == "__main__": + main() diff --git a/ernie-sat/paddlespeech/text/models/__init__.py b/ernie-sat/paddlespeech/text/models/__init__.py new file mode 100644 index 0000000..3828e13 --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .ernie_crf import ErnieCrf +from .ernie_linear import ErnieLinear diff --git a/ernie-sat/paddlespeech/text/models/ernie_crf/__init__.py b/ernie-sat/paddlespeech/text/models/ernie_crf/__init__.py new file mode 100644 index 0000000..bbe467a --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_crf/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .model import ErnieCrf diff --git a/ernie-sat/paddlespeech/text/models/ernie_crf/model.py b/ernie-sat/paddlespeech/text/models/ernie_crf/model.py new file mode 100644 index 0000000..d1ce809 --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_crf/model.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import paddle.nn as nn +from paddlenlp.layers.crf import LinearChainCrf +from paddlenlp.layers.crf import LinearChainCrfLoss +from paddlenlp.layers.crf import ViterbiDecoder +from paddlenlp.transformers import ErnieForTokenClassification + + +class ErnieCrf(nn.Layer): + def __init__(self, + num_classes, + pretrained_token='ernie-1.0', + crf_lr=100, + **kwargs): + super().__init__() + self.ernie = ErnieForTokenClassification.from_pretrained( + pretrained_token, num_classes=num_classes, **kwargs) + self.num_classes = num_classes + self.crf = LinearChainCrf( + self.num_classes, crf_lr=crf_lr, with_start_stop_tag=False) + self.crf_loss = LinearChainCrfLoss(self.crf) + self.viterbi_decoder = ViterbiDecoder( + self.crf.transitions, with_start_stop_tag=False) + + def forward(self, + input_ids, + token_type_ids=None, + position_ids=None, + attention_mask=None, + lengths=None, + labels=None): + logits = self.ernie( + input_ids, + token_type_ids=token_type_ids, + attention_mask=attention_mask, + position_ids=position_ids) + + if lengths is None: + lengths = paddle.ones( + shape=[input_ids.shape[0]], + dtype=paddle.int64) * input_ids.shape[1] + + _, prediction = self.viterbi_decoder(logits, lengths) + prediction = prediction.reshape([-1]) + + if labels is not None: + labels = labels.reshape([input_ids.shape[0], -1]) + loss = self.crf_loss(logits, lengths, labels) + avg_loss = paddle.mean(loss) + return avg_loss, prediction + else: + return prediction diff --git a/ernie-sat/paddlespeech/text/models/ernie_linear/__init__.py b/ernie-sat/paddlespeech/text/models/ernie_linear/__init__.py new file mode 100644 index 0000000..0a10a6e --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_linear/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .dataset import * +from .ernie_linear import * +from .ernie_linear_updater import * diff --git a/ernie-sat/paddlespeech/text/models/ernie_linear/dataset.py b/ernie-sat/paddlespeech/text/models/ernie_linear/dataset.py new file mode 100644 index 0000000..64c8d0b --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_linear/dataset.py @@ -0,0 +1,154 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle +from paddle.io import Dataset +from paddlenlp.transformers import ErnieTokenizer + +__all__ = ["PuncDataset", "PuncDatasetFromErnieTokenizer"] + + +class PuncDataset(Dataset): + def __init__(self, train_path, vocab_path, punc_path, seq_len=100): + self.seq_len = seq_len + + self.word2id = self.load_vocab( + vocab_path, extra_word_list=['', '']) + self.id2word = {v: k for k, v in self.word2id.items()} + self.punc2id = self.load_vocab(punc_path, extra_word_list=[" "]) + self.id2punc = {k: v for (v, k) in self.punc2id.items()} + + tmp_seqs = open(train_path, encoding='utf-8').readlines() + self.txt_seqs = [i for seq in tmp_seqs for i in seq.split()] + self.preprocess(self.txt_seqs) + + def __len__(self): + """return the sentence nums in .txt + """ + return self.in_len + + def __getitem__(self, index): + return self.input_data[index], self.label[index] + + def load_vocab(self, vocab_path, extra_word_list=[], encoding='utf-8'): + n = len(extra_word_list) + with open(vocab_path, encoding='utf-8') as vf: + vocab = {word.strip(): i + n for i, word in enumerate(vf)} + for i, word in enumerate(extra_word_list): + vocab[word] = i + return vocab + + def preprocess(self, txt_seqs: list): + input_data = [] + label = [] + input_r = [] + label_r = [] + + count = 0 + length = len(txt_seqs) + for token in txt_seqs: + count += 1 + if count == length: + break + if token in self.punc2id: + continue + punc = txt_seqs[count] + if punc not in self.punc2id: + label.append(self.punc2id[" "]) + input_data.append( + self.word2id.get(token, self.word2id[""])) + input_r.append(token) + label_r.append(' ') + else: + label.append(self.punc2id[punc]) + input_data.append( + self.word2id.get(token, self.word2id[""])) + input_r.append(token) + label_r.append(punc) + if len(input_data) != len(label): + assert 'error: length input_data != label' + + self.in_len = len(input_data) // self.seq_len + len_tmp = self.in_len * self.seq_len + input_data = input_data[:len_tmp] + label = label[:len_tmp] + + self.input_data = paddle.to_tensor( + np.array(input_data, dtype='int64').reshape(-1, self.seq_len)) + self.label = paddle.to_tensor( + np.array(label, dtype='int64').reshape(-1, self.seq_len)) + + +class PuncDatasetFromErnieTokenizer(Dataset): + def __init__(self, + train_path, + punc_path, + pretrained_token='ernie-1.0', + seq_len=100): + self.tokenizer = ErnieTokenizer.from_pretrained(pretrained_token) + self.paddingID = self.tokenizer.pad_token_id + self.seq_len = seq_len + self.punc2id = self.load_vocab(punc_path, extra_word_list=[" "]) + self.id2punc = {k: v for (v, k) in self.punc2id.items()} + tmp_seqs = open(train_path, encoding='utf-8').readlines() + self.txt_seqs = [i for seq in tmp_seqs for i in seq.split()] + self.preprocess(self.txt_seqs) + + def __len__(self): + return self.in_len + + def __getitem__(self, index): + return self.input_data[index], self.label[index] + + def load_vocab(self, vocab_path, extra_word_list=[], encoding='utf-8'): + n = len(extra_word_list) + with open(vocab_path, encoding='utf-8') as vf: + vocab = {word.strip(): i + n for i, word in enumerate(vf)} + for i, word in enumerate(extra_word_list): + vocab[word] = i + return vocab + + def preprocess(self, txt_seqs: list): + input_data = [] + label = [] + count = 0 + print("Preprocessing in PuncDatasetFromErnieTokenizer...") + for i in range(len(txt_seqs) - 1): + word = txt_seqs[i] + punc = txt_seqs[i + 1] + if word in self.punc2id: + continue + + token = self.tokenizer(word) + x = token["input_ids"][1:-1] + input_data.extend(x) + + for i in range(len(x) - 1): + label.append(self.punc2id[" "]) + + if punc not in self.punc2id: + label.append(self.punc2id[" "]) + else: + label.append(self.punc2id[punc]) + + if len(input_data) != len(label): + assert 'error: length input_data != label' + + self.in_len = len(input_data) // self.seq_len + len_tmp = self.in_len * self.seq_len + input_data = input_data[:len_tmp] + label = label[:len_tmp] + self.input_data = np.array( + input_data, dtype='int64').reshape(-1, self.seq_len) + self.label = np.array(label, dtype='int64').reshape(-1, self.seq_len) diff --git a/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear.py b/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear.py new file mode 100644 index 0000000..c450a90 --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear.py @@ -0,0 +1,65 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import paddle +import paddle.nn as nn +from paddlenlp.transformers import ErnieForTokenClassification + + +class ErnieLinear(nn.Layer): + def __init__(self, + num_classes=None, + pretrained_token='ernie-1.0', + cfg_path=None, + ckpt_path=None, + **kwargs): + super(ErnieLinear, self).__init__() + + if cfg_path is not None and ckpt_path is not None: + cfg_path = os.path.abspath(os.path.expanduser(cfg_path)) + ckpt_path = os.path.abspath(os.path.expanduser(ckpt_path)) + + assert os.path.isfile( + cfg_path), 'Config file is not valid: {}'.format(cfg_path) + assert os.path.isfile( + ckpt_path), 'Checkpoint file is not valid: {}'.format(ckpt_path) + + self.ernie = ErnieForTokenClassification.from_pretrained( + os.path.dirname(cfg_path)) + else: + assert isinstance( + num_classes, int + ) and num_classes > 0, 'Argument `num_classes` must be an integer.' + self.ernie = ErnieForTokenClassification.from_pretrained( + pretrained_token, num_classes=num_classes, **kwargs) + + self.num_classes = self.ernie.num_classes + self.softmax = nn.Softmax() + + def forward(self, + input_ids, + token_type_ids=None, + position_ids=None, + attention_mask=None): + y = self.ernie( + input_ids, + token_type_ids=token_type_ids, + attention_mask=attention_mask, + position_ids=position_ids) + + y = paddle.reshape(y, shape=[-1, self.num_classes]) + logits = self.softmax(y) + + return y, logits diff --git a/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear_updater.py b/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear_updater.py new file mode 100644 index 0000000..8b3d741 --- /dev/null +++ b/ernie-sat/paddlespeech/text/models/ernie_linear/ernie_linear_updater.py @@ -0,0 +1,123 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import paddle +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn import Layer +from paddle.optimizer import Optimizer +from paddle.optimizer.lr import LRScheduler +from sklearn.metrics import f1_score + +from paddlespeech.t2s.training.extensions.evaluator import StandardEvaluator +from paddlespeech.t2s.training.reporter import report +from paddlespeech.t2s.training.updaters.standard_updater import StandardUpdater +logging.basicConfig( + format='%(asctime)s [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s', + datefmt='[%Y-%m-%d %H:%M:%S]') +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class ErnieLinearUpdater(StandardUpdater): + def __init__(self, + model: Layer, + criterion: Layer, + scheduler: LRScheduler, + optimizer: Optimizer, + dataloader: DataLoader, + output_dir=None): + super().__init__(model, optimizer, dataloader, init_state=None) + self.model = model + self.dataloader = dataloader + + self.criterion = criterion + self.scheduler = scheduler + self.optimizer = optimizer + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def update_core(self, batch): + self.msg = "Rank: {}, ".format(dist.get_rank()) + losses_dict = {} + + input, label = batch + label = paddle.reshape(label, shape=[-1]) + y, logit = self.model(input) + pred = paddle.argmax(logit, axis=1) + + loss = self.criterion(y, label) + + self.optimizer.clear_grad() + loss.backward() + + self.optimizer.step() + self.scheduler.step() + + F1_score = f1_score( + label.numpy().tolist(), pred.numpy().tolist(), average="macro") + + report("train/loss", float(loss)) + losses_dict["loss"] = float(loss) + report("train/F1_score", float(F1_score)) + losses_dict["F1_score"] = float(F1_score) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + + +class ErnieLinearEvaluator(StandardEvaluator): + def __init__(self, + model: Layer, + criterion: Layer, + dataloader: DataLoader, + output_dir=None): + super().__init__(model, dataloader) + self.model = model + self.criterion = criterion + self.dataloader = dataloader + + log_file = output_dir / 'worker_{}.log'.format(dist.get_rank()) + self.filehandler = logging.FileHandler(str(log_file)) + logger.addHandler(self.filehandler) + self.logger = logger + self.msg = "" + + def evaluate_core(self, batch): + self.msg = "Evaluate: " + losses_dict = {} + + input, label = batch + label = paddle.reshape(label, shape=[-1]) + y, logit = self.model(input) + pred = paddle.argmax(logit, axis=1) + + loss = self.criterion(y, label) + + F1_score = f1_score( + label.numpy().tolist(), pred.numpy().tolist(), average="macro") + + report("eval/loss", float(loss)) + losses_dict["loss"] = float(loss) + report("eval/F1_score", float(F1_score)) + losses_dict["F1_score"] = float(F1_score) + + self.msg += ', '.join('{}: {:>.6f}'.format(k, v) + for k, v in losses_dict.items()) + self.logger.info(self.msg) diff --git a/ernie-sat/paddlespeech/vector/__init__.py b/ernie-sat/paddlespeech/vector/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/vector/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/cluster/__init__.py b/ernie-sat/paddlespeech/vector/cluster/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/cluster/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/cluster/diarization.py b/ernie-sat/paddlespeech/vector/cluster/diarization.py new file mode 100644 index 0000000..597aa48 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/cluster/diarization.py @@ -0,0 +1,1080 @@ +# Copyright (c) 2022 SpeechBrain Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +This script contains basic functions used for speaker diarization. +This script has an optional dependency on open source sklearn library. +A few sklearn functions are modified in this script as per requirement. +""" +import argparse +import warnings + +import numpy as np +import scipy +import sklearn +from distutils.util import strtobool +from scipy import sparse +from scipy.sparse.csgraph import connected_components +from scipy.sparse.csgraph import laplacian as csgraph_laplacian +from scipy.sparse.linalg import eigsh +from sklearn.cluster import SpectralClustering +from sklearn.cluster._kmeans import k_means +from sklearn.neighbors import kneighbors_graph + + +def _graph_connected_component(graph, node_id): + """ + Find the largest graph connected components that contains one + given node. + + Arguments + --------- + graph : array-like, shape: (n_samples, n_samples) + Adjacency matrix of the graph, non-zero weight means an edge + between the nodes. + node_id : int + The index of the query node of the graph. + + Returns + ------- + connected_components_matrix : array-like + shape - (n_samples,). + An array of bool value indicating the indexes of the nodes belonging + to the largest connected components of the given query node. + """ + + n_node = graph.shape[0] + if sparse.issparse(graph): + # speed up row-wise access to boolean connection mask + graph = graph.tocsr() + connected_nodes = np.zeros(n_node, dtype=bool) + nodes_to_explore = np.zeros(n_node, dtype=bool) + nodes_to_explore[node_id] = True + for _ in range(n_node): + last_num_component = connected_nodes.sum() + np.logical_or(connected_nodes, nodes_to_explore, out=connected_nodes) + if last_num_component >= connected_nodes.sum(): + break + indices = np.where(nodes_to_explore)[0] + nodes_to_explore.fill(False) + for i in indices: + if sparse.issparse(graph): + neighbors = graph[i].toarray().ravel() + else: + neighbors = graph[i] + np.logical_or(nodes_to_explore, neighbors, out=nodes_to_explore) + return connected_nodes + + +def _graph_is_connected(graph): + """ + Return whether the graph is connected (True) or Not (False) + + Arguments + --------- + graph : array-like or sparse matrix, shape: (n_samples, n_samples) + Adjacency matrix of the graph, non-zero weight means an edge between the nodes. + + Returns + ------- + is_connected : bool + True means the graph is fully connected and False means not. + """ + + if sparse.isspmatrix(graph): + # sparse graph, find all the connected components + n_connected_components, _ = connected_components(graph) + return n_connected_components == 1 + else: + # dense graph, find all connected components start from node 0 + return _graph_connected_component(graph, 0).sum() == graph.shape[0] + + +def _set_diag(laplacian, value, norm_laplacian): + """ + Set the diagonal of the laplacian matrix and convert it to a sparse + format well suited for eigenvalue decomposition. + + Arguments + --------- + laplacian : array or sparse matrix + The graph laplacian. + value : float + The value of the diagonal. + norm_laplacian : bool + Whether the value of the diagonal should be changed or not. + + Returns + ------- + laplacian : array or sparse matrix + An array of matrix in a form that is well suited to fast eigenvalue + decomposition, depending on the bandwidth of the matrix. + """ + + n_nodes = laplacian.shape[0] + # We need all entries in the diagonal to values + if not sparse.isspmatrix(laplacian): + if norm_laplacian: + laplacian.flat[::n_nodes + 1] = value + else: + laplacian = laplacian.tocoo() + if norm_laplacian: + diag_idx = laplacian.row == laplacian.col + laplacian.data[diag_idx] = value + # If the matrix has a small number of diagonals (as in the + # case of structured matrices coming from images), the + # dia format might be best suited for matvec products: + n_diags = np.unique(laplacian.row - laplacian.col).size + if n_diags <= 7: + # 3 or less outer diagonals on each side + laplacian = laplacian.todia() + else: + # csr has the fastest matvec and is thus best suited to + # arpack + laplacian = laplacian.tocsr() + return laplacian + + +def _deterministic_vector_sign_flip(u): + """ + Modify the sign of vectors for reproducibility. Flips the sign of + elements of all the vectors (rows of u) such that the absolute + maximum element of each vector is positive. + + Arguments + --------- + u : ndarray + Array with vectors as its rows. + + Returns + ------- + u_flipped : ndarray + Array with the sign flipped vectors as its rows. The same shape as `u`. + """ + + max_abs_rows = np.argmax(np.abs(u), axis=1) + signs = np.sign(u[range(u.shape[0]), max_abs_rows]) + u *= signs[:, np.newaxis] + return u + + +def _check_random_state(seed): + """ + Turn seed into a np.random.RandomState instance. + + Arguments + --------- + seed : None | int | instance of RandomState + If seed is None, return the RandomState singleton used by np.random. + If seed is an int, return a new RandomState instance seeded with seed. + If seed is already a RandomState instance, return it. + Otherwise raise ValueError. + """ + + if seed is None or seed is np.random: + return np.random.mtrand._rand + if isinstance(seed, numbers.Integral): + return np.random.RandomState(seed) + if isinstance(seed, np.random.RandomState): + return seed + raise ValueError("%r cannot be used to seed a np.random.RandomState" + " instance" % seed) + + +def spectral_embedding( + adjacency, + n_components=8, + norm_laplacian=True, + drop_first=True, ): + """ + Returns spectral embeddings. + + Arguments + --------- + adjacency : array-like or sparse graph + shape - (n_samples, n_samples) + The adjacency matrix of the graph to embed. + n_components : int + The dimension of the projection subspace. + norm_laplacian : bool + If True, then compute normalized Laplacian. + drop_first : bool + Whether to drop the first eigenvector. + + Returns + ------- + embedding : array + Spectral embeddings for each sample. + + Example + ------- + >>> import numpy as np + >>> import diarization as diar + >>> affinity = np.array([[1, 1, 1, 0.5, 0, 0, 0, 0, 0, 0.5], + ... [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [0.5, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0.5, 0, 0, 0, 0, 0, 1, 1, 1, 1]]) + >>> embs = diar.spectral_embedding(affinity, 3) + >>> # Notice similar embeddings + >>> print(np.around(embs , decimals=3)) + [[ 0.075 0.244 0.285] + [ 0.083 0.356 -0.203] + [ 0.083 0.356 -0.203] + [ 0.26 -0.149 0.154] + [ 0.29 -0.218 -0.11 ] + [ 0.29 -0.218 -0.11 ] + [-0.198 -0.084 -0.122] + [-0.198 -0.084 -0.122] + [-0.198 -0.084 -0.122] + [-0.167 -0.044 0.316]] + """ + + # Whether to drop the first eigenvector + if drop_first: + n_components = n_components + 1 + + if not _graph_is_connected(adjacency): + warnings.warn("Graph is not fully connected, spectral embedding" + " may not work as expected.") + + laplacian, dd = csgraph_laplacian( + adjacency, normed=norm_laplacian, return_diag=True) + + laplacian = _set_diag(laplacian, 1, norm_laplacian) + + laplacian *= -1 + + vals, diffusion_map = eigsh( + laplacian, + k=n_components, + sigma=1.0, + which="LM", ) + + embedding = diffusion_map.T[n_components::-1] + + if norm_laplacian: + embedding = embedding / dd + + embedding = _deterministic_vector_sign_flip(embedding) + if drop_first: + return embedding[1:n_components].T + else: + return embedding[:n_components].T + + +def spectral_clustering( + affinity, + n_clusters=8, + n_components=None, + random_state=None, + n_init=10, ): + """ + Performs spectral clustering. + + Arguments + --------- + affinity : matrix + Affinity matrix. + n_clusters : int + Number of clusters for kmeans. + n_components : int + Number of components to retain while estimating spectral embeddings. + random_state : int + A pseudo random number generator used by kmeans. + n_init : int + Number of time the k-means algorithm will be run with different centroid seeds. + + Returns + ------- + labels : array + Cluster label for each sample. + + Example + ------- + >>> import numpy as np + >>> diarization as diar + >>> affinity = np.array([[1, 1, 1, 0.5, 0, 0, 0, 0, 0, 0.5], + ... [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [1, 1, 1, 0, 0, 0, 0, 0, 0, 0], + ... [0.5, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 1, 1, 1, 0, 0, 0, 0], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0, 0, 0, 0, 0, 0, 1, 1, 1, 1], + ... [0.5, 0, 0, 0, 0, 0, 1, 1, 1, 1]]) + >>> labs = diar.spectral_clustering(affinity, 3) + >>> # print (labs) # [2 2 2 1 1 1 0 0 0 0] + """ + + random_state = _check_random_state(random_state) + n_components = n_clusters if n_components is None else n_components + + maps = spectral_embedding( + affinity, + n_components=n_components, + drop_first=False, ) + + _, labels, _ = k_means( + maps, n_clusters, random_state=random_state, n_init=n_init) + + return labels + + +class EmbeddingMeta: + """ + A utility class to pack deep embeddings and meta-information in one object. + + Arguments + --------- + segset : list + List of session IDs as an array of strings. + stats : tensor + An ndarray of float64. Each line contains embedding + from the corresponding session. + """ + + def __init__( + self, + segset=None, + stats=None, ): + + if segset is None: + self.segset = numpy.empty(0, dtype="|O") + self.stats = numpy.array([], dtype=np.float64) + else: + self.segset = segset + self.stats = stats + + def norm_stats(self): + """ + Divide all first-order statistics by their Euclidean norm. + """ + + vect_norm = np.clip(np.linalg.norm(self.stats, axis=1), 1e-08, np.inf) + self.stats = (self.stats.transpose() / vect_norm).transpose() + + +class SpecClustUnorm: + """ + This class implements the spectral clustering with unnormalized affinity matrix. + Useful when affinity matrix is based on cosine similarities. + + Reference + --------- + Von Luxburg, U. A tutorial on spectral clustering. Stat Comput 17, 395–416 (2007). + https://doi.org/10.1007/s11222-007-9033-z + + Example + ------- + >>> import diarization as diar + >>> clust = diar.SpecClustUnorm(min_num_spkrs=2, max_num_spkrs=10) + >>> emb = [[ 2.1, 3.1, 4.1, 4.2, 3.1], + ... [ 2.2, 3.1, 4.2, 4.2, 3.2], + ... [ 2.0, 3.0, 4.0, 4.1, 3.0], + ... [ 8.0, 7.0, 7.0, 8.1, 9.0], + ... [ 8.1, 7.1, 7.2, 8.1, 9.2], + ... [ 8.3, 7.4, 7.0, 8.4, 9.0], + ... [ 0.3, 0.4, 0.4, 0.5, 0.8], + ... [ 0.4, 0.3, 0.6, 0.7, 0.8], + ... [ 0.2, 0.3, 0.2, 0.3, 0.7], + ... [ 0.3, 0.4, 0.4, 0.4, 0.7],] + >>> # Estimating similarity matrix + >>> sim_mat = clust.get_sim_mat(emb) + >>> print (np.around(sim_mat[5:,5:], decimals=3)) + [[1. 0.957 0.961 0.904 0.966] + [0.957 1. 0.977 0.982 0.997] + [0.961 0.977 1. 0.928 0.972] + [0.904 0.982 0.928 1. 0.976] + [0.966 0.997 0.972 0.976 1. ]] + >>> # Prunning + >>> prunned_sim_mat = clust.p_pruning(sim_mat, 0.3) + >>> print (np.around(prunned_sim_mat[5:,5:], decimals=3)) + [[1. 0. 0. 0. 0. ] + [0. 1. 0. 0.982 0.997] + [0. 0.977 1. 0. 0.972] + [0. 0.982 0. 1. 0.976] + [0. 0.997 0. 0.976 1. ]] + >>> # Symmetrization + >>> sym_prund_sim_mat = 0.5 * (prunned_sim_mat + prunned_sim_mat.T) + >>> print (np.around(sym_prund_sim_mat[5:,5:], decimals=3)) + [[1. 0. 0. 0. 0. ] + [0. 1. 0.489 0.982 0.997] + [0. 0.489 1. 0. 0.486] + [0. 0.982 0. 1. 0.976] + [0. 0.997 0.486 0.976 1. ]] + >>> # Laplacian + >>> laplacian = clust.get_laplacian(sym_prund_sim_mat) + >>> print (np.around(laplacian[5:,5:], decimals=3)) + [[ 1.999 0. 0. 0. 0. ] + [ 0. 2.468 -0.489 -0.982 -0.997] + [ 0. -0.489 0.975 0. -0.486] + [ 0. -0.982 0. 1.958 -0.976] + [ 0. -0.997 -0.486 -0.976 2.458]] + >>> # Spectral Embeddings + >>> spec_emb, num_of_spk = clust.get_spec_embs(laplacian, 3) + >>> print(num_of_spk) + 3 + >>> # Clustering + >>> clust.cluster_embs(spec_emb, num_of_spk) + >>> # print (clust.labels_) # [0 0 0 2 2 2 1 1 1 1] + >>> # Complete spectral clustering + >>> clust.do_spec_clust(emb, k_oracle=3, p_val=0.3) + >>> # print(clust.labels_) # [0 0 0 2 2 2 1 1 1 1] + """ + + def __init__(self, min_num_spkrs=2, max_num_spkrs=10): + + self.min_num_spkrs = min_num_spkrs + self.max_num_spkrs = max_num_spkrs + + def do_spec_clust(self, X, k_oracle, p_val): + """ + Function for spectral clustering. + + Arguments + --------- + X : array + (n_samples, n_features). + Embeddings extracted from the model. + k_oracle : int + Number of speakers (when oracle number of speakers). + p_val : float + p percent value to prune the affinity matrix. + """ + + # Similarity matrix computation + sim_mat = self.get_sim_mat(X) + + # Refining similarity matrix with p_val + prunned_sim_mat = self.p_pruning(sim_mat, p_val) + + # Symmetrization + sym_prund_sim_mat = 0.5 * (prunned_sim_mat + prunned_sim_mat.T) + + # Laplacian calculation + laplacian = self.get_laplacian(sym_prund_sim_mat) + + # Get Spectral Embeddings + emb, num_of_spk = self.get_spec_embs(laplacian, k_oracle) + + # Perform clustering + self.cluster_embs(emb, num_of_spk) + + def get_sim_mat(self, X): + """ + Returns the similarity matrix based on cosine similarities. + + Arguments + --------- + X : array + (n_samples, n_features). + Embeddings extracted from the model. + + Returns + ------- + M : array + (n_samples, n_samples). + Similarity matrix with cosine similarities between each pair of embedding. + """ + + # Cosine similarities + M = sklearn.metrics.pairwise.cosine_similarity(X, X) + return M + + def p_pruning(self, A, pval): + """ + Refine the affinity matrix by zeroing less similar values. + + Arguments + --------- + A : array + (n_samples, n_samples). + Affinity matrix. + pval : float + p-value to be retained in each row of the affinity matrix. + + Returns + ------- + A : array + (n_samples, n_samples). + Prunned affinity matrix based on p_val. + """ + + n_elems = int((1 - pval) * A.shape[0]) + + # For each row in a affinity matrix + for i in range(A.shape[0]): + low_indexes = np.argsort(A[i, :]) + low_indexes = low_indexes[0:n_elems] + + # Replace smaller similarity values by 0s + A[i, low_indexes] = 0 + + return A + + def get_laplacian(self, M): + """ + Returns the un-normalized laplacian for the given affinity matrix. + + Arguments + --------- + M : array + (n_samples, n_samples) + Affinity matrix. + + Returns + ------- + L : array + (n_samples, n_samples) + Laplacian matrix. + """ + + M[np.diag_indices(M.shape[0])] = 0 + D = np.sum(np.abs(M), axis=1) + D = np.diag(D) + L = D - M + return L + + def get_spec_embs(self, L, k_oracle=4): + """ + Returns spectral embeddings and estimates the number of speakers + using maximum Eigen gap. + + Arguments + --------- + L : array (n_samples, n_samples) + Laplacian matrix. + k_oracle : int + Number of speakers when the condition is oracle number of speakers, + else None. + + Returns + ------- + emb : array (n_samples, n_components) + Spectral embedding for each sample with n Eigen components. + num_of_spk : int + Estimated number of speakers. If the condition is set to the oracle + number of speakers then returns k_oracle. + """ + + lambdas, eig_vecs = scipy.linalg.eigh(L) + + # if params["oracle_n_spkrs"] is True: + if k_oracle is not None: + num_of_spk = k_oracle + else: + lambda_gap_list = self.get_eigen_gaps(lambdas[1:self.max_num_spkrs]) + + num_of_spk = (np.argmax( + lambda_gap_list[:min(self.max_num_spkrs, len(lambda_gap_list))]) + + 2) + + if num_of_spk < self.min_num_spkrs: + num_of_spk = self.min_num_spkrs + + emb = eig_vecs[:, 0:num_of_spk] + + return emb, num_of_spk + + def cluster_embs(self, emb, k): + """ + Clusters the embeddings using kmeans. + + Arguments + --------- + emb : array (n_samples, n_components) + Spectral embedding for each sample with n Eigen components. + k : int + Number of clusters to kmeans. + + Returns + ------- + self.labels_ : self + Labels for each sample embedding. + """ + _, self.labels_, _ = k_means(emb, k) + + def get_eigen_gaps(self, eig_vals): + """ + Returns the difference (gaps) between the Eigen values. + + Arguments + --------- + eig_vals : list + List of eigen values + + Returns + ------- + eig_vals_gap_list : list + List of differences (gaps) between adjacent Eigen values. + """ + + eig_vals_gap_list = [] + for i in range(len(eig_vals) - 1): + gap = float(eig_vals[i + 1]) - float(eig_vals[i]) + eig_vals_gap_list.append(gap) + + return eig_vals_gap_list + + +class SpecCluster(SpectralClustering): + def perform_sc(self, X, n_neighbors=10): + """ + Performs spectral clustering using sklearn on embeddings. + + Arguments + --------- + X : array (n_samples, n_features) + Embeddings to be clustered. + n_neighbors : int + Number of neighbors in estimating affinity matrix. + """ + + # Computation of affinity matrix + connectivity = kneighbors_graph( + X, + n_neighbors=n_neighbors, + include_self=True, ) + self.affinity_matrix_ = 0.5 * (connectivity + connectivity.T) + + # Perform spectral clustering on affinity matrix + self.labels_ = spectral_clustering( + self.affinity_matrix_, + n_clusters=self.n_clusters, ) + return self + + +def is_overlapped(end1, start2): + """ + Returns True if segments are overlapping. + + Arguments + --------- + end1 : float + End time of the first segment. + start2 : float + Start time of the second segment. + + Returns + ------- + overlapped : bool + True of segments overlapped else False. + + Example + ------- + >>> import diarization as diar + >>> diar.is_overlapped(5.5, 3.4) + True + >>> diar.is_overlapped(5.5, 6.4) + False + """ + + if start2 > end1: + return False + else: + return True + + +def merge_ssegs_same_speaker(lol): + """ + Merge adjacent sub-segs from the same speaker. + + Arguments + --------- + lol : list of list + Each list contains [rec_id, seg_start, seg_end, spkr_id]. + + Returns + ------- + new_lol : list of list + new_lol contains adjacent segments merged from the same speaker ID. + + Example + ------- + >>> import diarization as diar + >>> lol=[['r1', 5.5, 7.0, 's1'], + ... ['r1', 6.5, 9.0, 's1'], + ... ['r1', 8.0, 11.0, 's1'], + ... ['r1', 11.5, 13.0, 's2'], + ... ['r1', 14.0, 15.0, 's2'], + ... ['r1', 14.5, 15.0, 's1']] + >>> diar.merge_ssegs_same_speaker(lol) + [['r1', 5.5, 11.0, 's1'], ['r1', 11.5, 13.0, 's2'], ['r1', 14.0, 15.0, 's2'], ['r1', 14.5, 15.0, 's1']] + """ + + new_lol = [] + + # Start from the first sub-seg + sseg = lol[0] + flag = False + for i in range(1, len(lol)): + next_sseg = lol[i] + + # IF sub-segments overlap AND has same speaker THEN merge + if is_overlapped(sseg[2], next_sseg[1]) and sseg[3] == next_sseg[3]: + sseg[2] = next_sseg[2] # just update the end time + # This is important. For the last sseg, if it is the same speaker the merge + # Make sure we don't append the last segment once more. Hence, set FLAG=True + if i == len(lol) - 1: + flag = True + new_lol.append(sseg) + else: + new_lol.append(sseg) + sseg = next_sseg + + # Add last segment only when it was skipped earlier. + if flag is False: + new_lol.append(lol[-1]) + + return new_lol + + +def distribute_overlap(lol): + """ + Distributes the overlapped speech equally among the adjacent segments + with different speakers. + + Arguments + --------- + lol : list of list + It has each list structure as [rec_id, seg_start, seg_end, spkr_id]. + + Returns + ------- + new_lol : list of list + It contains the overlapped part equally divided among the adjacent + segments with different speaker IDs. + + Example + ------- + >>> import diarization as diar + >>> lol = [['r1', 5.5, 9.0, 's1'], + ... ['r1', 8.0, 11.0, 's2'], + ... ['r1', 11.5, 13.0, 's2'], + ... ['r1', 12.0, 15.0, 's1']] + >>> diar.distribute_overlap(lol) + [['r1', 5.5, 8.5, 's1'], ['r1', 8.5, 11.0, 's2'], ['r1', 11.5, 12.5, 's2'], ['r1', 12.5, 15.0, 's1']] + """ + + new_lol = [] + sseg = lol[0] + + # Add first sub-segment here to avoid error at: "if new_lol[-1] != sseg:" when new_lol is empty + # new_lol.append(sseg) + + for i in range(1, len(lol)): + next_sseg = lol[i] + # No need to check if they are different speakers. + # Because if segments are overlapped then they always have different speakers. + # This is because similar speaker's adjacent sub-segments are already merged by "merge_ssegs_same_speaker()" + + if is_overlapped(sseg[2], next_sseg[1]): + + # Get overlap duration. + # Now this overlap will be divided equally between adjacent segments. + overlap = sseg[2] - next_sseg[1] + + # Update end time of old seg + sseg[2] = sseg[2] - (overlap / 2.0) + + # Update start time of next seg + next_sseg[1] = next_sseg[1] + (overlap / 2.0) + + if len(new_lol) == 0: + # For first sub-segment entry + new_lol.append(sseg) + else: + # To avoid duplicate entries + if new_lol[-1] != sseg: + new_lol.append(sseg) + + # Current sub-segment is next sub-segment + sseg = next_sseg + + else: + # For the first sseg + if len(new_lol) == 0: + new_lol.append(sseg) + else: + # To avoid duplicate entries + if new_lol[-1] != sseg: + new_lol.append(sseg) + + # Update the current sub-segment + sseg = next_sseg + + # Add the remaining last sub-segment + new_lol.append(next_sseg) + + return new_lol + + +def write_rttm(segs_list, out_rttm_file): + """ + Writes the segment list in RTTM format (A standard NIST format). + + Arguments + --------- + segs_list : list of list + Each list contains [rec_id, seg_start, seg_end, spkr_id]. + out_rttm_file : str + Path of the output RTTM file. + """ + + rttm = [] + rec_id = segs_list[0][0] + + for seg in segs_list: + new_row = [ + "SPEAKER", + rec_id, + "0", + str(round(seg[1], 4)), + str(round(seg[2] - seg[1], 4)), + "", + "", + seg[3], + "", + "", + ] + rttm.append(new_row) + + with open(out_rttm_file, "w") as f: + for row in rttm: + line_str = " ".join(row) + f.write("%s\n" % line_str) + + +def do_AHC(diary_obj, out_rttm_file, rec_id, k_oracle=4, p_val=0.3): + """ + Performs Agglomerative Hierarchical Clustering on embeddings. + + Arguments + --------- + diary_obj : EmbeddingMeta type + Contains embeddings in diary_obj.stats and segment IDs in diary_obj.segset. + out_rttm_file : str + Path of the output RTTM file. + rec_id : str + Recording ID for the recording under processing. + k : int + Number of speaker (None, if it has to be estimated). + pval : float + `pval` for prunning affinity matrix. Used only when number of speakers + are unknown. Note that this is just for experiment. Prefer Spectral clustering + for better clustering results. + """ + + from sklearn.cluster import AgglomerativeClustering + + # p_val is the threshold_val (for AHC) + diary_obj.norm_stats() + + # processing + if k_oracle is not None: + num_of_spk = k_oracle + + clustering = AgglomerativeClustering( + n_clusters=num_of_spk, + affinity="cosine", + linkage="average", ).fit(diary_obj.stats) + labels = clustering.labels_ + + else: + # Estimate num of using max eigen gap with `cos` affinity matrix. + # This is just for experimentation. + clustering = AgglomerativeClustering( + n_clusters=None, + affinity="cosine", + linkage="average", + distance_threshold=p_val, ).fit(diary_obj.stats) + labels = clustering.labels_ + + # Convert labels to speaker boundaries + subseg_ids = diary_obj.segset + lol = [] + + for i in range(labels.shape[0]): + spkr_id = rec_id + "_" + str(labels[i]) + + sub_seg = subseg_ids[i] + + splitted = sub_seg.rsplit("_", 2) + rec_id = str(splitted[0]) + sseg_start = float(splitted[1]) + sseg_end = float(splitted[2]) + + a = [rec_id, sseg_start, sseg_end, spkr_id] + lol.append(a) + + # Sorting based on start time of sub-segment + lol.sort(key=lambda x: float(x[1])) + + # Merge and split in 2 simple steps: (i) Merge sseg of same speakers then (ii) split different speakers + # Step 1: Merge adjacent sub-segments that belong to same speaker (or cluster) + lol = merge_ssegs_same_speaker(lol) + + # Step 2: Distribute duration of adjacent overlapping sub-segments belonging to different speakers (or cluster) + # Taking mid-point as the splitting time location. + lol = distribute_overlap(lol) + + # logger.info("Completed diarizing " + rec_id) + write_rttm(lol, out_rttm_file) + + +def do_spec_clustering(diary_obj, out_rttm_file, rec_id, k, pval, affinity_type, + n_neighbors): + """ + Performs spectral clustering on embeddings. This function calls specific + clustering algorithms as per affinity. + + Arguments + --------- + diary_obj : EmbeddingMeta type + Contains embeddings in diary_obj.stats and segment IDs in diary_obj.segset. + out_rttm_file : str + Path of the output RTTM file. + rec_id : str + Recording ID for the recording under processing. + k : int + Number of speaker (None, if it has to be estimated). + pval : float + `pval` for prunning affinity matrix. + affinity_type : str + Type of similarity to be used to get affinity matrix (cos or nn). + """ + + if affinity_type == "cos": + clust_obj = SpecClustUnorm(min_num_spkrs=2, max_num_spkrs=10) + k_oracle = k # use it only when oracle num of speakers + clust_obj.do_spec_clust(diary_obj.stats, k_oracle, pval) + labels = clust_obj.labels_ + else: + clust_obj = SpecCluster( + n_clusters=k, + assign_labels="kmeans", + random_state=1234, + affinity="nearest_neighbors", ) + clust_obj.perform_sc(diary_obj.stats, n_neighbors) + labels = clust_obj.labels_ + + # Convert labels to speaker boundaries + subseg_ids = diary_obj.segset + lol = [] + + for i in range(labels.shape[0]): + spkr_id = rec_id + "_" + str(labels[i]) + + sub_seg = subseg_ids[i] + + splitted = sub_seg.rsplit("_", 2) + rec_id = str(splitted[0]) + sseg_start = float(splitted[1]) + sseg_end = float(splitted[2]) + + a = [rec_id, sseg_start, sseg_end, spkr_id] + lol.append(a) + + # Sorting based on start time of sub-segment + lol.sort(key=lambda x: float(x[1])) + + # Merge and split in 2 simple steps: (i) Merge sseg of same speakers then (ii) split different speakers + # Step 1: Merge adjacent sub-segments that belong to same speaker (or cluster) + lol = merge_ssegs_same_speaker(lol) + + # Step 2: Distribute duration of adjacent overlapping sub-segments belonging to different speakers (or cluster) + # Taking mid-point as the splitting time location. + lol = distribute_overlap(lol) + + # logger.info("Completed diarizing " + rec_id) + write_rttm(lol, out_rttm_file) + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser( + prog='python diarization.py --backend AHC', description='diarizing') + parser.add_argument( + '--sys_rttm_dir', + required=False, + help='Directory to store system RTTM files') + parser.add_argument( + '--ref_rttm_dir', + required=False, + help='Directory to store reference RTTM files') + parser.add_argument( + '--backend', default="AHC", help='type of backend, AHC or SC or kmeans') + parser.add_argument( + '--oracle_n_spkrs', + default=True, + type=strtobool, + help='Oracle num of speakers') + parser.add_argument( + '--mic_type', + default="Mix-Headset", + help='Type of microphone to be used') + parser.add_argument( + '--affinity', default="cos", help='affinity matrix, cos or nn') + parser.add_argument( + '--max_subseg_dur', + default=3.0, + type=float, + help='Duration in seconds of a subsegments to be prepared from larger segments' + ) + parser.add_argument( + '--overlap', + default=1.5, + type=float, + help='Overlap duration in seconds between adjacent subsegments') + + args = parser.parse_args() + + pval = 0.3 + rec_id = "utt0001" + n_neighbors = 10 + out_rttm_file = "./out.rttm" + + embeddings = np.empty(shape=[0, 32], dtype=np.float64) + segset = [] + + for i in range(10): + seg = [rec_id + "_" + str(i) + "_" + str(i + 1)] + segset = segset + seg + emb = np.random.rand(1, 32) + embeddings = np.concatenate((embeddings, emb), axis=0) + + segset = np.array(segset, dtype="|O") + stat_obj = EmbeddingMeta(segset, embeddings) + if args.oracle_n_spkrs is True: + num_spkrs = 2 + + if args.backend == "SC": + print("begin SC ") + do_spec_clustering( + stat_obj, + out_rttm_file, + rec_id, + num_spkrs, + pval, + args.affinity, + n_neighbors, ) + if args.backend == "AHC": + print("begin AHC ") + do_AHC(stat_obj, out_rttm_file, rec_id, num_spkrs, pval) diff --git a/ernie-sat/paddlespeech/vector/exps/__init__.py b/ernie-sat/paddlespeech/vector/exps/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/extract_emb.py b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/extract_emb.py new file mode 100644 index 0000000..686de93 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/extract_emb.py @@ -0,0 +1,119 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import time + +import paddle +from yacs.config import CfgNode + +from paddleaudio.backends import load as load_audio +from paddleaudio.compliance.librosa import melspectrogram +from paddlespeech.s2t.utils.log import Log +from paddlespeech.vector.io.batch import feature_normalize +from paddlespeech.vector.models.ecapa_tdnn import EcapaTdnn +from paddlespeech.vector.modules.sid_model import SpeakerIdetification +from paddlespeech.vector.training.seeding import seed_everything + +logger = Log(__name__).getlog() + + +def extract_audio_embedding(args, config): + # stage 0: set the training device, cpu or gpu + paddle.set_device(args.device) + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + # stage 1: build the dnn backbone model network + ecapa_tdnn = EcapaTdnn(**config.model) + + # stage4: build the speaker verification train instance with backbone model + model = SpeakerIdetification( + backbone=ecapa_tdnn, num_class=config.num_speakers) + # stage 2: load the pre-trained model + args.load_checkpoint = os.path.abspath( + os.path.expanduser(args.load_checkpoint)) + + # load model checkpoint to sid model + state_dict = paddle.load( + os.path.join(args.load_checkpoint, 'model.pdparams')) + model.set_state_dict(state_dict) + logger.info(f'Checkpoint loaded from {args.load_checkpoint}') + + # stage 3: we must set the model to eval mode + model.eval() + + # stage 4: read the audio data and extract the embedding + # wavform is one dimension numpy array + waveform, sr = load_audio(args.audio_path) + + # feat type is numpy array, whose shape is [dim, time] + # we need convert the audio feat to one-batch shape [batch, dim, time], where the batch is one + # so the final shape is [1, dim, time] + start_time = time.time() + feat = melspectrogram( + x=waveform, + sr=config.sr, + n_mels=config.n_mels, + window_size=config.window_size, + hop_length=config.hop_size) + feat = paddle.to_tensor(feat).unsqueeze(0) + + # in inference period, the lengths is all one without padding + lengths = paddle.ones([1]) + feat = feature_normalize(feat, mean_norm=True, std_norm=False) + + # model backbone network forward the feats and get the embedding + embedding = model.backbone( + feat, lengths).squeeze().numpy() # (1, emb_size, 1) -> (emb_size) + elapsed_time = time.time() - start_time + audio_length = waveform.shape[0] / sr + + # stage 5: do global norm with external mean and std + rtf = elapsed_time / audio_length + logger.info(f"{args.device} rft={rtf}") + + return embedding + + +if __name__ == "__main__": + # yapf: disable + parser = argparse.ArgumentParser(__doc__) + parser.add_argument('--device', + choices=['cpu', 'gpu'], + default="cpu", + help="Select which device to train model, defaults to gpu.") + parser.add_argument("--config", + default=None, + type=str, + help="configuration file") + parser.add_argument("--load-checkpoint", + type=str, + default='', + help="Directory to load model checkpoint to contiune trainning.") + parser.add_argument("--audio-path", + default="./data/demo.wav", + type=str, + help="Single audio file path") + args = parser.parse_args() + # yapf: enable + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + + config.freeze() + print(config) + + extract_audio_embedding(args, config) diff --git a/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/test.py b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/test.py new file mode 100644 index 0000000..d0de6dc --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/test.py @@ -0,0 +1,203 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os + +import numpy as np +import paddle +from paddle.io import BatchSampler +from paddle.io import DataLoader +from tqdm import tqdm +from yacs.config import CfgNode + +from paddleaudio.datasets import VoxCeleb +from paddleaudio.metric import compute_eer +from paddlespeech.s2t.utils.log import Log +from paddlespeech.vector.io.batch import batch_feature_normalize +from paddlespeech.vector.models.ecapa_tdnn import EcapaTdnn +from paddlespeech.vector.modules.sid_model import SpeakerIdetification +from paddlespeech.vector.training.seeding import seed_everything + +logger = Log(__name__).getlog() + + +def main(args, config): + # stage0: set the training device, cpu or gpu + paddle.set_device(args.device) + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + # stage1: build the dnn backbone model network + ecapa_tdnn = EcapaTdnn(**config.model) + + # stage2: build the speaker verification eval instance with backbone model + model = SpeakerIdetification( + backbone=ecapa_tdnn, num_class=config.num_speakers) + + # stage3: load the pre-trained model + # we get the last model from the epoch and save_interval + args.load_checkpoint = os.path.abspath( + os.path.expanduser(args.load_checkpoint)) + + # load model checkpoint to sid model + state_dict = paddle.load( + os.path.join(args.load_checkpoint, 'model.pdparams')) + model.set_state_dict(state_dict) + logger.info(f'Checkpoint loaded from {args.load_checkpoint}') + + # stage4: construct the enroll and test dataloader + + enroll_dataset = VoxCeleb( + subset='enroll', + target_dir=args.data_dir, + feat_type='melspectrogram', + random_chunk=False, + n_mels=config.n_mels, + window_size=config.window_size, + hop_length=config.hop_size) + enroll_sampler = BatchSampler( + enroll_dataset, batch_size=config.batch_size, + shuffle=True) # Shuffle to make embedding normalization more robust. + enrol_loader = DataLoader(enroll_dataset, + batch_sampler=enroll_sampler, + collate_fn=lambda x: batch_feature_normalize( + x, mean_norm=True, std_norm=False), + num_workers=config.num_workers, + return_list=True,) + test_dataset = VoxCeleb( + subset='test', + target_dir=args.data_dir, + feat_type='melspectrogram', + random_chunk=False, + n_mels=config.n_mels, + window_size=config.window_size, + hop_length=config.hop_size) + + test_sampler = BatchSampler( + test_dataset, batch_size=config.batch_size, shuffle=True) + test_loader = DataLoader(test_dataset, + batch_sampler=test_sampler, + collate_fn=lambda x: batch_feature_normalize( + x, mean_norm=True, std_norm=False), + num_workers=config.num_workers, + return_list=True,) + # stage5: we must set the model to eval mode + model.eval() + + # stage6: global embedding norm to imporve the performance + logger.info(f"global embedding norm: {config.global_embedding_norm}") + if config.global_embedding_norm: + global_embedding_mean = None + global_embedding_std = None + mean_norm_flag = config.embedding_mean_norm + std_norm_flag = config.embedding_std_norm + batch_count = 0 + + # stage7: Compute embeddings of audios in enrol and test dataset from model. + id2embedding = {} + # Run multi times to make embedding normalization more stable. + for i in range(2): + for dl in [enrol_loader, test_loader]: + logger.info( + f'Loop {[i+1]}: Computing embeddings on {dl.dataset.subset} dataset' + ) + with paddle.no_grad(): + for batch_idx, batch in enumerate(tqdm(dl)): + + # stage 8-1: extrac the audio embedding + ids, feats, lengths = batch['ids'], batch['feats'], batch[ + 'lengths'] + embeddings = model.backbone(feats, lengths).squeeze( + -1).numpy() # (N, emb_size, 1) -> (N, emb_size) + + # Global embedding normalization. + # if we use the global embedding norm + # eer can reduece about relative 10% + if config.global_embedding_norm: + batch_count += 1 + current_mean = embeddings.mean( + axis=0) if mean_norm_flag else 0 + current_std = embeddings.std( + axis=0) if std_norm_flag else 1 + # Update global mean and std. + if global_embedding_mean is None and global_embedding_std is None: + global_embedding_mean, global_embedding_std = current_mean, current_std + else: + weight = 1 / batch_count # Weight decay by batches. + global_embedding_mean = ( + 1 - weight + ) * global_embedding_mean + weight * current_mean + global_embedding_std = ( + 1 - weight + ) * global_embedding_std + weight * current_std + # Apply global embedding normalization. + embeddings = (embeddings - global_embedding_mean + ) / global_embedding_std + + # Update embedding dict. + id2embedding.update(dict(zip(ids, embeddings))) + + # stage 8: Compute cosine scores. + labels = [] + enroll_ids = [] + test_ids = [] + logger.info(f"read the trial from {VoxCeleb.veri_test_file}") + with open(VoxCeleb.veri_test_file, 'r') as f: + for line in f.readlines(): + label, enroll_id, test_id = line.strip().split(' ') + labels.append(int(label)) + enroll_ids.append(enroll_id.split('.')[0].replace('/', '-')) + test_ids.append(test_id.split('.')[0].replace('/', '-')) + + cos_sim_func = paddle.nn.CosineSimilarity(axis=1) + enrol_embeddings, test_embeddings = map(lambda ids: paddle.to_tensor( + np.asarray([id2embedding[uttid] for uttid in ids], dtype='float32')), + [enroll_ids, test_ids + ]) # (N, emb_size) + scores = cos_sim_func(enrol_embeddings, test_embeddings) + EER, threshold = compute_eer(np.asarray(labels), scores.numpy()) + logger.info( + f'EER of verification test: {EER*100:.4f}%, score threshold: {threshold:.5f}' + ) + + +if __name__ == "__main__": + # yapf: disable + parser = argparse.ArgumentParser(__doc__) + parser.add_argument('--device', + choices=['cpu', 'gpu'], + default="gpu", + help="Select which device to train model, defaults to gpu.") + parser.add_argument("--config", + default=None, + type=str, + help="configuration file") + parser.add_argument("--data-dir", + default="./data/", + type=str, + help="data directory") + parser.add_argument("--load-checkpoint", + type=str, + default='', + help="Directory to load model checkpoint to contiune trainning.") + args = parser.parse_args() + # yapf: enable + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + + config.freeze() + print(config) + main(args, config) diff --git a/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/train.py b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/train.py new file mode 100644 index 0000000..257b97a --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ecapa_tdnn/train.py @@ -0,0 +1,351 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +import os +import time + +import numpy as np +import paddle +from paddle.io import BatchSampler +from paddle.io import DataLoader +from paddle.io import DistributedBatchSampler +from yacs.config import CfgNode + +from paddleaudio.compliance.librosa import melspectrogram +from paddleaudio.datasets.voxceleb import VoxCeleb +from paddlespeech.s2t.utils.log import Log +from paddlespeech.vector.io.augment import build_augment_pipeline +from paddlespeech.vector.io.augment import waveform_augment +from paddlespeech.vector.io.batch import batch_pad_right +from paddlespeech.vector.io.batch import feature_normalize +from paddlespeech.vector.io.batch import waveform_collate_fn +from paddlespeech.vector.models.ecapa_tdnn import EcapaTdnn +from paddlespeech.vector.modules.loss import AdditiveAngularMargin +from paddlespeech.vector.modules.loss import LogSoftmaxWrapper +from paddlespeech.vector.modules.sid_model import SpeakerIdetification +from paddlespeech.vector.training.scheduler import CyclicLRScheduler +from paddlespeech.vector.training.seeding import seed_everything +from paddlespeech.vector.utils.time import Timer + +logger = Log(__name__).getlog() + + +def main(args, config): + # stage0: set the training device, cpu or gpu + paddle.set_device(args.device) + + # stage1: we must call the paddle.distributed.init_parallel_env() api at the begining + paddle.distributed.init_parallel_env() + nranks = paddle.distributed.get_world_size() + local_rank = paddle.distributed.get_rank() + # set the random seed, it is a must for multiprocess training + seed_everything(config.seed) + + # stage2: data prepare, such vox1 and vox2 data, and augment noise data and pipline + # note: some cmd must do in rank==0, so wo will refactor the data prepare code + train_dataset = VoxCeleb('train', target_dir=args.data_dir) + dev_dataset = VoxCeleb('dev', target_dir=args.data_dir) + + if config.augment: + augment_pipeline = build_augment_pipeline(target_dir=args.data_dir) + else: + augment_pipeline = [] + + # stage3: build the dnn backbone model network + ecapa_tdnn = EcapaTdnn(**config.model) + + # stage4: build the speaker verification train instance with backbone model + model = SpeakerIdetification( + backbone=ecapa_tdnn, num_class=VoxCeleb.num_speakers) + + # stage5: build the optimizer, we now only construct the AdamW optimizer + # 140000 is single gpu steps + # so, in multi-gpu mode, wo reduce the step_size to 140000//nranks to enable CyclicLRScheduler + lr_schedule = CyclicLRScheduler( + base_lr=config.learning_rate, max_lr=1e-3, step_size=140000 // nranks) + optimizer = paddle.optimizer.AdamW( + learning_rate=lr_schedule, parameters=model.parameters()) + + # stage6: build the loss function, we now only support LogSoftmaxWrapper + criterion = LogSoftmaxWrapper( + loss_fn=AdditiveAngularMargin(margin=0.2, scale=30)) + + # stage7: confirm training start epoch + # if pre-trained model exists, start epoch confirmed by the pre-trained model + start_epoch = 0 + if args.load_checkpoint: + logger.info("load the check point") + args.load_checkpoint = os.path.abspath( + os.path.expanduser(args.load_checkpoint)) + try: + # load model checkpoint + state_dict = paddle.load( + os.path.join(args.load_checkpoint, 'model.pdparams')) + model.set_state_dict(state_dict) + + # load optimizer checkpoint + state_dict = paddle.load( + os.path.join(args.load_checkpoint, 'model.pdopt')) + optimizer.set_state_dict(state_dict) + if local_rank == 0: + logger.info(f'Checkpoint loaded from {args.load_checkpoint}') + except FileExistsError: + if local_rank == 0: + logger.info('Train from scratch.') + + try: + start_epoch = int(args.load_checkpoint[-1]) + logger.info(f'Restore training from epoch {start_epoch}.') + except ValueError: + pass + + # stage8: we build the batch sampler for paddle.DataLoader + train_sampler = DistributedBatchSampler( + train_dataset, + batch_size=config.batch_size, + shuffle=True, + drop_last=False) + train_loader = DataLoader( + train_dataset, + batch_sampler=train_sampler, + num_workers=config.num_workers, + collate_fn=waveform_collate_fn, + return_list=True, + use_buffer_reader=True, ) + + # stage9: start to train + # we will comment the training process + steps_per_epoch = len(train_sampler) + timer = Timer(steps_per_epoch * config.epochs) + last_saved_epoch = "" + timer.start() + + for epoch in range(start_epoch + 1, config.epochs + 1): + # at the begining, model must set to train mode + model.train() + + avg_loss = 0 + num_corrects = 0 + num_samples = 0 + train_reader_cost = 0.0 + train_feat_cost = 0.0 + train_run_cost = 0.0 + + reader_start = time.time() + for batch_idx, batch in enumerate(train_loader): + train_reader_cost += time.time() - reader_start + + # stage 9-1: batch data is audio sample points and speaker id label + feat_start = time.time() + waveforms, labels = batch['waveforms'], batch['labels'] + waveforms, lengths = batch_pad_right(waveforms.numpy()) + waveforms = paddle.to_tensor(waveforms) + + # stage 9-2: audio sample augment method, which is done on the audio sample point + # the original wavefrom and the augmented waveform is concatented in a batch + # eg. five augment method in the augment pipeline + # the final data nums is batch_size * [five + one] + # -> five augmented waveform batch plus one original batch waveform + if len(augment_pipeline) != 0: + waveforms = waveform_augment(waveforms, augment_pipeline) + labels = paddle.concat( + [labels for i in range(len(augment_pipeline) + 1)]) + + # stage 9-3: extract the audio feats,such fbank, mfcc, spectrogram + feats = [] + for waveform in waveforms.numpy(): + feat = melspectrogram( + x=waveform, + sr=config.sr, + n_mels=config.n_mels, + window_size=config.window_size, + hop_length=config.hop_size) + feats.append(feat) + feats = paddle.to_tensor(np.asarray(feats)) + + # stage 9-4: feature normalize, which help converge and imporve the performance + feats = feature_normalize( + feats, mean_norm=True, std_norm=False) # Features normalization + train_feat_cost += time.time() - feat_start + + # stage 9-5: model forward, such ecapa-tdnn, x-vector + train_start = time.time() + logits = model(feats) + + # stage 9-6: loss function criterion, such AngularMargin, AdditiveAngularMargin + loss = criterion(logits, labels) + + # stage 9-7: update the gradient and clear the gradient cache + loss.backward() + optimizer.step() + if isinstance(optimizer._learning_rate, + paddle.optimizer.lr.LRScheduler): + optimizer._learning_rate.step() + optimizer.clear_grad() + train_run_cost += time.time() - train_start + + # stage 9-8: Calculate average loss per batch + avg_loss += loss.numpy()[0] + + # stage 9-9: Calculate metrics, which is one-best accuracy + preds = paddle.argmax(logits, axis=1) + num_corrects += (preds == labels).numpy().sum() + num_samples += feats.shape[0] + timer.count() # step plus one in timer + + # stage 9-10: print the log information only on 0-rank per log-freq batchs + if (batch_idx + 1) % config.log_interval == 0 and local_rank == 0: + lr = optimizer.get_lr() + avg_loss /= config.log_interval + avg_acc = num_corrects / num_samples + + print_msg = 'Train Epoch={}/{}, Step={}/{}'.format( + epoch, config.epochs, batch_idx + 1, steps_per_epoch) + print_msg += ' loss={:.4f}'.format(avg_loss) + print_msg += ' acc={:.4f}'.format(avg_acc) + print_msg += ' avg_reader_cost: {:.5f} sec,'.format( + train_reader_cost / config.log_interval) + print_msg += ' avg_feat_cost: {:.5f} sec,'.format( + train_feat_cost / config.log_interval) + print_msg += ' avg_train_cost: {:.5f} sec,'.format( + train_run_cost / config.log_interval) + print_msg += ' lr={:.4E} step/sec={:.2f} | ETA {}'.format( + lr, timer.timing, timer.eta) + logger.info(print_msg) + + avg_loss = 0 + num_corrects = 0 + num_samples = 0 + train_reader_cost = 0.0 + train_feat_cost = 0.0 + train_run_cost = 0.0 + + reader_start = time.time() + + # stage 9-11: save the model parameters only on 0-rank per save-freq batchs + if epoch % config.save_interval == 0 and batch_idx + 1 == steps_per_epoch: + if local_rank != 0: + paddle.distributed.barrier( + ) # Wait for valid step in main process + continue # Resume trainning on other process + + # stage 9-12: construct the valid dataset dataloader + dev_sampler = BatchSampler( + dev_dataset, + batch_size=config.batch_size, + shuffle=False, + drop_last=False) + dev_loader = DataLoader( + dev_dataset, + batch_sampler=dev_sampler, + collate_fn=waveform_collate_fn, + num_workers=config.num_workers, + return_list=True, ) + + # set the model to eval mode + model.eval() + num_corrects = 0 + num_samples = 0 + + # stage 9-13: evaluation the valid dataset batch data + logger.info('Evaluate on validation dataset') + with paddle.no_grad(): + for batch_idx, batch in enumerate(dev_loader): + waveforms, labels = batch['waveforms'], batch['labels'] + + feats = [] + for waveform in waveforms.numpy(): + feat = melspectrogram( + x=waveform, + sr=config.sr, + n_mels=config.n_mels, + window_size=config.window_size, + hop_length=config.hop_size) + feats.append(feat) + + feats = paddle.to_tensor(np.asarray(feats)) + feats = feature_normalize( + feats, mean_norm=True, std_norm=False) + logits = model(feats) + + preds = paddle.argmax(logits, axis=1) + num_corrects += (preds == labels).numpy().sum() + num_samples += feats.shape[0] + + print_msg = '[Evaluation result]' + print_msg += ' dev_acc={:.4f}'.format(num_corrects / num_samples) + logger.info(print_msg) + + # stage 9-14: Save model parameters + save_dir = os.path.join(args.checkpoint_dir, + 'epoch_{}'.format(epoch)) + last_saved_epoch = os.path.join('epoch_{}'.format(epoch), + "model.pdparams") + logger.info('Saving model checkpoint to {}'.format(save_dir)) + paddle.save(model.state_dict(), + os.path.join(save_dir, 'model.pdparams')) + paddle.save(optimizer.state_dict(), + os.path.join(save_dir, 'model.pdopt')) + + if nranks > 1: + paddle.distributed.barrier() # Main process + + # stage 10: create the final trained model.pdparams with soft link + if local_rank == 0: + final_model = os.path.join(args.checkpoint_dir, "model.pdparams") + logger.info(f"we will create the final model: {final_model}") + if os.path.islink(final_model): + logger.info( + f"An {final_model} already exists, we will rm is and create it again" + ) + os.unlink(final_model) + os.symlink(last_saved_epoch, final_model) + + +if __name__ == "__main__": + # yapf: disable + parser = argparse.ArgumentParser(__doc__) + parser.add_argument('--device', + choices=['cpu', 'gpu'], + default="cpu", + help="Select which device to train model, defaults to gpu.") + parser.add_argument("--config", + default=None, + type=str, + help="configuration file") + parser.add_argument("--data-dir", + default="./data/", + type=str, + help="data directory") + parser.add_argument("--load-checkpoint", + type=str, + default=None, + help="Directory to load model checkpoint to contiune trainning.") + parser.add_argument("--checkpoint-dir", + type=str, + default='./checkpoint', + help="Directory to save model checkpoints.") + + args = parser.parse_args() + # yapf: enable + + # https://yaml.org/type/float.html + config = CfgNode(new_allowed=True) + if args.config: + config.merge_from_file(args.config) + + config.freeze() + print(config) + + main(args, config) diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/__init__.py b/ernie-sat/paddlespeech/vector/exps/ge2e/__init__.py new file mode 100644 index 0000000..abf198b --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/audio_processor.py b/ernie-sat/paddlespeech/vector/exps/ge2e/audio_processor.py new file mode 100644 index 0000000..1ab0419 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/audio_processor.py @@ -0,0 +1,246 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import struct +from pathlib import Path +from warnings import warn + +import librosa +import numpy as np +from scipy.ndimage.morphology import binary_dilation + +try: + import webrtcvad +except ModuleNotFoundError: + warn("Unable to import 'webrtcvad'." + "This package enables noise removal and is recommended.") + webrtcvad = None + +INT16_MAX = (2**15) - 1 + + +def normalize_volume(wav, target_dBFS, increase_only=False, + decrease_only=False): + # this function implements Loudness normalization, instead of peak + # normalization, See https://en.wikipedia.org/wiki/Audio_normalization + # dBFS: Decibels relative to full scale + # See https://en.wikipedia.org/wiki/DBFS for more details + # for 16Bit PCM audio, minimal level is -96dB + # compute the mean dBFS and adjust to target dBFS, with by increasing + # or decreasing + if increase_only and decrease_only: + raise ValueError("Both increase only and decrease only are set") + dBFS_change = target_dBFS - 10 * np.log10(np.mean(wav**2)) + if dBFS_change < 0 and increase_only: + return wav + if dBFS_change > 0 and decrease_only: + return wav + gain = 10**(dBFS_change / 20) + return wav * gain + + +def trim_long_silences(wav, + vad_window_length: int, + vad_moving_average_width: int, + vad_max_silence_length: int, + sampling_rate: int): + """ + Ensures that segments without voice in the waveform remain no longer than a + threshold determined by the VAD parameters in params.py. + Parameters + ---------- + wav : np.array + the raw waveform as a numpy array of floats + Returns + ---------- + np.array + the same waveform with silences trimmed away (length <= original wav length) + """ + # Compute the voice detection window size + samples_per_window = (vad_window_length * sampling_rate) // 1000 + + # Trim the end of the audio to have a multiple of the window size + wav = wav[:len(wav) - (len(wav) % samples_per_window)] + + # Convert the float waveform to 16-bit mono PCM + pcm_wave = struct.pack("%dh" % len(wav), + *(np.round(wav * INT16_MAX)).astype(np.int16)) + + # Perform voice activation detection + voice_flags = [] + vad = webrtcvad.Vad(mode=3) + for window_start in range(0, len(wav), samples_per_window): + window_end = window_start + samples_per_window + voice_flags.append( + vad.is_speech( + pcm_wave[window_start * 2:window_end * 2], + sample_rate=sampling_rate)) + voice_flags = np.array(voice_flags) + + # Smooth the voice detection with a moving average + def moving_average(array, width): + array_padded = np.concatenate((np.zeros((width - 1) // 2), array, + np.zeros(width // 2))) + ret = np.cumsum(array_padded, dtype=float) + ret[width:] = ret[width:] - ret[:-width] + return ret[width - 1:] / width + + audio_mask = moving_average(voice_flags, vad_moving_average_width) + audio_mask = np.round(audio_mask).astype(bool) + + # Dilate the voiced regions + audio_mask = binary_dilation(audio_mask, + np.ones(vad_max_silence_length + 1)) + audio_mask = np.repeat(audio_mask, samples_per_window) + + return wav[audio_mask] + + +def compute_partial_slices(n_samples: int, + partial_utterance_n_frames: int, + hop_length: int, + min_pad_coverage: float=0.75, + overlap: float=0.5): + """ + Computes where to split an utterance waveform and its corresponding mel spectrogram to obtain + partial utterances of each. Both the waveform and the mel + spectrogram slices are returned, so as to make each partial utterance waveform correspond to + its spectrogram. This function assumes that the mel spectrogram parameters used are those + defined in params_data.py. + + The returned ranges may be indexing further than the length of the waveform. It is + recommended that you pad the waveform with zeros up to wave_slices[-1].stop. + Parameters + ---------- + n_samples : int + the number of samples in the waveform. + partial_utterance_n_frames : int + the number of mel spectrogram frames in each partial utterance. + + min_pad_coverage : int + when reaching the last partial utterance, it may or may not have enough frames. + If at least of are present, + then the last partial utterance will be considered, as if we padded the audio. Otherwise, + it will be discarded, as if we trimmed the audio. If there aren't enough frames for 1 partial + utterance, this parameter is ignored so that the function always returns at least 1 slice. + overlap : float + by how much the partial utterance should overlap. If set to 0, the partial utterances are entirely disjoint. + Returns + ---------- + the waveform slices and mel spectrogram slices as lists of array slices. + Index respectively the waveform and the mel spectrogram with these slices to obtain the partialutterances. + """ + assert 0 <= overlap < 1 + assert 0 < min_pad_coverage <= 1 + + # librosa's function to compute num_frames from num_samples + n_frames = int(np.ceil((n_samples + 1) / hop_length)) + # frame shift between ajacent partials + frame_step = max(1, + int(np.round(partial_utterance_n_frames * (1 - overlap)))) + + # Compute the slices + wav_slices, mel_slices = [], [] + steps = max(1, n_frames - partial_utterance_n_frames + frame_step + 1) + for i in range(0, steps, frame_step): + mel_range = np.array([i, i + partial_utterance_n_frames]) + wav_range = mel_range * hop_length + mel_slices.append(slice(*mel_range)) + wav_slices.append(slice(*wav_range)) + + # Evaluate whether extra padding is warranted or not + last_wav_range = wav_slices[-1] + coverage = (n_samples - last_wav_range.start) / ( + last_wav_range.stop - last_wav_range.start) + if coverage < min_pad_coverage and len(mel_slices) > 1: + mel_slices = mel_slices[:-1] + wav_slices = wav_slices[:-1] + + return wav_slices, mel_slices + + +class SpeakerVerificationPreprocessor(object): + def __init__(self, + sampling_rate: int, + audio_norm_target_dBFS: float, + vad_window_length, + vad_moving_average_width, + vad_max_silence_length, + mel_window_length, + mel_window_step, + n_mels, + partial_n_frames: int, + min_pad_coverage: float=0.75, + partial_overlap_ratio: float=0.5): + self.sampling_rate = sampling_rate + self.audio_norm_target_dBFS = audio_norm_target_dBFS + + self.vad_window_length = vad_window_length + self.vad_moving_average_width = vad_moving_average_width + self.vad_max_silence_length = vad_max_silence_length + + self.n_fft = int(mel_window_length * sampling_rate / 1000) + self.hop_length = int(mel_window_step * sampling_rate / 1000) + self.n_mels = n_mels + + self.partial_n_frames = partial_n_frames + self.min_pad_coverage = min_pad_coverage + self.partial_overlap_ratio = partial_overlap_ratio + + def preprocess_wav(self, fpath_or_wav, source_sr=None): + # Load the wav from disk if needed + if isinstance(fpath_or_wav, (str, Path)): + wav, source_sr = librosa.load(str(fpath_or_wav), sr=None) + else: + wav = fpath_or_wav + + # Resample if numpy.array is passed and sr does not match + if source_sr is not None and source_sr != self.sampling_rate: + wav = librosa.resample( + wav, orig_sr=source_sr, target_sr=self.sampling_rate) + + # loudness normalization + wav = normalize_volume( + wav, self.audio_norm_target_dBFS, increase_only=True) + + # trim long silence + if webrtcvad: + wav = trim_long_silences( + wav, self.vad_window_length, self.vad_moving_average_width, + self.vad_max_silence_length, self.sampling_rate) + return wav + + def melspectrogram(self, wav): + mel = librosa.feature.melspectrogram( + y=wav, + sr=self.sampling_rate, + n_fft=self.n_fft, + hop_length=self.hop_length, + n_mels=self.n_mels) + mel = mel.astype(np.float32).T + return mel + + def extract_mel_partials(self, wav): + wav_slices, mel_slices = compute_partial_slices( + len(wav), self.partial_n_frames, self.hop_length, + self.min_pad_coverage, self.partial_overlap_ratio) + + # pad audio if needed + max_wave_length = wav_slices[-1].stop + if max_wave_length >= len(wav): + wav = np.pad(wav, (0, max_wave_length - len(wav)), "constant") + + # Split the utterance into partials + frames = self.melspectrogram(wav) + frames_batch = np.array([frames[s] for s in mel_slices]) + return frames_batch # [B, T, C] diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/config.py b/ernie-sat/paddlespeech/vector/exps/ge2e/config.py new file mode 100644 index 0000000..3e11429 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/config.py @@ -0,0 +1,61 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from yacs.config import CfgNode + +_C = CfgNode() + +data_config = _C.data = CfgNode() + +## Audio volume normalization +data_config.audio_norm_target_dBFS = -30 + +## Audio sample rate +data_config.sampling_rate = 16000 # Hz + +## Voice Activation Detection +# Window size of the VAD. Must be either 10, 20 or 30 milliseconds. +# This sets the granularity of the VAD. Should not need to be changed. +data_config.vad_window_length = 30 # In milliseconds +# Number of frames to average together when performing the moving average smoothing. +# The larger this value, the larger the VAD variations must be to not get smoothed out. +data_config.vad_moving_average_width = 8 +# Maximum number of consecutive silent frames a segment can have. +data_config.vad_max_silence_length = 6 + +## Mel-filterbank +data_config.mel_window_length = 25 # In milliseconds +data_config.mel_window_step = 10 # In milliseconds +data_config.n_mels = 40 # mel bands + +# Number of spectrogram frames in a partial utterance +data_config.partial_n_frames = 160 # 1600 ms +data_config.min_pad_coverage = 0.75 # at least 75% of the audio is valid in a partial +data_config.partial_overlap_ratio = 0.5 # overlap ratio between ajancent partials + +model_config = _C.model = CfgNode() +model_config.num_layers = 3 +model_config.hidden_size = 256 +model_config.embedding_size = 256 # output size + +training_config = _C.training = CfgNode() +training_config.learning_rate_init = 1e-4 +training_config.speakers_per_batch = 64 +training_config.utterances_per_speaker = 10 +training_config.max_iteration = 1560000 +training_config.save_interval = 10000 +training_config.valid_interval = 10000 + + +def get_cfg_defaults(): + return _C.clone() diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/dataset_processors.py b/ernie-sat/paddlespeech/vector/exps/ge2e/dataset_processors.py new file mode 100644 index 0000000..908c852 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/dataset_processors.py @@ -0,0 +1,173 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import multiprocessing as mp +from functools import partial +from pathlib import Path +from typing import List + +import numpy as np +from tqdm import tqdm + +from paddlespeech.vector.exps.ge2e.audio_processor import SpeakerVerificationPreprocessor + + +def _process_utterance(path_pair, processor: SpeakerVerificationPreprocessor): + # Load and preprocess the waveform + input_path, output_path = path_pair + wav = processor.preprocess_wav(input_path) + if len(wav) == 0: + return + + # Create the mel spectrogram, discard those that are too short + frames = processor.melspectrogram(wav) + if len(frames) < processor.partial_n_frames: + return + + np.save(output_path, frames) + + +def _process_speaker(speaker_dir: Path, + processor: SpeakerVerificationPreprocessor, + datasets_root: Path, + output_dir: Path, + pattern: str, + skip_existing: bool=False): + # datastes root: a reference path to compute speaker_name + # we prepand dataset name to speaker_id becase we are mixing serveal + # multispeaker datasets together + speaker_name = "_".join(speaker_dir.relative_to(datasets_root).parts) + speaker_output_dir = output_dir / speaker_name + speaker_output_dir.mkdir(parents=True, exist_ok=True) + + # load exsiting file set + sources_fpath = speaker_output_dir / "_sources.txt" + if sources_fpath.exists(): + try: + with sources_fpath.open("rt") as sources_file: + existing_names = {line.split(",")[0] for line in sources_file} + except Exception as e: + existing_names = {} + else: + existing_names = {} + + sources_file = sources_fpath.open("at" if skip_existing else "wt") + for in_fpath in speaker_dir.rglob(pattern): + out_name = "_".join( + in_fpath.relative_to(speaker_dir).with_suffix(".npy").parts) + if skip_existing and out_name in existing_names: + continue + out_fpath = speaker_output_dir / out_name + _process_utterance((in_fpath, out_fpath), processor) + sources_file.write(f"{out_name},{in_fpath}\n") + + sources_file.close() + + +def _process_dataset(processor: SpeakerVerificationPreprocessor, + datasets_root: Path, + speaker_dirs: List[Path], + dataset_name: str, + output_dir: Path, + pattern: str, + skip_existing: bool=False): + print( + f"{dataset_name}: Preprocessing data for {len(speaker_dirs)} speakers.") + + _func = partial( + _process_speaker, + processor=processor, + datasets_root=datasets_root, + output_dir=output_dir, + pattern=pattern, + skip_existing=skip_existing) + + with mp.Pool(16) as pool: + list( + tqdm( + pool.imap(_func, speaker_dirs), + dataset_name, + len(speaker_dirs), + unit="speakers")) + print(f"Done preprocessing {dataset_name}.") + + +def process_librispeech(processor, + datasets_root, + output_dir, + skip_existing=False): + dataset_name = "LibriSpeech/train-other-500" + dataset_root = datasets_root / dataset_name + speaker_dirs = list(dataset_root.glob("*")) + _process_dataset(processor, datasets_root, speaker_dirs, dataset_name, + output_dir, "*.flac", skip_existing) + + +def process_voxceleb1(processor, datasets_root, output_dir, + skip_existing=False): + dataset_name = "VoxCeleb1" + dataset_root = datasets_root / dataset_name + + anglophone_nationalites = ["australia", "canada", "ireland", "uk", "usa"] + with dataset_root.joinpath("vox1_meta.csv").open("rt") as metafile: + metadata = [line.strip().split("\t") for line in metafile][1:] + + # speaker id -> nationality + nationalities = {line[0]: line[3] for line in metadata if line[-1] == "dev"} + keep_speaker_ids = [ + speaker_id for speaker_id, nationality in nationalities.items() + if nationality.lower() in anglophone_nationalites + ] + print( + "VoxCeleb1: using samples from {} (presumed anglophone) speakers out of {}." + .format(len(keep_speaker_ids), len(nationalities))) + + speaker_dirs = list((dataset_root / "wav").glob("*")) + speaker_dirs = [ + speaker_dir for speaker_dir in speaker_dirs + if speaker_dir.name in keep_speaker_ids + ] + _process_dataset(processor, datasets_root, speaker_dirs, dataset_name, + output_dir, "*.wav", skip_existing) + + +def process_voxceleb2(processor, datasets_root, output_dir, + skip_existing=False): + dataset_name = "VoxCeleb2" + dataset_root = datasets_root / dataset_name + # There is no nationality in meta data for VoxCeleb2 + speaker_dirs = list((dataset_root / "wav").glob("*")) + _process_dataset(processor, datasets_root, speaker_dirs, dataset_name, + output_dir, "*.wav", skip_existing) + + +def process_aidatatang_200zh(processor, + datasets_root, + output_dir, + skip_existing=False): + dataset_name = "aidatatang_200zh/train" + dataset_root = datasets_root / dataset_name + + speaker_dirs = list((dataset_root).glob("*")) + _process_dataset(processor, datasets_root, speaker_dirs, dataset_name, + output_dir, "*.wav", skip_existing) + + +def process_magicdata(processor, datasets_root, output_dir, + skip_existing=False): + dataset_name = "magicdata/train" + dataset_root = datasets_root / dataset_name + + speaker_dirs = list((dataset_root).glob("*")) + _process_dataset(processor, datasets_root, speaker_dirs, dataset_name, + output_dir, "*.wav", skip_existing) diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/inference.py b/ernie-sat/paddlespeech/vector/exps/ge2e/inference.py new file mode 100644 index 0000000..7660de5 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/inference.py @@ -0,0 +1,140 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +import numpy as np +import paddle +import tqdm + +from paddlespeech.vector.exps.ge2e.audio_processor import SpeakerVerificationPreprocessor +from paddlespeech.vector.exps.ge2e.config import get_cfg_defaults +from paddlespeech.vector.models.lstm_speaker_encoder import LSTMSpeakerEncoder + + +def embed_utterance(processor, model, fpath_or_wav): + # audio processor + wav = processor.preprocess_wav(fpath_or_wav) + mel_partials = processor.extract_mel_partials(wav) + + model.eval() + # speaker encoder + with paddle.no_grad(): + mel_partials = paddle.to_tensor(mel_partials) + with paddle.no_grad(): + embed = model.embed_utterance(mel_partials) + embed = embed.numpy() + return embed + + +def _process_utterance(ifpath: Path, + input_dir: Path, + output_dir: Path, + processor: SpeakerVerificationPreprocessor, + model: LSTMSpeakerEncoder): + rel_path = ifpath.relative_to(input_dir) + ofpath = (output_dir / rel_path).with_suffix(".npy") + ofpath.parent.mkdir(parents=True, exist_ok=True) + embed = embed_utterance(processor, model, ifpath) + np.save(ofpath, embed) + + +def main(config, args): + + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + # load model + model = LSTMSpeakerEncoder(config.data.n_mels, config.model.num_layers, + config.model.hidden_size, + config.model.embedding_size) + weights_fpath = str(Path(args.checkpoint_path).expanduser()) + model_state_dict = paddle.load(weights_fpath + ".pdparams") + model.set_state_dict(model_state_dict) + model.eval() + print(f"Loaded encoder {weights_fpath}") + + # create audio processor + c = config.data + processor = SpeakerVerificationPreprocessor( + sampling_rate=c.sampling_rate, + audio_norm_target_dBFS=c.audio_norm_target_dBFS, + vad_window_length=c.vad_window_length, + vad_moving_average_width=c.vad_moving_average_width, + vad_max_silence_length=c.vad_max_silence_length, + mel_window_length=c.mel_window_length, + mel_window_step=c.mel_window_step, + n_mels=c.n_mels, + partial_n_frames=c.partial_n_frames, + min_pad_coverage=c.min_pad_coverage, + partial_overlap_ratio=c.min_pad_coverage, ) + + # input output preparation + input_dir = Path(args.input).expanduser() + ifpaths = list(input_dir.rglob(args.pattern)) + print(f"{len(ifpaths)} utterances in total") + output_dir = Path(args.output).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + + for ifpath in tqdm.tqdm(ifpaths, unit="utterance"): + _process_utterance(ifpath, input_dir, output_dir, processor, model) + + +if __name__ == "__main__": + config = get_cfg_defaults() + parser = argparse.ArgumentParser(description="compute utterance embed.") + parser.add_argument( + "--config", + metavar="FILE", + help="path of the config file to overwrite to default config with.") + parser.add_argument( + "--input", type=str, help="path of the audio_file folder.") + parser.add_argument( + "--pattern", + type=str, + default="*.wav", + help="pattern to filter audio files.") + parser.add_argument( + "--output", + metavar="OUTPUT_DIR", + help="path to save checkpoint and logs.") + + # load from saved checkpoint + parser.add_argument( + "--checkpoint_path", type=str, help="path of the checkpoint to load") + + # overwrite extra config and default config + parser.add_argument( + "--opts", + nargs=argparse.REMAINDER, + help="options to overwrite --config file and the default config, passing in KEY VALUE pairs" + ) + + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu=0, use cpu.") + + args = parser.parse_args() + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + print(args) + + main(config, args) diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/preprocess.py b/ernie-sat/paddlespeech/vector/exps/ge2e/preprocess.py new file mode 100644 index 0000000..dabe0ce --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/preprocess.py @@ -0,0 +1,103 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import argparse +from pathlib import Path + +from paddlespeech.vector.exps.ge2e.audio_processor import SpeakerVerificationPreprocessor +from paddlespeech.vector.exps.ge2e.config import get_cfg_defaults +from paddlespeech.vector.exps.ge2e.dataset_processors import process_aidatatang_200zh +from paddlespeech.vector.exps.ge2e.dataset_processors import process_librispeech +from paddlespeech.vector.exps.ge2e.dataset_processors import process_magicdata +from paddlespeech.vector.exps.ge2e.dataset_processors import process_voxceleb1 +from paddlespeech.vector.exps.ge2e.dataset_processors import process_voxceleb2 + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="preprocess dataset for speaker verification task") + parser.add_argument( + "--datasets_root", + type=Path, + help="Path to the directory containing your LibriSpeech, LibriTTS and VoxCeleb datasets." + ) + parser.add_argument( + "--output_dir", type=Path, help="Path to save processed dataset.") + parser.add_argument( + "--dataset_names", + type=str, + default="librispeech_other,voxceleb1,voxceleb2", + help="comma-separated list of names of the datasets you want to preprocess. only " + "the train set of these datastes will be used. Possible names: librispeech_other, " + "voxceleb1, voxceleb2, aidatatang_200zh, magicdata.") + parser.add_argument( + "--skip_existing", + action="store_true", + help="Whether to skip ouput files with the same name. Useful if this script was interrupted." + ) + parser.add_argument( + "--no_trim", + action="store_true", + help="Preprocess audio without trimming silences (not recommended).") + + args = parser.parse_args() + + if not args.no_trim: + try: + import webrtcvad + print(webrtcvad.__version__) + except Exception as e: + raise ModuleNotFoundError( + "Package 'webrtcvad' not found. This package enables " + "noise removal and is recommended. Please install and " + "try again. If installation fails, " + "use --no_trim to disable this error message.") + del args.no_trim + + args.datasets = [item.strip() for item in args.dataset_names.split(",")] + if not hasattr(args, "output_dir"): + args.output_dir = args.dataset_root / "SV2TTS" / "encoder" + + args.output_dir = args.output_dir.expanduser() + args.datasets_root = args.datasets_root.expanduser() + assert args.datasets_root.exists() + args.output_dir.mkdir(exist_ok=True, parents=True) + + config = get_cfg_defaults() + print(args) + + c = config.data + processor = SpeakerVerificationPreprocessor( + sampling_rate=c.sampling_rate, + audio_norm_target_dBFS=c.audio_norm_target_dBFS, + vad_window_length=c.vad_window_length, + vad_moving_average_width=c.vad_moving_average_width, + vad_max_silence_length=c.vad_max_silence_length, + mel_window_length=c.mel_window_length, + mel_window_step=c.mel_window_step, + n_mels=c.n_mels, + partial_n_frames=c.partial_n_frames, + min_pad_coverage=c.min_pad_coverage, + partial_overlap_ratio=c.min_pad_coverage, ) + + preprocess_func = { + "librispeech_other": process_librispeech, + "voxceleb1": process_voxceleb1, + "voxceleb2": process_voxceleb2, + "aidatatang_200zh": process_aidatatang_200zh, + "magicdata": process_magicdata, + } + + for dataset in args.datasets: + print("Preprocessing %s" % dataset) + preprocess_func[dataset](processor, args.datasets_root, args.output_dir, + args.skip_existing) diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/random_cycle.py b/ernie-sat/paddlespeech/vector/exps/ge2e/random_cycle.py new file mode 100644 index 0000000..290fd2f --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/random_cycle.py @@ -0,0 +1,38 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random + + +def cycle(iterable): + # cycle('ABCD') --> A B C D A B C D A B C D ... + saved = [] + for element in iterable: + yield element + saved.append(element) + while saved: + for element in saved: + yield element + + +def random_cycle(iterable): + # cycle('ABCD') --> A B C D B C D A A D B C ... + saved = [] + for element in iterable: + yield element + saved.append(element) + random.shuffle(saved) + while saved: + for element in saved: + yield element + random.shuffle(saved) diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/speaker_verification_dataset.py b/ernie-sat/paddlespeech/vector/exps/ge2e/speaker_verification_dataset.py new file mode 100644 index 0000000..ae6f6ad --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/speaker_verification_dataset.py @@ -0,0 +1,125 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import random +from pathlib import Path + +import numpy as np +from paddle.io import BatchSampler +from paddle.io import Dataset + +from paddlespeech.vector.exps.ge2e.random_cycle import random_cycle + + +class MultiSpeakerMelDataset(Dataset): + """A 2 layer directory thatn contains mel spectrograms in *.npy format. + An Example file structure tree is shown below. We prefer to preprocess + raw datasets and organized them like this. + + dataset_root/ + speaker1/ + utterance1.npy + utterance2.npy + utterance3.npy + speaker2/ + utterance1.npy + utterance2.npy + utterance3.npy + """ + + def __init__(self, dataset_root: Path): + self.root = Path(dataset_root).expanduser() + speaker_dirs = [f for f in self.root.glob("*") if f.is_dir()] + + speaker_utterances = { + speaker_dir: list(speaker_dir.glob("*.npy")) + for speaker_dir in speaker_dirs + } + + self.speaker_dirs = speaker_dirs + self.speaker_to_utterances = speaker_utterances + + # meta data + self.num_speakers = len(self.speaker_dirs) + self.num_utterances = np.sum( + len(utterances) + for speaker, utterances in self.speaker_to_utterances.items()) + + def get_example_by_index(self, speaker_index, utterance_index): + speaker_dir = self.speaker_dirs[speaker_index] + fpath = self.speaker_to_utterances[speaker_dir][utterance_index] + return self[fpath] + + def __getitem__(self, fpath): + return np.load(fpath) + + def __len__(self): + return int(self.num_utterances) + + +class MultiSpeakerSampler(BatchSampler): + """A multi-stratal sampler designed for speaker verification task. + First, N speakers from all speakers are sampled randomly. Then, for each + speaker, randomly sample M utterances from their corresponding utterances. + """ + + def __init__(self, + dataset: MultiSpeakerMelDataset, + speakers_per_batch: int, + utterances_per_speaker: int): + self._speakers = list(dataset.speaker_dirs) + self._speaker_to_utterances = dataset.speaker_to_utterances + + self.speakers_per_batch = speakers_per_batch + self.utterances_per_speaker = utterances_per_speaker + + def __iter__(self): + # yield list of Paths + speaker_generator = iter(random_cycle(self._speakers)) + speaker_utterances_generator = { + s: iter(random_cycle(us)) + for s, us in self._speaker_to_utterances.items() + } + + while True: + speakers = [] + for _ in range(self.speakers_per_batch): + speakers.append(next(speaker_generator)) + + utterances = [] + for s in speakers: + us = speaker_utterances_generator[s] + for _ in range(self.utterances_per_speaker): + utterances.append(next(us)) + yield utterances + + +class RandomClip(object): + def __init__(self, frames): + self.frames = frames + + def __call__(self, spec): + # spec [T, C] + T = spec.shape[0] + start = random.randint(0, T - self.frames) + return spec[start:start + self.frames, :] + + +class Collate(object): + def __init__(self, num_frames): + self.random_crop = RandomClip(num_frames) + + def __call__(self, examples): + frame_clips = [self.random_crop(mel) for mel in examples] + batced_clips = np.stack(frame_clips) + return batced_clips diff --git a/ernie-sat/paddlespeech/vector/exps/ge2e/train.py b/ernie-sat/paddlespeech/vector/exps/ge2e/train.py new file mode 100644 index 0000000..bf1cf10 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/exps/ge2e/train.py @@ -0,0 +1,123 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import time + +from paddle import DataParallel +from paddle import distributed as dist +from paddle.io import DataLoader +from paddle.nn.clip import ClipGradByGlobalNorm +from paddle.optimizer import Adam + +from paddlespeech.t2s.training import default_argument_parser +from paddlespeech.t2s.training import ExperimentBase +from paddlespeech.vector.exps.ge2e.config import get_cfg_defaults +from paddlespeech.vector.exps.ge2e.speaker_verification_dataset import Collate +from paddlespeech.vector.exps.ge2e.speaker_verification_dataset import MultiSpeakerMelDataset +from paddlespeech.vector.exps.ge2e.speaker_verification_dataset import MultiSpeakerSampler +from paddlespeech.vector.models.lstm_speaker_encoder import LSTMSpeakerEncoder + + +class Ge2eExperiment(ExperimentBase): + def setup_model(self): + config = self.config + model = LSTMSpeakerEncoder(config.data.n_mels, config.model.num_layers, + config.model.hidden_size, + config.model.embedding_size) + optimizer = Adam( + config.training.learning_rate_init, + parameters=model.parameters(), + grad_clip=ClipGradByGlobalNorm(3)) + self.model = DataParallel(model) if self.parallel else model + self.model_core = model + self.optimizer = optimizer + + def setup_dataloader(self): + config = self.config + train_dataset = MultiSpeakerMelDataset(self.args.data) + sampler = MultiSpeakerSampler(train_dataset, + config.training.speakers_per_batch, + config.training.utterances_per_speaker) + train_loader = DataLoader( + train_dataset, + batch_sampler=sampler, + collate_fn=Collate(config.data.partial_n_frames), + num_workers=16) + + self.train_dataset = train_dataset + self.train_loader = train_loader + + def train_batch(self): + start = time.time() + batch = self.read_batch() + data_loader_time = time.time() - start + + self.optimizer.clear_grad() + self.model.train() + specs = batch + loss, eer = self.model(specs, self.config.training.speakers_per_batch) + loss.backward() + self.model_core.do_gradient_ops() + self.optimizer.step() + iteration_time = time.time() - start + + # logging + loss_value = float(loss) + msg = "Rank: {}, ".format(dist.get_rank()) + msg += "step: {}, ".format(self.iteration) + msg += "time: {:>.3f}s/{:>.3f}s, ".format(data_loader_time, + iteration_time) + msg += 'loss: {:>.6f} err: {:>.6f}'.format(loss_value, eer) + self.logger.info(msg) + + if dist.get_rank() == 0: + self.visualizer.add_scalar("train/loss", loss_value, self.iteration) + self.visualizer.add_scalar("train/eer", eer, self.iteration) + self.visualizer.add_scalar("param/w", + float(self.model_core.similarity_weight), + self.iteration) + self.visualizer.add_scalar("param/b", + float(self.model_core.similarity_bias), + self.iteration) + + def valid(self): + pass + + +def main_sp(config, args): + exp = Ge2eExperiment(config, args) + exp.setup() + exp.resume_or_load() + exp.run() + + +def main(config, args): + if args.ngpu > 1: + dist.spawn(main_sp, args=(config, args), nprocs=args.ngpu) + else: + main_sp(config, args) + + +if __name__ == "__main__": + config = get_cfg_defaults() + parser = default_argument_parser() + args = parser.parse_args() + if args.config: + config.merge_from_file(args.config) + if args.opts: + config.merge_from_list(args.opts) + config.freeze() + print(config) + print(args) + + main(config, args) diff --git a/ernie-sat/paddlespeech/vector/io/__init__.py b/ernie-sat/paddlespeech/vector/io/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/io/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/io/augment.py b/ernie-sat/paddlespeech/vector/io/augment.py new file mode 100644 index 0000000..3baace1 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/io/augment.py @@ -0,0 +1,906 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# this is modified from SpeechBrain +# https://github.com/speechbrain/speechbrain/blob/085be635c07f16d42cd1295045bc46c407f1e15b/speechbrain/lobes/augment.py +import math +from typing import List + +import numpy as np +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from paddleaudio.datasets.rirs_noises import OpenRIRNoise +from paddlespeech.s2t.utils.log import Log +from paddlespeech.vector.io.signal_processing import compute_amplitude +from paddlespeech.vector.io.signal_processing import convolve1d +from paddlespeech.vector.io.signal_processing import dB_to_amplitude +from paddlespeech.vector.io.signal_processing import notch_filter +from paddlespeech.vector.io.signal_processing import reverberate + +logger = Log(__name__).getlog() + + +# TODO: Complete type-hint and doc string. +class DropFreq(nn.Layer): + def __init__( + self, + drop_freq_low=1e-14, + drop_freq_high=1, + drop_count_low=1, + drop_count_high=2, + drop_width=0.05, + drop_prob=1, ): + super(DropFreq, self).__init__() + self.drop_freq_low = drop_freq_low + self.drop_freq_high = drop_freq_high + self.drop_count_low = drop_count_low + self.drop_count_high = drop_count_high + self.drop_width = drop_width + self.drop_prob = drop_prob + + def forward(self, waveforms): + # Don't drop (return early) 1-`drop_prob` portion of the batches + dropped_waveform = waveforms.clone() + if paddle.rand([1]) > self.drop_prob: + return dropped_waveform + + # Add channels dimension + if len(waveforms.shape) == 2: + dropped_waveform = dropped_waveform.unsqueeze(-1) + + # Pick number of frequencies to drop + drop_count = paddle.randint( + low=self.drop_count_low, high=self.drop_count_high + 1, shape=[1]) + + # Pick a frequency to drop + drop_range = self.drop_freq_high - self.drop_freq_low + drop_frequency = ( + paddle.rand([drop_count]) * drop_range + self.drop_freq_low) + + # Filter parameters + filter_length = 101 + pad = filter_length // 2 + + # Start with delta function + drop_filter = paddle.zeros([1, filter_length, 1]) + drop_filter[0, pad, 0] = 1 + + # Subtract each frequency + for frequency in drop_frequency: + notch_kernel = notch_filter(frequency, filter_length, + self.drop_width) + drop_filter = convolve1d(drop_filter, notch_kernel, pad) + + # Apply filter + dropped_waveform = convolve1d(dropped_waveform, drop_filter, pad) + + # Remove channels dimension if added + return dropped_waveform.squeeze(-1) + + +class DropChunk(nn.Layer): + def __init__( + self, + drop_length_low=100, + drop_length_high=1000, + drop_count_low=1, + drop_count_high=10, + drop_start=0, + drop_end=None, + drop_prob=1, + noise_factor=0.0, ): + super(DropChunk, self).__init__() + self.drop_length_low = drop_length_low + self.drop_length_high = drop_length_high + self.drop_count_low = drop_count_low + self.drop_count_high = drop_count_high + self.drop_start = drop_start + self.drop_end = drop_end + self.drop_prob = drop_prob + self.noise_factor = noise_factor + + # Validate low < high + if drop_length_low > drop_length_high: + raise ValueError("Low limit must not be more than high limit") + if drop_count_low > drop_count_high: + raise ValueError("Low limit must not be more than high limit") + + # Make sure the length doesn't exceed end - start + if drop_end is not None and drop_end >= 0: + if drop_start > drop_end: + raise ValueError("Low limit must not be more than high limit") + + drop_range = drop_end - drop_start + self.drop_length_low = min(drop_length_low, drop_range) + self.drop_length_high = min(drop_length_high, drop_range) + + def forward(self, waveforms, lengths): + # Reading input list + lengths = (lengths * waveforms.shape[1]).astype('int64') + batch_size = waveforms.shape[0] + dropped_waveform = waveforms.clone() + + # Don't drop (return early) 1-`drop_prob` portion of the batches + if paddle.rand([1]) > self.drop_prob: + return dropped_waveform + + # Store original amplitude for computing white noise amplitude + clean_amplitude = compute_amplitude(waveforms, lengths.unsqueeze(1)) + + # Pick a number of times to drop + drop_times = paddle.randint( + low=self.drop_count_low, + high=self.drop_count_high + 1, + shape=[batch_size], ) + + # Iterate batch to set mask + for i in range(batch_size): + if drop_times[i] == 0: + continue + + # Pick lengths + length = paddle.randint( + low=self.drop_length_low, + high=self.drop_length_high + 1, + shape=[drop_times[i]], ) + + # Compute range of starting locations + start_min = self.drop_start + if start_min < 0: + start_min += lengths[i] + start_max = self.drop_end + if start_max is None: + start_max = lengths[i] + if start_max < 0: + start_max += lengths[i] + start_max = max(0, start_max - length.max()) + + # Pick starting locations + start = paddle.randint( + low=start_min, + high=start_max + 1, + shape=[drop_times[i]], ) + + end = start + length + + # Update waveform + if not self.noise_factor: + for j in range(drop_times[i]): + if start[j] < end[j]: + dropped_waveform[i, start[j]:end[j]] = 0.0 + else: + # Uniform distribution of -2 to +2 * avg amplitude should + # preserve the average for normalization + noise_max = 2 * clean_amplitude[i] * self.noise_factor + for j in range(drop_times[i]): + # zero-center the noise distribution + noise_vec = paddle.rand([length[j]], dtype='float32') + + noise_vec = 2 * noise_max * noise_vec - noise_max + dropped_waveform[i, int(start[j]):int(end[j])] = noise_vec + + return dropped_waveform + + +class Resample(nn.Layer): + def __init__( + self, + orig_freq=16000, + new_freq=16000, + lowpass_filter_width=6, ): + super(Resample, self).__init__() + self.orig_freq = orig_freq + self.new_freq = new_freq + self.lowpass_filter_width = lowpass_filter_width + + # Compute rate for striding + self._compute_strides() + assert self.orig_freq % self.conv_stride == 0 + assert self.new_freq % self.conv_transpose_stride == 0 + + def _compute_strides(self): + # Compute new unit based on ratio of in/out frequencies + base_freq = math.gcd(self.orig_freq, self.new_freq) + input_samples_in_unit = self.orig_freq // base_freq + self.output_samples = self.new_freq // base_freq + + # Store the appropriate stride based on the new units + self.conv_stride = input_samples_in_unit + self.conv_transpose_stride = self.output_samples + + def forward(self, waveforms): + if not hasattr(self, "first_indices"): + self._indices_and_weights(waveforms) + + # Don't do anything if the frequencies are the same + if self.orig_freq == self.new_freq: + return waveforms + + unsqueezed = False + if len(waveforms.shape) == 2: + waveforms = waveforms.unsqueeze(1) + unsqueezed = True + elif len(waveforms.shape) == 3: + waveforms = waveforms.transpose([0, 2, 1]) + else: + raise ValueError("Input must be 2 or 3 dimensions") + + # Do resampling + resampled_waveform = self._perform_resample(waveforms) + + if unsqueezed: + resampled_waveform = resampled_waveform.squeeze(1) + else: + resampled_waveform = resampled_waveform.transpose([0, 2, 1]) + + return resampled_waveform + + def _perform_resample(self, waveforms): + # Compute output size and initialize + batch_size, num_channels, wave_len = waveforms.shape + window_size = self.weights.shape[1] + tot_output_samp = self._output_samples(wave_len) + resampled_waveform = paddle.zeros((batch_size, num_channels, + tot_output_samp)) + + # eye size: (num_channels, num_channels, 1) + eye = paddle.eye(num_channels).unsqueeze(2) + + # Iterate over the phases in the polyphase filter + for i in range(self.first_indices.shape[0]): + wave_to_conv = waveforms + first_index = int(self.first_indices[i].item()) + if first_index >= 0: + # trim the signal as the filter will not be applied + # before the first_index + wave_to_conv = wave_to_conv[:, :, first_index:] + + # pad the right of the signal to allow partial convolutions + # meaning compute values for partial windows (e.g. end of the + # window is outside the signal length) + max_index = (tot_output_samp - 1) // self.output_samples + end_index = max_index * self.conv_stride + window_size + current_wave_len = wave_len - first_index + right_padding = max(0, end_index + 1 - current_wave_len) + left_padding = max(0, -first_index) + wave_to_conv = paddle.nn.functional.pad( + wave_to_conv, [left_padding, right_padding], data_format='NCL') + conv_wave = paddle.nn.functional.conv1d( + x=wave_to_conv, + # weight=self.weights[i].repeat(num_channels, 1, 1), + weight=self.weights[i].expand((num_channels, 1, -1)), + stride=self.conv_stride, + groups=num_channels, ) + + # we want conv_wave[:, i] to be at + # output[:, i + n*conv_transpose_stride] + dilated_conv_wave = paddle.nn.functional.conv1d_transpose( + conv_wave, eye, stride=self.conv_transpose_stride) + + # pad dilated_conv_wave so it reaches the output length if needed. + left_padding = i + previous_padding = left_padding + dilated_conv_wave.shape[-1] + right_padding = max(0, tot_output_samp - previous_padding) + dilated_conv_wave = paddle.nn.functional.pad( + dilated_conv_wave, [left_padding, right_padding], + data_format='NCL') + dilated_conv_wave = dilated_conv_wave[:, :, :tot_output_samp] + + resampled_waveform += dilated_conv_wave + + return resampled_waveform + + def _output_samples(self, input_num_samp): + samp_in = int(self.orig_freq) + samp_out = int(self.new_freq) + + tick_freq = abs(samp_in * samp_out) // math.gcd(samp_in, samp_out) + ticks_per_input_period = tick_freq // samp_in + + # work out the number of ticks in the time interval + # [ 0, input_num_samp/samp_in ). + interval_length = input_num_samp * ticks_per_input_period + if interval_length <= 0: + return 0 + ticks_per_output_period = tick_freq // samp_out + + # Get the last output-sample in the closed interval, + # i.e. replacing [ ) with [ ]. Note: integer division rounds down. + # See http://en.wikipedia.org/wiki/Interval_(mathematics) for an + # explanation of the notation. + last_output_samp = interval_length // ticks_per_output_period + + # We need the last output-sample in the open interval, so if it + # takes us to the end of the interval exactly, subtract one. + if last_output_samp * ticks_per_output_period == interval_length: + last_output_samp -= 1 + + # First output-sample index is zero, so the number of output samples + # is the last output-sample plus one. + num_output_samp = last_output_samp + 1 + + return num_output_samp + + def _indices_and_weights(self, waveforms): + # Lowpass filter frequency depends on smaller of two frequencies + min_freq = min(self.orig_freq, self.new_freq) + lowpass_cutoff = 0.99 * 0.5 * min_freq + + assert lowpass_cutoff * 2 <= min_freq + window_width = self.lowpass_filter_width / (2.0 * lowpass_cutoff) + + assert lowpass_cutoff < min(self.orig_freq, self.new_freq) / 2 + output_t = paddle.arange(start=0.0, end=self.output_samples) + output_t /= self.new_freq + min_t = output_t - window_width + max_t = output_t + window_width + + min_input_index = paddle.ceil(min_t * self.orig_freq) + max_input_index = paddle.floor(max_t * self.orig_freq) + num_indices = max_input_index - min_input_index + 1 + + max_weight_width = num_indices.max() + j = paddle.arange(max_weight_width, dtype='float32') + input_index = min_input_index.unsqueeze(1) + j.unsqueeze(0) + delta_t = (input_index / self.orig_freq) - output_t.unsqueeze(1) + + weights = paddle.zeros_like(delta_t) + inside_window_indices = delta_t.abs().less_than( + paddle.to_tensor(window_width)) + + # raised-cosine (Hanning) window with width `window_width` + weights[inside_window_indices] = 0.5 * (1 + paddle.cos( + 2 * math.pi * lowpass_cutoff / self.lowpass_filter_width * + delta_t.masked_select(inside_window_indices))) + + t_eq_zero_indices = delta_t.equal(paddle.zeros_like(delta_t)) + t_not_eq_zero_indices = delta_t.not_equal(paddle.zeros_like(delta_t)) + + # sinc filter function + weights = paddle.where( + t_not_eq_zero_indices, + weights * paddle.sin(2 * math.pi * lowpass_cutoff * delta_t) / + (math.pi * delta_t), weights) + + # limit of the function at t = 0 + weights = paddle.where(t_eq_zero_indices, weights * 2 * lowpass_cutoff, + weights) + + # size (output_samples, max_weight_width) + weights /= self.orig_freq + + self.first_indices = min_input_index + self.weights = weights + + +class SpeedPerturb(nn.Layer): + def __init__( + self, + orig_freq, + speeds=[90, 100, 110], + perturb_prob=1.0, ): + super(SpeedPerturb, self).__init__() + self.orig_freq = orig_freq + self.speeds = speeds + self.perturb_prob = perturb_prob + + # Initialize index of perturbation + self.samp_index = 0 + + # Initialize resamplers + self.resamplers = [] + for speed in self.speeds: + config = { + "orig_freq": self.orig_freq, + "new_freq": self.orig_freq * speed // 100, + } + self.resamplers.append(Resample(**config)) + + def forward(self, waveform): + # Don't perturb (return early) 1-`perturb_prob` portion of the batches + if paddle.rand([1]) > self.perturb_prob: + return waveform.clone() + + # Perform a random perturbation + self.samp_index = paddle.randint(len(self.speeds), shape=[1]).item() + perturbed_waveform = self.resamplers[self.samp_index](waveform) + + return perturbed_waveform + + +class AddNoise(nn.Layer): + def __init__( + self, + noise_dataset=None, # None for white noise + num_workers=0, + snr_low=0, + snr_high=0, + mix_prob=1.0, + start_index=None, + normalize=False, ): + super(AddNoise, self).__init__() + + self.num_workers = num_workers + self.snr_low = snr_low + self.snr_high = snr_high + self.mix_prob = mix_prob + self.start_index = start_index + self.normalize = normalize + self.noise_dataset = noise_dataset + self.noise_dataloader = None + + def forward(self, waveforms, lengths=None): + if lengths is None: + lengths = paddle.ones([len(waveforms)]) + + # Copy clean waveform to initialize noisy waveform + noisy_waveform = waveforms.clone() + lengths = (lengths * waveforms.shape[1]).astype('int64').unsqueeze(1) + + # Don't add noise (return early) 1-`mix_prob` portion of the batches + if paddle.rand([1]) > self.mix_prob: + return noisy_waveform + + # Compute the average amplitude of the clean waveforms + clean_amplitude = compute_amplitude(waveforms, lengths) + + # Pick an SNR and use it to compute the mixture amplitude factors + SNR = paddle.rand((len(waveforms), 1)) + SNR = SNR * (self.snr_high - self.snr_low) + self.snr_low + noise_amplitude_factor = 1 / (dB_to_amplitude(SNR) + 1) + new_noise_amplitude = noise_amplitude_factor * clean_amplitude + + # Scale clean signal appropriately + noisy_waveform *= 1 - noise_amplitude_factor + + # Loop through clean samples and create mixture + if self.noise_dataset is None: + white_noise = paddle.normal(shape=waveforms.shape) + noisy_waveform += new_noise_amplitude * white_noise + else: + tensor_length = waveforms.shape[1] + noise_waveform, noise_length = self._load_noise( + lengths, + tensor_length, ) + + # Rescale and add + noise_amplitude = compute_amplitude(noise_waveform, noise_length) + noise_waveform *= new_noise_amplitude / (noise_amplitude + 1e-14) + noisy_waveform += noise_waveform + + # Normalizing to prevent clipping + if self.normalize: + abs_max, _ = paddle.max( + paddle.abs(noisy_waveform), axis=1, keepdim=True) + noisy_waveform = noisy_waveform / abs_max.clip(min=1.0) + + return noisy_waveform + + def _load_noise(self, lengths, max_length): + """ + Load a batch of noises + + args + lengths(Paddle.Tensor): Num samples of waveforms with shape (N, 1). + max_length(int): Width of a batch. + """ + lengths = lengths.squeeze(1) + batch_size = len(lengths) + + # Load a noise batch + if self.noise_dataloader is None: + + def noise_collate_fn(batch): + def pad(x, target_length, mode='constant', **kwargs): + x = np.asarray(x) + w = target_length - x.shape[0] + assert w >= 0, f'Target length {target_length} is less than origin length {x.shape[0]}' + return np.pad(x, [0, w], mode=mode, **kwargs) + + ids = [item['id'] for item in batch] + lengths = np.asarray([item['feat'].shape[0] for item in batch]) + waveforms = list( + map(lambda x: pad(x, max(max_length, lengths.max().item())), + [item['feat'] for item in batch])) + waveforms = np.stack(waveforms) + return {'ids': ids, 'feats': waveforms, 'lengths': lengths} + + # Create noise data loader. + self.noise_dataloader = paddle.io.DataLoader( + self.noise_dataset, + batch_size=batch_size, + shuffle=True, + num_workers=self.num_workers, + collate_fn=noise_collate_fn, + return_list=True, ) + self.noise_data = iter(self.noise_dataloader) + + noise_batch, noise_len = self._load_noise_batch_of_size(batch_size) + + # Select a random starting location in the waveform + start_index = self.start_index + if self.start_index is None: + start_index = 0 + max_chop = (noise_len - lengths).min().clip(min=1) + start_index = paddle.randint(high=max_chop, shape=[1]) + + # Truncate noise_batch to max_length + noise_batch = noise_batch[:, start_index:start_index + max_length] + noise_len = (noise_len - start_index).clip(max=max_length).unsqueeze(1) + return noise_batch, noise_len + + def _load_noise_batch_of_size(self, batch_size): + """Concatenate noise batches, then chop to correct size""" + noise_batch, noise_lens = self._load_noise_batch() + + # Expand + while len(noise_batch) < batch_size: + noise_batch = paddle.concat((noise_batch, noise_batch)) + noise_lens = paddle.concat((noise_lens, noise_lens)) + + # Contract + if len(noise_batch) > batch_size: + noise_batch = noise_batch[:batch_size] + noise_lens = noise_lens[:batch_size] + + return noise_batch, noise_lens + + def _load_noise_batch(self): + """Load a batch of noises, restarting iteration if necessary.""" + try: + batch = next(self.noise_data) + except StopIteration: + self.noise_data = iter(self.noise_dataloader) + batch = next(self.noise_data) + + noises, lens = batch['feats'], batch['lengths'] + return noises, lens + + +class AddReverb(nn.Layer): + def __init__( + self, + rir_dataset, + reverb_prob=1.0, + rir_scale_factor=1.0, + num_workers=0, ): + super(AddReverb, self).__init__() + self.rir_dataset = rir_dataset + self.reverb_prob = reverb_prob + self.rir_scale_factor = rir_scale_factor + + # Create rir data loader. + def rir_collate_fn(batch): + def pad(x, target_length, mode='constant', **kwargs): + x = np.asarray(x) + w = target_length - x.shape[0] + assert w >= 0, f'Target length {target_length} is less than origin length {x.shape[0]}' + return np.pad(x, [0, w], mode=mode, **kwargs) + + ids = [item['id'] for item in batch] + lengths = np.asarray([item['feat'].shape[0] for item in batch]) + waveforms = list( + map(lambda x: pad(x, lengths.max().item()), + [item['feat'] for item in batch])) + waveforms = np.stack(waveforms) + return {'ids': ids, 'feats': waveforms, 'lengths': lengths} + + self.rir_dataloader = paddle.io.DataLoader( + self.rir_dataset, + collate_fn=rir_collate_fn, + num_workers=num_workers, + shuffle=True, + return_list=True, ) + + self.rir_data = iter(self.rir_dataloader) + + def forward(self, waveforms, lengths=None): + """ + Arguments + --------- + waveforms : tensor + Shape should be `[batch, time]` or `[batch, time, channels]`. + lengths : tensor + Shape should be a single dimension, `[batch]`. + + Returns + ------- + Tensor of shape `[batch, time]` or `[batch, time, channels]`. + """ + + if lengths is None: + lengths = paddle.ones([len(waveforms)]) + + # Don't add reverb (return early) 1-`reverb_prob` portion of the time + if paddle.rand([1]) > self.reverb_prob: + return waveforms.clone() + + # Add channels dimension if necessary + channel_added = False + if len(waveforms.shape) == 2: + waveforms = waveforms.unsqueeze(-1) + channel_added = True + + # Load and prepare RIR + rir_waveform = self._load_rir() + + # Compress or dilate RIR + if self.rir_scale_factor != 1: + rir_waveform = F.interpolate( + rir_waveform.transpose([0, 2, 1]), + scale_factor=self.rir_scale_factor, + mode="linear", + align_corners=False, + data_format='NCW', ) + # (N, C, L) -> (N, L, C) + rir_waveform = rir_waveform.transpose([0, 2, 1]) + + rev_waveform = reverberate( + waveforms, + rir_waveform, + self.rir_dataset.sample_rate, + rescale_amp="avg") + + # Remove channels dimension if added + if channel_added: + return rev_waveform.squeeze(-1) + + return rev_waveform + + def _load_rir(self): + try: + batch = next(self.rir_data) + except StopIteration: + self.rir_data = iter(self.rir_dataloader) + batch = next(self.rir_data) + + rir_waveform = batch['feats'] + + # Make sure RIR has correct channels + if len(rir_waveform.shape) == 2: + rir_waveform = rir_waveform.unsqueeze(-1) + + return rir_waveform + + +class AddBabble(nn.Layer): + def __init__( + self, + speaker_count=3, + snr_low=0, + snr_high=0, + mix_prob=1, ): + super(AddBabble, self).__init__() + self.speaker_count = speaker_count + self.snr_low = snr_low + self.snr_high = snr_high + self.mix_prob = mix_prob + + def forward(self, waveforms, lengths=None): + if lengths is None: + lengths = paddle.ones([len(waveforms)]) + + babbled_waveform = waveforms.clone() + lengths = (lengths * waveforms.shape[1]).unsqueeze(1) + batch_size = len(waveforms) + + # Don't mix (return early) 1-`mix_prob` portion of the batches + if paddle.rand([1]) > self.mix_prob: + return babbled_waveform + + # Pick an SNR and use it to compute the mixture amplitude factors + clean_amplitude = compute_amplitude(waveforms, lengths) + SNR = paddle.rand((batch_size, 1)) + SNR = SNR * (self.snr_high - self.snr_low) + self.snr_low + noise_amplitude_factor = 1 / (dB_to_amplitude(SNR) + 1) + new_noise_amplitude = noise_amplitude_factor * clean_amplitude + + # Scale clean signal appropriately + babbled_waveform *= 1 - noise_amplitude_factor + + # For each speaker in the mixture, roll and add + babble_waveform = waveforms.roll((1, ), axis=0) + babble_len = lengths.roll((1, ), axis=0) + for i in range(1, self.speaker_count): + babble_waveform += waveforms.roll((1 + i, ), axis=0) + babble_len = paddle.concat( + [babble_len, babble_len.roll((1, ), axis=0)], axis=-1).max( + axis=-1, keepdim=True) + + # Rescale and add to mixture + babble_amplitude = compute_amplitude(babble_waveform, babble_len) + babble_waveform *= new_noise_amplitude / (babble_amplitude + 1e-14) + babbled_waveform += babble_waveform + + return babbled_waveform + + +class TimeDomainSpecAugment(nn.Layer): + def __init__( + self, + perturb_prob=1.0, + drop_freq_prob=1.0, + drop_chunk_prob=1.0, + speeds=[95, 100, 105], + sample_rate=16000, + drop_freq_count_low=0, + drop_freq_count_high=3, + drop_chunk_count_low=0, + drop_chunk_count_high=5, + drop_chunk_length_low=1000, + drop_chunk_length_high=2000, + drop_chunk_noise_factor=0, ): + super(TimeDomainSpecAugment, self).__init__() + self.speed_perturb = SpeedPerturb( + perturb_prob=perturb_prob, + orig_freq=sample_rate, + speeds=speeds, ) + self.drop_freq = DropFreq( + drop_prob=drop_freq_prob, + drop_count_low=drop_freq_count_low, + drop_count_high=drop_freq_count_high, ) + self.drop_chunk = DropChunk( + drop_prob=drop_chunk_prob, + drop_count_low=drop_chunk_count_low, + drop_count_high=drop_chunk_count_high, + drop_length_low=drop_chunk_length_low, + drop_length_high=drop_chunk_length_high, + noise_factor=drop_chunk_noise_factor, ) + + def forward(self, waveforms, lengths=None): + if lengths is None: + lengths = paddle.ones([len(waveforms)]) + + with paddle.no_grad(): + # Augmentation + waveforms = self.speed_perturb(waveforms) + waveforms = self.drop_freq(waveforms) + waveforms = self.drop_chunk(waveforms, lengths) + + return waveforms + + +class EnvCorrupt(nn.Layer): + def __init__( + self, + reverb_prob=1.0, + babble_prob=1.0, + noise_prob=1.0, + rir_dataset=None, + noise_dataset=None, + num_workers=0, + babble_speaker_count=0, + babble_snr_low=0, + babble_snr_high=0, + noise_snr_low=0, + noise_snr_high=0, + rir_scale_factor=1.0, ): + super(EnvCorrupt, self).__init__() + + # Initialize corrupters + if rir_dataset is not None and reverb_prob > 0.0: + self.add_reverb = AddReverb( + rir_dataset=rir_dataset, + num_workers=num_workers, + reverb_prob=reverb_prob, + rir_scale_factor=rir_scale_factor, ) + + if babble_speaker_count > 0 and babble_prob > 0.0: + self.add_babble = AddBabble( + speaker_count=babble_speaker_count, + snr_low=babble_snr_low, + snr_high=babble_snr_high, + mix_prob=babble_prob, ) + + if noise_dataset is not None and noise_prob > 0.0: + self.add_noise = AddNoise( + noise_dataset=noise_dataset, + num_workers=num_workers, + snr_low=noise_snr_low, + snr_high=noise_snr_high, + mix_prob=noise_prob, ) + + def forward(self, waveforms, lengths=None): + if lengths is None: + lengths = paddle.ones([len(waveforms)]) + + # Augmentation + with paddle.no_grad(): + if hasattr(self, "add_reverb"): + try: + waveforms = self.add_reverb(waveforms, lengths) + except Exception: + pass + if hasattr(self, "add_babble"): + waveforms = self.add_babble(waveforms, lengths) + if hasattr(self, "add_noise"): + waveforms = self.add_noise(waveforms, lengths) + + return waveforms + + +def build_augment_pipeline(target_dir=None) -> List[paddle.nn.Layer]: + """build augment pipeline + Note: this pipeline cannot be used in the paddle.DataLoader + + Returns: + List[paddle.nn.Layer]: all augment process + """ + logger.info("start to build the augment pipeline") + noise_dataset = OpenRIRNoise('noise', target_dir=target_dir) + rir_dataset = OpenRIRNoise('rir', target_dir=target_dir) + + wavedrop = TimeDomainSpecAugment( + sample_rate=16000, + speeds=[100], ) + speed_perturb = TimeDomainSpecAugment( + sample_rate=16000, + speeds=[95, 100, 105], ) + add_noise = EnvCorrupt( + noise_dataset=noise_dataset, + reverb_prob=0.0, + noise_prob=1.0, + noise_snr_low=0, + noise_snr_high=15, + rir_scale_factor=1.0, ) + add_rev = EnvCorrupt( + rir_dataset=rir_dataset, + reverb_prob=1.0, + noise_prob=0.0, + rir_scale_factor=1.0, ) + add_rev_noise = EnvCorrupt( + noise_dataset=noise_dataset, + rir_dataset=rir_dataset, + reverb_prob=1.0, + noise_prob=1.0, + noise_snr_low=0, + noise_snr_high=15, + rir_scale_factor=1.0, ) + + return [wavedrop, speed_perturb, add_noise, add_rev, add_rev_noise] + + +def waveform_augment(waveforms: paddle.Tensor, + augment_pipeline: List[paddle.nn.Layer]) -> paddle.Tensor: + """process the augment pipeline and return all the waveforms + + Args: + waveforms (paddle.Tensor): original batch waveform + augment_pipeline (List[paddle.nn.Layer]): agument pipeline process + + Returns: + paddle.Tensor: all the audio waveform including the original waveform and augmented waveform + """ + # stage 0: store the original waveforms + waveforms_aug_list = [waveforms] + + # augment the original batch waveform + for aug in augment_pipeline: + # stage 1: augment the data + waveforms_aug = aug(waveforms) # (N, L) + if waveforms_aug.shape[1] >= waveforms.shape[1]: + # Trunc + waveforms_aug = waveforms_aug[:, :waveforms.shape[1]] + else: + # Pad + lengths_to_pad = waveforms.shape[1] - waveforms_aug.shape[1] + waveforms_aug = F.pad( + waveforms_aug.unsqueeze(-1), [0, lengths_to_pad], + data_format='NLC').squeeze(-1) + # stage 2: append the augmented waveform into the list + waveforms_aug_list.append(waveforms_aug) + + # get the all the waveforms + return paddle.concat(waveforms_aug_list, axis=0) diff --git a/ernie-sat/paddlespeech/vector/io/batch.py b/ernie-sat/paddlespeech/vector/io/batch.py new file mode 100644 index 0000000..92ca990 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/io/batch.py @@ -0,0 +1,166 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy +import numpy as np +import paddle + + +def waveform_collate_fn(batch): + waveforms = np.stack([item['feat'] for item in batch]) + labels = np.stack([item['label'] for item in batch]) + + return {'waveforms': waveforms, 'labels': labels} + + +def feature_normalize(feats: paddle.Tensor, + mean_norm: bool=True, + std_norm: bool=True, + convert_to_numpy: bool=False): + # Features normalization if needed + # numpy.mean is a little with paddle.mean about 1e-6 + if convert_to_numpy: + feats_np = feats.numpy() + mean = feats_np.mean(axis=-1, keepdims=True) if mean_norm else 0 + std = feats_np.std(axis=-1, keepdims=True) if std_norm else 1 + feats_np = (feats_np - mean) / std + feats = paddle.to_tensor(feats_np, dtype=feats.dtype) + else: + mean = feats.mean(axis=-1, keepdim=True) if mean_norm else 0 + std = feats.std(axis=-1, keepdim=True) if std_norm else 1 + feats = (feats - mean) / std + + return feats + + +def pad_right_2d(x, target_length, axis=-1, mode='constant', **kwargs): + x = np.asarray(x) + assert len( + x.shape) == 2, f'Only 2D arrays supported, but got shape: {x.shape}' + + w = target_length - x.shape[axis] + assert w >= 0, f'Target length {target_length} is less than origin length {x.shape[axis]}' + + if axis == 0: + pad_width = [[0, w], [0, 0]] + else: + pad_width = [[0, 0], [0, w]] + + return np.pad(x, pad_width, mode=mode, **kwargs) + + +def batch_feature_normalize(batch, mean_norm: bool=True, std_norm: bool=True): + ids = [item['id'] for item in batch] + lengths = np.asarray([item['feat'].shape[1] for item in batch]) + feats = list( + map(lambda x: pad_right_2d(x, lengths.max()), + [item['feat'] for item in batch])) + feats = np.stack(feats) + + # Features normalization if needed + for i in range(len(feats)): + feat = feats[i][:, :lengths[i]] # Excluding pad values. + mean = feat.mean(axis=-1, keepdims=True) if mean_norm else 0 + std = feat.std(axis=-1, keepdims=True) if std_norm else 1 + feats[i][:, :lengths[i]] = (feat - mean) / std + assert feats[i][:, lengths[ + i]:].sum() == 0 # Padding valus should all be 0. + + # Converts into ratios. + # the utterance of the max length doesn't need to padding + # the remaining utterances need to padding and all of them will be padded to max length + # we convert the original length of each utterance to the ratio of the max length + lengths = (lengths / lengths.max()).astype(np.float32) + + return {'ids': ids, 'feats': feats, 'lengths': lengths} + + +def pad_right_to(array, target_shape, mode="constant", value=0): + """ + This function takes a numpy array of arbitrary shape and pads it to target + shape by appending values on the right. + + Args: + array: input numpy array. Input array whose dimension we need to pad. + target_shape : (list, tuple). Target shape we want for the target array its len must be equal to array.ndim + mode : str. Pad mode, please refer to numpy.pad documentation. + value : float. Pad value, please refer to numpy.pad documentation. + + Returns: + array: numpy.array. Padded array. + valid_vals : list. List containing proportion for each dimension of original, non-padded values. + """ + assert len(target_shape) == array.ndim + pads = [] # this contains the abs length of the padding for each dimension. + valid_vals = [] # this contains the relative lengths for each dimension. + i = 0 # iterating over target_shape ndims + while i < len(target_shape): + assert (target_shape[i] >= array.shape[i] + ), "Target shape must be >= original shape for every dim" + pads.append([0, target_shape[i] - array.shape[i]]) + valid_vals.append(array.shape[i] / target_shape[i]) + i += 1 + + array = numpy.pad(array, pads, mode=mode, constant_values=value) + + return array, valid_vals + + +def batch_pad_right(arrays, mode="constant", value=0): + """Given a list of numpy arrays it batches them together by padding to the right + on each dimension in order to get same length for all. + + Args: + arrays : list. List of array we wish to pad together. + mode : str. Padding mode see numpy.pad documentation. + value : float. Padding value see numpy.pad documentation. + + Returns: + array : numpy.array. Padded array. + valid_vals : list. List containing proportion for each dimension of original, non-padded values. + """ + + if not len(arrays): + raise IndexError("arrays list must not be empty") + + if len(arrays) == 1: + # if there is only one array in the batch we simply unsqueeze it. + return numpy.expand_dims(arrays[0], axis=0), numpy.array([1.0]) + + if not (any( + [arrays[i].ndim == arrays[0].ndim for i in range(1, len(arrays))])): + raise IndexError("All arrays must have same number of dimensions") + + # FIXME we limit the support here: we allow padding of only the last dimension + # need to remove this when feat extraction is updated to handle multichannel. + max_shape = [] + for dim in range(arrays[0].ndim): + if dim != (arrays[0].ndim - 1): + if not all( + [x.shape[dim] == arrays[0].shape[dim] for x in arrays[1:]]): + raise EnvironmentError( + "arrays should have same dimensions except for last one") + max_shape.append(max([x.shape[dim] for x in arrays])) + + batched = [] + valid = [] + for t in arrays: + # for each array we apply pad_right_to + padded, valid_percent = pad_right_to( + t, max_shape, mode=mode, value=value) + batched.append(padded) + valid.append(valid_percent[-1]) + + batched = numpy.stack(batched) + + return batched, numpy.array(valid) diff --git a/ernie-sat/paddlespeech/vector/io/signal_processing.py b/ernie-sat/paddlespeech/vector/io/signal_processing.py new file mode 100644 index 0000000..ee939bd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/io/signal_processing.py @@ -0,0 +1,217 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle + +# TODO: Complete type-hint and doc string. + + +def blackman_window(win_len, dtype=np.float32): + arcs = np.pi * np.arange(win_len) / float(win_len) + win = np.asarray( + [0.42 - 0.5 * np.cos(2 * arc) + 0.08 * np.cos(4 * arc) for arc in arcs], + dtype=dtype) + return paddle.to_tensor(win) + + +def compute_amplitude(waveforms, lengths=None, amp_type="avg", scale="linear"): + if len(waveforms.shape) == 1: + waveforms = waveforms.unsqueeze(0) + + assert amp_type in ["avg", "peak"] + assert scale in ["linear", "dB"] + + if amp_type == "avg": + if lengths is None: + out = paddle.mean(paddle.abs(waveforms), axis=1, keepdim=True) + else: + wav_sum = paddle.sum(paddle.abs(waveforms), axis=1, keepdim=True) + out = wav_sum / lengths + elif amp_type == "peak": + out = paddle.max(paddle.abs(waveforms), axis=1, keepdim=True) + else: + raise NotImplementedError + + if scale == "linear": + return out + elif scale == "dB": + return paddle.clip(20 * paddle.log10(out), min=-80) + else: + raise NotImplementedError + + +def dB_to_amplitude(SNR): + return 10**(SNR / 20) + + +def convolve1d( + waveform, + kernel, + padding=0, + pad_type="constant", + stride=1, + groups=1, ): + if len(waveform.shape) != 3: + raise ValueError("Convolve1D expects a 3-dimensional tensor") + + # Padding can be a tuple (left_pad, right_pad) or an int + if isinstance(padding, list): + waveform = paddle.nn.functional.pad( + x=waveform, + pad=padding, + mode=pad_type, + data_format='NLC', ) + + # Move time dimension last, which pad and fft and conv expect. + # (N, L, C) -> (N, C, L) + waveform = waveform.transpose([0, 2, 1]) + kernel = kernel.transpose([0, 2, 1]) + + convolved = paddle.nn.functional.conv1d( + x=waveform, + weight=kernel, + stride=stride, + groups=groups, + padding=padding if not isinstance(padding, list) else 0, ) + + # Return time dimension to the second dimension. + return convolved.transpose([0, 2, 1]) + + +def notch_filter(notch_freq, filter_width=101, notch_width=0.05): + # Check inputs + assert 0 < notch_freq <= 1 + assert filter_width % 2 != 0 + pad = filter_width // 2 + inputs = paddle.arange(filter_width, dtype='float32') - pad + + # Avoid frequencies that are too low + notch_freq += notch_width + + # Define sinc function, avoiding division by zero + def sinc(x): + def _sinc(x): + return paddle.sin(x) / x + + # The zero is at the middle index + res = paddle.concat( + [_sinc(x[:pad]), paddle.ones([1]), _sinc(x[pad + 1:])]) + return res + + # Compute a low-pass filter with cutoff frequency notch_freq. + hlpf = sinc(3 * (notch_freq - notch_width) * inputs) + # import torch + # hlpf *= paddle.to_tensor(torch.blackman_window(filter_width).detach().numpy()) + hlpf *= blackman_window(filter_width) + hlpf /= paddle.sum(hlpf) + + # Compute a high-pass filter with cutoff frequency notch_freq. + hhpf = sinc(3 * (notch_freq + notch_width) * inputs) + # hhpf *= paddle.to_tensor(torch.blackman_window(filter_width).detach().numpy()) + hhpf *= blackman_window(filter_width) + hhpf /= -paddle.sum(hhpf) + hhpf[pad] += 1 + + # Adding filters creates notch filter + return (hlpf + hhpf).reshape([1, -1, 1]) + + +def reverberate(waveforms, + rir_waveform, + sample_rate, + impulse_duration=0.3, + rescale_amp="avg"): + orig_shape = waveforms.shape + + if len(waveforms.shape) > 3 or len(rir_waveform.shape) > 3: + raise NotImplementedError + + # if inputs are mono tensors we reshape to 1, samples + if len(waveforms.shape) == 1: + waveforms = waveforms.unsqueeze(0).unsqueeze(-1) + elif len(waveforms.shape) == 2: + waveforms = waveforms.unsqueeze(-1) + + if len(rir_waveform.shape) == 1: # convolve1d expects a 3d tensor ! + rir_waveform = rir_waveform.unsqueeze(0).unsqueeze(-1) + elif len(rir_waveform.shape) == 2: + rir_waveform = rir_waveform.unsqueeze(-1) + + # Compute the average amplitude of the clean + orig_amplitude = compute_amplitude(waveforms, waveforms.shape[1], + rescale_amp) + + # Compute index of the direct signal, so we can preserve alignment + impulse_index_start = rir_waveform.abs().argmax(axis=1).item() + impulse_index_end = min( + impulse_index_start + int(sample_rate * impulse_duration), + rir_waveform.shape[1]) + rir_waveform = rir_waveform[:, impulse_index_start:impulse_index_end, :] + rir_waveform = rir_waveform / paddle.norm(rir_waveform, p=2) + rir_waveform = paddle.flip(rir_waveform, [1]) + + waveforms = convolve1d( + waveform=waveforms, + kernel=rir_waveform, + padding=[rir_waveform.shape[1] - 1, 0], ) + + # Rescale to the peak amplitude of the clean waveform + waveforms = rescale(waveforms, waveforms.shape[1], orig_amplitude, + rescale_amp) + + if len(orig_shape) == 1: + waveforms = waveforms.squeeze(0).squeeze(-1) + if len(orig_shape) == 2: + waveforms = waveforms.squeeze(-1) + + return waveforms + + +def rescale(waveforms, lengths, target_lvl, amp_type="avg", scale="linear"): + assert amp_type in ["peak", "avg"] + assert scale in ["linear", "dB"] + + batch_added = False + if len(waveforms.shape) == 1: + batch_added = True + waveforms = waveforms.unsqueeze(0) + + waveforms = normalize(waveforms, lengths, amp_type) + + if scale == "linear": + out = target_lvl * waveforms + elif scale == "dB": + out = dB_to_amplitude(target_lvl) * waveforms + + else: + raise NotImplementedError("Invalid scale, choose between dB and linear") + + if batch_added: + out = out.squeeze(0) + + return out + + +def normalize(waveforms, lengths=None, amp_type="avg", eps=1e-14): + assert amp_type in ["avg", "peak"] + + batch_added = False + if len(waveforms.shape) == 1: + batch_added = True + waveforms = waveforms.unsqueeze(0) + + den = compute_amplitude(waveforms, lengths, amp_type) + eps + if batch_added: + waveforms = waveforms.squeeze(0) + return waveforms / den diff --git a/ernie-sat/paddlespeech/vector/models/__init__.py b/ernie-sat/paddlespeech/vector/models/__init__.py new file mode 100644 index 0000000..185a92b --- /dev/null +++ b/ernie-sat/paddlespeech/vector/models/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/models/ecapa_tdnn.py b/ernie-sat/paddlespeech/vector/models/ecapa_tdnn.py new file mode 100644 index 0000000..895ff13 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/models/ecapa_tdnn.py @@ -0,0 +1,520 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +def length_to_mask(length, max_len=None, dtype=None): + assert len(length.shape) == 1 + + if max_len is None: + max_len = length.max().astype( + 'int').item() # using arange to generate mask + mask = paddle.arange( + max_len, dtype=length.dtype).expand( + (len(length), max_len)) < length.unsqueeze(1) + + if dtype is None: + dtype = length.dtype + + mask = paddle.to_tensor(mask, dtype=dtype) + return mask + + +class Conv1d(nn.Layer): + def __init__( + self, + in_channels, + out_channels, + kernel_size, + stride=1, + padding="same", + dilation=1, + groups=1, + bias=True, + padding_mode="reflect", ): + """_summary_ + + Args: + in_channels (int): intput channel or input data dimensions + out_channels (int): output channel or output data dimensions + kernel_size (int): kernel size of 1-d convolution + stride (int, optional): strid in 1-d convolution . Defaults to 1. + padding (str, optional): padding value. Defaults to "same". + dilation (int, optional): dilation in 1-d convolution. Defaults to 1. + groups (int, optional): groups in 1-d convolution. Defaults to 1. + bias (bool, optional): bias in 1-d convolution . Defaults to True. + padding_mode (str, optional): padding mode. Defaults to "reflect". + """ + super().__init__() + + self.kernel_size = kernel_size + self.stride = stride + self.dilation = dilation + self.padding = padding + self.padding_mode = padding_mode + + self.conv = nn.Conv1D( + in_channels, + out_channels, + self.kernel_size, + stride=self.stride, + padding=0, + dilation=self.dilation, + groups=groups, + bias_attr=bias, ) + + def forward(self, x): + """Do conv1d forward + + Args: + x (paddle.Tensor): [N, C, L] input data, + N is the batch, + C is the data dimension, + L is the time + + Raises: + ValueError: only support the same padding type + + Returns: + paddle.Tensor: the value of conv1d + """ + if self.padding == "same": + x = self._manage_padding(x, self.kernel_size, self.dilation, + self.stride) + else: + raise ValueError("Padding must be 'same'. Got {self.padding}") + + return self.conv(x) + + def _manage_padding(self, x, kernel_size: int, dilation: int, stride: int): + """Padding the input data + + Args: + x (paddle.Tensor): [N, C, L] input data + N is the batch, + C is the data dimension, + L is the time + kernel_size (int): 1-d convolution kernel size + dilation (int): 1-d convolution dilation + stride (int): 1-d convolution stride + + Returns: + paddle.Tensor: the padded input data + """ + L_in = x.shape[-1] # Detecting input shape + padding = self._get_padding_elem(L_in, stride, kernel_size, + dilation) # Time padding + x = F.pad( + x, padding, mode=self.padding_mode, + data_format="NCL") # Applying padding + return x + + def _get_padding_elem(self, + L_in: int, + stride: int, + kernel_size: int, + dilation: int): + """Calculate the padding value in same mode + + Args: + L_in (int): the times of the input data, + stride (int): 1-d convolution stride + kernel_size (int): 1-d convolution kernel size + dilation (int): 1-d convolution stride + + Returns: + int: return the padding value in same mode + """ + if stride > 1: + n_steps = math.ceil(((L_in - kernel_size * dilation) / stride) + 1) + L_out = stride * (n_steps - 1) + kernel_size * dilation + padding = [kernel_size // 2, kernel_size // 2] + else: + L_out = (L_in - dilation * (kernel_size - 1) - 1) // stride + 1 + + padding = [(L_in - L_out) // 2, (L_in - L_out) // 2] + + return padding + + +class BatchNorm1d(nn.Layer): + def __init__( + self, + input_size, + eps=1e-05, + momentum=0.9, + weight_attr=None, + bias_attr=None, + data_format='NCL', + use_global_stats=None, ): + super().__init__() + + self.norm = nn.BatchNorm1D( + input_size, + epsilon=eps, + momentum=momentum, + weight_attr=weight_attr, + bias_attr=bias_attr, + data_format=data_format, + use_global_stats=use_global_stats, ) + + def forward(self, x): + x_n = self.norm(x) + return x_n + + +class TDNNBlock(nn.Layer): + def __init__( + self, + in_channels, + out_channels, + kernel_size, + dilation, + activation=nn.ReLU, ): + """Implementation of TDNN network + + Args: + in_channels (int): input channels or input embedding dimensions + out_channels (int): output channels or output embedding dimensions + kernel_size (int): the kernel size of the TDNN network block + dilation (int): the dilation of the TDNN network block + activation (paddle class, optional): the activation layers. Defaults to nn.ReLU. + """ + super().__init__() + self.conv = Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=kernel_size, + dilation=dilation, ) + self.activation = activation() + self.norm = BatchNorm1d(input_size=out_channels) + + def forward(self, x): + return self.norm(self.activation(self.conv(x))) + + +class Res2NetBlock(nn.Layer): + def __init__(self, in_channels, out_channels, scale=8, dilation=1): + """Implementation of Res2Net Block with dilation + The paper is refered as "Res2Net: A New Multi-scale Backbone Architecture", + whose url is https://arxiv.org/abs/1904.01169 + Args: + in_channels (int): input channels or input dimensions + out_channels (int): output channels or output dimensions + scale (int, optional): scale in res2net bolck. Defaults to 8. + dilation (int, optional): dilation of 1-d convolution in TDNN block. Defaults to 1. + """ + super().__init__() + assert in_channels % scale == 0 + assert out_channels % scale == 0 + + in_channel = in_channels // scale + hidden_channel = out_channels // scale + + self.blocks = nn.LayerList([ + TDNNBlock( + in_channel, hidden_channel, kernel_size=3, dilation=dilation) + for i in range(scale - 1) + ]) + self.scale = scale + + def forward(self, x): + y = [] + for i, x_i in enumerate(paddle.chunk(x, self.scale, axis=1)): + if i == 0: + y_i = x_i + elif i == 1: + y_i = self.blocks[i - 1](x_i) + else: + y_i = self.blocks[i - 1](x_i + y_i) + y.append(y_i) + y = paddle.concat(y, axis=1) + return y + + +class SEBlock(nn.Layer): + def __init__(self, in_channels, se_channels, out_channels): + """Implementation of SEBlock + The paper is refered as "Squeeze-and-Excitation Networks" + whose url is https://arxiv.org/abs/1709.01507 + Args: + in_channels (int): input channels or input data dimensions + se_channels (_type_): _description_ + out_channels (int): output channels or output data dimensions + """ + super().__init__() + + self.conv1 = Conv1d( + in_channels=in_channels, out_channels=se_channels, kernel_size=1) + self.relu = paddle.nn.ReLU() + self.conv2 = Conv1d( + in_channels=se_channels, out_channels=out_channels, kernel_size=1) + self.sigmoid = paddle.nn.Sigmoid() + + def forward(self, x, lengths=None): + L = x.shape[-1] + if lengths is not None: + mask = length_to_mask(lengths * L, max_len=L) + mask = mask.unsqueeze(1) + total = mask.sum(axis=2, keepdim=True) + s = (x * mask).sum(axis=2, keepdim=True) / total + else: + s = x.mean(axis=2, keepdim=True) + + s = self.relu(self.conv1(s)) + s = self.sigmoid(self.conv2(s)) + + return s * x + + +class AttentiveStatisticsPooling(nn.Layer): + def __init__(self, channels, attention_channels=128, global_context=True): + """Compute the speaker verification statistics + The detail info is section 3.1 in https://arxiv.org/pdf/1709.01507.pdf + Args: + channels (int): input data channel or data dimension + attention_channels (int, optional): attention dimension. Defaults to 128. + global_context (bool, optional): If use the global context information. Defaults to True. + """ + super().__init__() + + self.eps = 1e-12 + self.global_context = global_context + if global_context: + self.tdnn = TDNNBlock(channels * 3, attention_channels, 1, 1) + else: + self.tdnn = TDNNBlock(channels, attention_channels, 1, 1) + self.tanh = nn.Tanh() + self.conv = Conv1d( + in_channels=attention_channels, + out_channels=channels, + kernel_size=1) + + def forward(self, x, lengths=None): + C, L = x.shape[1], x.shape[2] # KP: (N, C, L) + + def _compute_statistics(x, m, axis=2, eps=self.eps): + mean = (m * x).sum(axis) + std = paddle.sqrt( + (m * (x - mean.unsqueeze(axis)).pow(2)).sum(axis).clip(eps)) + return mean, std + + if lengths is None: + lengths = paddle.ones([x.shape[0]]) + + # Make binary mask of shape [N, 1, L] + mask = length_to_mask(lengths * L, max_len=L) + mask = mask.unsqueeze(1) + + # Expand the temporal context of the pooling layer by allowing the + # self-attention to look at global properties of the utterance. + if self.global_context: + total = mask.sum(axis=2, keepdim=True).astype('float32') + mean, std = _compute_statistics(x, mask / total) + mean = mean.unsqueeze(2).tile((1, 1, L)) + std = std.unsqueeze(2).tile((1, 1, L)) + attn = paddle.concat([x, mean, std], axis=1) + else: + attn = x + + # Apply layers + attn = self.conv(self.tanh(self.tdnn(attn))) + + # Filter out zero-paddings + attn = paddle.where( + mask.tile((1, C, 1)) == 0, + paddle.ones_like(attn) * float("-inf"), attn) + + attn = F.softmax(attn, axis=2) + mean, std = _compute_statistics(x, attn) + + # Append mean and std of the batch + pooled_stats = paddle.concat((mean, std), axis=1) + pooled_stats = pooled_stats.unsqueeze(2) + + return pooled_stats + + +class SERes2NetBlock(nn.Layer): + def __init__( + self, + in_channels, + out_channels, + res2net_scale=8, + se_channels=128, + kernel_size=1, + dilation=1, + activation=nn.ReLU, ): + """Implementation of Squeeze-Extraction Res2Blocks in ECAPA-TDNN network model + The paper is refered "Squeeze-and-Excitation Networks" + whose url is: https://arxiv.org/pdf/1709.01507.pdf + Args: + in_channels (int): input channels or input data dimensions + out_channels (int): output channels or output data dimensions + res2net_scale (int, optional): scale in the res2net block. Defaults to 8. + se_channels (int, optional): embedding dimensions of res2net block. Defaults to 128. + kernel_size (int, optional): kernel size of 1-d convolution in TDNN block. Defaults to 1. + dilation (int, optional): dilation of 1-d convolution in TDNN block. Defaults to 1. + activation (paddle.nn.class, optional): activation function. Defaults to nn.ReLU. + """ + super().__init__() + self.out_channels = out_channels + self.tdnn1 = TDNNBlock( + in_channels, + out_channels, + kernel_size=1, + dilation=1, + activation=activation, ) + self.res2net_block = Res2NetBlock(out_channels, out_channels, + res2net_scale, dilation) + self.tdnn2 = TDNNBlock( + out_channels, + out_channels, + kernel_size=1, + dilation=1, + activation=activation, ) + self.se_block = SEBlock(out_channels, se_channels, out_channels) + + self.shortcut = None + if in_channels != out_channels: + self.shortcut = Conv1d( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, ) + + def forward(self, x, lengths=None): + residual = x + if self.shortcut: + residual = self.shortcut(x) + + x = self.tdnn1(x) + x = self.res2net_block(x) + x = self.tdnn2(x) + x = self.se_block(x, lengths) + + return x + residual + + +class EcapaTdnn(nn.Layer): + def __init__( + self, + input_size, + lin_neurons=192, + activation=nn.ReLU, + channels=[512, 512, 512, 512, 1536], + kernel_sizes=[5, 3, 3, 3, 1], + dilations=[1, 2, 3, 4, 1], + attention_channels=128, + res2net_scale=8, + se_channels=128, + global_context=True, ): + """Implementation of ECAPA-TDNN backbone model network + The paper is refered as "ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation in TDNN Based Speaker Verification" + whose url is: https://arxiv.org/abs/2005.07143 + Args: + input_size (_type_): input fature dimension + lin_neurons (int, optional): speaker embedding size. Defaults to 192. + activation (paddle.nn.class, optional): activation function. Defaults to nn.ReLU. + channels (list, optional): inter embedding dimension. Defaults to [512, 512, 512, 512, 1536]. + kernel_sizes (list, optional): kernel size of 1-d convolution in TDNN block . Defaults to [5, 3, 3, 3, 1]. + dilations (list, optional): dilations of 1-d convolution in TDNN block. Defaults to [1, 2, 3, 4, 1]. + attention_channels (int, optional): attention dimensions. Defaults to 128. + res2net_scale (int, optional): scale value in res2net. Defaults to 8. + se_channels (int, optional): dimensions of squeeze-excitation block. Defaults to 128. + global_context (bool, optional): global context flag. Defaults to True. + """ + super().__init__() + assert len(channels) == len(kernel_sizes) + assert len(channels) == len(dilations) + self.channels = channels + self.blocks = nn.LayerList() + self.emb_size = lin_neurons + + # The initial TDNN layer + self.blocks.append( + TDNNBlock( + input_size, + channels[0], + kernel_sizes[0], + dilations[0], + activation, )) + + # SE-Res2Net layers + for i in range(1, len(channels) - 1): + self.blocks.append( + SERes2NetBlock( + channels[i - 1], + channels[i], + res2net_scale=res2net_scale, + se_channels=se_channels, + kernel_size=kernel_sizes[i], + dilation=dilations[i], + activation=activation, )) + + # Multi-layer feature aggregation + self.mfa = TDNNBlock( + channels[-1], + channels[-1], + kernel_sizes[-1], + dilations[-1], + activation, ) + + # Attentive Statistical Pooling + self.asp = AttentiveStatisticsPooling( + channels[-1], + attention_channels=attention_channels, + global_context=global_context, ) + self.asp_bn = BatchNorm1d(input_size=channels[-1] * 2) + + # Final linear transformation + self.fc = Conv1d( + in_channels=channels[-1] * 2, + out_channels=self.emb_size, + kernel_size=1, ) + + def forward(self, x, lengths=None): + """ + Compute embeddings. + + Args: + x (paddle.Tensor): Input log-fbanks with shape (N, n_mels, T). + lengths (paddle.Tensor, optional): Length proportions of batch length with shape (N). Defaults to None. + + Returns: + paddle.Tensor: Output embeddings with shape (N, self.emb_size, 1) + """ + xl = [] + for layer in self.blocks: + try: + x = layer(x, lengths=lengths) + except TypeError: + x = layer(x) + xl.append(x) + + # Multi-layer feature aggregation + x = paddle.concat(xl[1:], axis=1) + x = self.mfa(x) + + # Attentive Statistical Pooling + x = self.asp(x, lengths=lengths) + x = self.asp_bn(x) + + # Final linear transformation + x = self.fc(x) + + return x diff --git a/ernie-sat/paddlespeech/vector/models/lstm_speaker_encoder.py b/ernie-sat/paddlespeech/vector/models/lstm_speaker_encoder.py new file mode 100644 index 0000000..f92fddc --- /dev/null +++ b/ernie-sat/paddlespeech/vector/models/lstm_speaker_encoder.py @@ -0,0 +1,147 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +import paddle +from paddle import nn +from paddle.nn import functional as F +from paddle.nn import initializer as I +from scipy.interpolate import interp1d +from scipy.optimize import brentq +from sklearn.metrics import roc_curve + + +class LSTMSpeakerEncoder(nn.Layer): + def __init__(self, n_mels, num_layers, hidden_size, output_size): + super().__init__() + self.lstm = nn.LSTM(n_mels, hidden_size, num_layers) + self.linear = nn.Linear(hidden_size, output_size) + self.similarity_weight = self.create_parameter( + [1], default_initializer=I.Constant(10.)) + self.similarity_bias = self.create_parameter( + [1], default_initializer=I.Constant(-5.)) + + def forward(self, utterances, num_speakers, initial_states=None): + normalized_embeds = self.embed_sequences(utterances, initial_states) + embeds = normalized_embeds.reshape([num_speakers, -1, num_speakers]) + loss, eer = self.loss(embeds) + return loss, eer + + def embed_sequences(self, utterances, initial_states=None, reduce=False): + out, (h, c) = self.lstm(utterances, initial_states) + embeds = F.relu(self.linear(h[-1])) + normalized_embeds = F.normalize(embeds) + if reduce: + embed = paddle.mean(normalized_embeds, 0) + embed = F.normalize(embed, axis=0) + return embed + return normalized_embeds + + def embed_utterance(self, utterances, initial_states=None): + # utterances: [B, T, C] -> embed [C'] + embed = self.embed_sequences(utterances, initial_states, reduce=True) + return embed + + def similarity_matrix(self, embeds): + # (N, M, C) + speakers_per_batch, utterances_per_speaker, embed_dim = embeds.shape + + # Inclusive centroids (1 per speaker). Cloning is needed for reverse differentiation + centroids_incl = paddle.mean(embeds, axis=1) + centroids_incl_norm = paddle.norm( + centroids_incl, p=2, axis=1, keepdim=True) + normalized_centroids_incl = centroids_incl / centroids_incl_norm + + # Exclusive centroids (1 per utterance) + centroids_excl = paddle.broadcast_to( + paddle.sum(embeds, axis=1, keepdim=True), embeds.shape) - embeds + centroids_excl /= (utterances_per_speaker - 1) + centroids_excl_norm = paddle.norm( + centroids_excl, p=2, axis=2, keepdim=True) + normalized_centroids_excl = centroids_excl / centroids_excl_norm + + p1 = paddle.matmul( + embeds.reshape([-1, embed_dim]), + normalized_centroids_incl, + transpose_y=True) # (NMN) + p1 = p1.reshape([-1]) + # print("p1: ", p1.shape) + p2 = paddle.bmm( + embeds.reshape([-1, 1, embed_dim]), + normalized_centroids_excl.reshape([-1, embed_dim, 1])) # (NM, 1, 1) + p2 = p2.reshape([-1]) # (NM) + + # begin: alternative implementation for scatter + with paddle.no_grad(): + index = paddle.arange( + 0, speakers_per_batch * utterances_per_speaker, + dtype="int64").reshape( + [speakers_per_batch, utterances_per_speaker]) + index = index * speakers_per_batch + paddle.arange( + 0, speakers_per_batch, dtype="int64").unsqueeze(-1) + index = paddle.reshape(index, [-1]) + ones = paddle.ones( + [speakers_per_batch * utterances_per_speaker * speakers_per_batch]) + zeros = paddle.zeros_like(index, dtype=ones.dtype) + mask_p1 = paddle.scatter(ones, index, zeros) + p = p1 * mask_p1 + (1 - mask_p1) * paddle.scatter(ones, index, p2) + # end: alternative implementation for scatter + # p = paddle.scatter(p1, index, p2) + + p = p * self.similarity_weight + self.similarity_bias # neg + p = p.reshape( + [speakers_per_batch * utterances_per_speaker, speakers_per_batch]) + return p, p1, p2 + + def do_gradient_ops(self): + for p in [self.similarity_weight, self.similarity_bias]: + g = p._grad_ivar() + g = g * 0.01 + + def inv_argmax(self, i, num): + return np.eye(1, num, i, dtype=int)[0] + + def loss(self, embeds): + """ + Computes the softmax loss according the section 2.1 of GE2E. + + :param embeds: the embeddings as a tensor of shape (speakers_per_batch, + utterances_per_speaker, embedding_size) + :return: the loss and the EER for this batch of embeddings. + """ + speakers_per_batch, utterances_per_speaker = embeds.shape[:2] + + # Loss + sim_matrix, *_ = self.similarity_matrix(embeds) + sim_matrix = sim_matrix.reshape( + [speakers_per_batch * utterances_per_speaker, speakers_per_batch]) + target = paddle.arange( + 0, speakers_per_batch, dtype="int64").unsqueeze(-1) + target = paddle.expand(target, + [speakers_per_batch, utterances_per_speaker]) + target = paddle.reshape(target, [-1]) + + loss = nn.CrossEntropyLoss()(sim_matrix, target) + + # EER (not backpropagated) + with paddle.no_grad(): + ground_truth = target.numpy() + labels = np.array( + [self.inv_argmax(i, speakers_per_batch) for i in ground_truth]) + preds = sim_matrix.numpy() + + # Snippet from https://yangcha.github.io/EER-ROC/ + fpr, tpr, thresholds = roc_curve(labels.flatten(), preds.flatten()) + eer = brentq(lambda x: 1. - x - interp1d(fpr, tpr)(x), 0., 1.) + + return loss, eer diff --git a/ernie-sat/paddlespeech/vector/modules/__init__.py b/ernie-sat/paddlespeech/vector/modules/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/modules/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/modules/loss.py b/ernie-sat/paddlespeech/vector/modules/loss.py new file mode 100644 index 0000000..1c80dda --- /dev/null +++ b/ernie-sat/paddlespeech/vector/modules/loss.py @@ -0,0 +1,93 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# This is modified from SpeechBrain +# https://github.com/speechbrain/speechbrain/blob/085be635c07f16d42cd1295045bc46c407f1e15b/speechbrain/nnet/losses.py +import math + +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +class AngularMargin(nn.Layer): + def __init__(self, margin=0.0, scale=1.0): + """An implementation of Angular Margin (AM) proposed in the following + paper: '''Margin Matters: Towards More Discriminative Deep Neural Network + Embeddings for Speaker Recognition''' (https://arxiv.org/abs/1906.07317) + + Args: + margin (float, optional): The margin for cosine similiarity. Defaults to 0.0. + scale (float, optional): The scale for cosine similiarity. Defaults to 1.0. + """ + super(AngularMargin, self).__init__() + self.margin = margin + self.scale = scale + + def forward(self, outputs, targets): + outputs = outputs - self.margin * targets + return self.scale * outputs + + +class AdditiveAngularMargin(AngularMargin): + def __init__(self, margin=0.0, scale=1.0, easy_margin=False): + """The Implementation of Additive Angular Margin (AAM) proposed + in the following paper: '''Margin Matters: Towards More Discriminative Deep Neural Network Embeddings for Speaker Recognition''' + (https://arxiv.org/abs/1906.07317) + + Args: + margin (float, optional): margin factor. Defaults to 0.0. + scale (float, optional): scale factor. Defaults to 1.0. + easy_margin (bool, optional): easy_margin flag. Defaults to False. + """ + super(AdditiveAngularMargin, self).__init__(margin, scale) + self.easy_margin = easy_margin + + self.cos_m = math.cos(self.margin) + self.sin_m = math.sin(self.margin) + self.th = math.cos(math.pi - self.margin) + self.mm = math.sin(math.pi - self.margin) * self.margin + + def forward(self, outputs, targets): + cosine = outputs.astype('float32') + sine = paddle.sqrt(1.0 - paddle.pow(cosine, 2)) + phi = cosine * self.cos_m - sine * self.sin_m # cos(theta + m) + if self.easy_margin: + phi = paddle.where(cosine > 0, phi, cosine) + else: + phi = paddle.where(cosine > self.th, phi, cosine - self.mm) + outputs = (targets * phi) + ((1.0 - targets) * cosine) + return self.scale * outputs + + +class LogSoftmaxWrapper(nn.Layer): + def __init__(self, loss_fn): + """Speaker identificatin loss function wrapper + including all of compositions of the loss transformation + Args: + loss_fn (_type_): the loss value of a batch + """ + super(LogSoftmaxWrapper, self).__init__() + self.loss_fn = loss_fn + self.criterion = paddle.nn.KLDivLoss(reduction="sum") + + def forward(self, outputs, targets, length=None): + targets = F.one_hot(targets, outputs.shape[1]) + try: + predictions = self.loss_fn(outputs, targets) + except TypeError: + predictions = self.loss_fn(outputs) + + predictions = F.log_softmax(predictions, axis=1) + loss = self.criterion(predictions, targets) / targets.sum() + return loss diff --git a/ernie-sat/paddlespeech/vector/modules/sid_model.py b/ernie-sat/paddlespeech/vector/modules/sid_model.py new file mode 100644 index 0000000..4045f75 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/modules/sid_model.py @@ -0,0 +1,87 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +class SpeakerIdetification(nn.Layer): + def __init__( + self, + backbone, + num_class, + lin_blocks=0, + lin_neurons=192, + dropout=0.1, ): + """The speaker identification model, which includes the speaker backbone network + and the a linear transform to speaker class num in training + + Args: + backbone (Paddle.nn.Layer class): the speaker identification backbone network model + num_class (_type_): the speaker class num in the training dataset + lin_blocks (int, optional): the linear layer transform between the embedding and the final linear layer. Defaults to 0. + lin_neurons (int, optional): the output dimension of final linear layer. Defaults to 192. + dropout (float, optional): the dropout factor on the embedding. Defaults to 0.1. + """ + super(SpeakerIdetification, self).__init__() + # speaker idenfication backbone network model + # the output of the backbond network is the target embedding + self.backbone = backbone + if dropout > 0: + self.dropout = nn.Dropout(dropout) + else: + self.dropout = None + + # construct the speaker classifer + input_size = self.backbone.emb_size + self.blocks = nn.LayerList() + for i in range(lin_blocks): + self.blocks.extend([ + nn.BatchNorm1D(input_size), + nn.Linear(in_features=input_size, out_features=lin_neurons), + ]) + input_size = lin_neurons + + # the final layer + self.weight = paddle.create_parameter( + shape=(input_size, num_class), + dtype='float32', + attr=paddle.ParamAttr(initializer=nn.initializer.XavierUniform()), ) + + def forward(self, x, lengths=None): + """Do the speaker identification model forwrd, + including the speaker embedding model and the classifier model network + + Args: + x (paddle.Tensor): input audio feats, + shape=[batch, dimension, times] + lengths (paddle.Tensor, optional): input audio length. + shape=[batch, times] + Defaults to None. + + Returns: + paddle.Tensor: return the logits of the feats + """ + # x.shape: (N, C, L) + x = self.backbone(x, lengths).squeeze( + -1) # (N, emb_size, 1) -> (N, emb_size) + if self.dropout is not None: + x = self.dropout(x) + + for fc in self.blocks: + x = fc(x) + + logits = F.linear(F.normalize(x), F.normalize(self.weight, axis=0)) + + return logits diff --git a/ernie-sat/paddlespeech/vector/training/__init__.py b/ernie-sat/paddlespeech/vector/training/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/training/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/training/scheduler.py b/ernie-sat/paddlespeech/vector/training/scheduler.py new file mode 100644 index 0000000..3dcac05 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/training/scheduler.py @@ -0,0 +1,45 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddle.optimizer.lr import LRScheduler + + +class CyclicLRScheduler(LRScheduler): + def __init__(self, + base_lr: float=1e-8, + max_lr: float=1e-3, + step_size: int=10000): + + super(CyclicLRScheduler, self).__init__() + + self.current_step = -1 + self.base_lr = base_lr + self.max_lr = max_lr + self.step_size = step_size + + def step(self): + if not hasattr(self, 'current_step'): + return + + self.current_step += 1 + if self.current_step >= 2 * self.step_size: + self.current_step %= 2 * self.step_size + + self.last_lr = self.get_lr() + + def get_lr(self): + p = self.current_step / (2 * self.step_size) # Proportion in one cycle. + if p < 0.5: # Increase + return self.base_lr + p / 0.5 * (self.max_lr - self.base_lr) + else: # Decrease + return self.max_lr - (p / 0.5 - 1) * (self.max_lr - self.base_lr) diff --git a/ernie-sat/paddlespeech/vector/training/seeding.py b/ernie-sat/paddlespeech/vector/training/seeding.py new file mode 100644 index 0000000..0778a27 --- /dev/null +++ b/ernie-sat/paddlespeech/vector/training/seeding.py @@ -0,0 +1,28 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddlespeech.s2t.utils.log import Log + +logger = Log(__name__).getlog() +import random + +import numpy as np +import paddle + + +def seed_everything(seed: int): + """Seed paddle, random and np.random to help reproductivity.""" + paddle.seed(seed) + random.seed(seed) + np.random.seed(seed) + logger.info(f"Set the seed of paddle, random, np.random to {seed}.") diff --git a/ernie-sat/paddlespeech/vector/utils/__init__.py b/ernie-sat/paddlespeech/vector/utils/__init__.py new file mode 100644 index 0000000..97043fd --- /dev/null +++ b/ernie-sat/paddlespeech/vector/utils/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/ernie-sat/paddlespeech/vector/utils/time.py b/ernie-sat/paddlespeech/vector/utils/time.py new file mode 100644 index 0000000..8e85b0e --- /dev/null +++ b/ernie-sat/paddlespeech/vector/utils/time.py @@ -0,0 +1,66 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License" +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math +import time + + +class Timer(object): + '''Calculate runing speed and estimated time of arrival(ETA)''' + + def __init__(self, total_step: int): + self.total_step = total_step + self.last_start_step = 0 + self.current_step = 0 + self._is_running = True + + def start(self): + self.last_time = time.time() + self.start_time = time.time() + + def stop(self): + self._is_running = False + self.end_time = time.time() + + def count(self) -> int: + if not self.current_step >= self.total_step: + self.current_step += 1 + return self.current_step + + @property + def timing(self) -> float: + run_steps = self.current_step - self.last_start_step + self.last_start_step = self.current_step + time_used = time.time() - self.last_time + self.last_time = time.time() + return time_used / run_steps + + @property + def is_running(self) -> bool: + return self._is_running + + @property + def eta(self) -> str: + if not self.is_running: + return '00:00:00' + remaining_time = time.time() - self.start_time + return seconds_to_hms(remaining_time) + + +def seconds_to_hms(seconds: int) -> str: + '''Convert the number of seconds to hh:mm:ss''' + h = math.floor(seconds / 3600) + m = math.floor((seconds - h * 3600) / 60) + s = int(seconds - h * 3600 - m * 60) + hms_str = '{:0>2}:{:0>2}:{:0>2}'.format(h, m, s) + return hms_str diff --git a/ernie-sat/phn_mapping.txt b/ernie-sat/phn_mapping.txt new file mode 100644 index 0000000..9553ced --- /dev/null +++ b/ernie-sat/phn_mapping.txt @@ -0,0 +1,306 @@ +ou3 ou3 +a3 a3 +eng4 eng4 +u1 u1 +vn2 vn2 +uang3 uang3 +ang3 ang3 +ua1 ua1 +ou1 ou1 +in3 in3 +uai4 uai4 +van1 van1 +en2 en2 +ia4 ia4 +uai2 uai2 +iang4 iang4 +ai3 ai3 +sp sp +in1 in1 +uai3 uai3 +ve1 ve1 +ou4 ou4 +d d +ang2 ang2 +iang3 iang3 +o1 o1 +iao3 iao3 +an1 an1 +en5 en5 +ong3 ong3 +e5 e5 +e3 e3 +van3 van3 +i3 i3 +i2 i2 +uo4 uo4 +i1 i1 +in2 in2 +v1 v1 +uang4 uang4 +en3 en3 +ian5 ian5 +ie3 ie3 +o2 o2 +x x +iang2 iang2 +ei1 ei1 +uang2 uang2 +t t +ao4 ao4 +ch ch +o3 o3 +en1 en1 +ie1 ie1 +uan3 uan3 +uo1 uo1 +iang5 iang5 +iong1 iong1 +l l +a5 a5 +an4 an4 +u2 u2 +ei3 ei3 +uo3 uo3 +ai2 ai2 +v3 v3 +k k +uan4 uan4 +ian2 ian2 +ei2 ei2 +sh sh +g g +ong2 ong2 +ing1 ing1 +vn3 vn3 +r r +ong1 ong1 +ao1 ao1 +ua3 ua3 +ia1 ia1 +u3 u3 +s s +b b +e2 e2 +ua4 ua4 +iang1 iang1 +ie4 ie4 +ou5 ou5 +ing4 ing4 +ai1 ai1 +iong4 iong4 +uo5 uo5 +ei5 ei5 +ueng1 ueng1 +ou2 ou2 +e1 e1 +f f +en4 en4 +v2 v2 +iao2 iao2 +ie2 ie2 +van2 van2 +eng1 eng1 +ai4 ai4 +uo2 uo2 +iao1 iao1 +in4 in4 +er4 er4 +e4 e4 +uan1 uan1 +ia3 ia3 +ao2 ao2 +u4 u4 +ei4 ei4 +eng3 eng3 +z z +j j +ve3 ve3 +n n +an3 an3 +uan2 uan2 +o5 o5 +ve2 ve2 +ang4 ang4 +er2 er2 +ia5 ia5 +ian4 ian4 +er5 er5 +ia2 ia2 +eng2 eng2 +ie5 ie5 +ang1 ang1 +er3 er3 +ian1 ian1 + +c c +v4 v4 +iao4 iao4 +a4 a4 +m m +a2 a2 +ong4 ong4 +q q +uang1 uang1 +an2 an2 +ua2 ua2 +zh zh +ing2 ing2 +ve4 ve4 +van4 van4 +vn4 vn4 +iong3 iong3 +i4 i4 +ian3 ian3 +ing3 ing3 +p p +iong2 iong2 +ao3 ao3 +vn1 vn1 +uai1 uai1 +a1 a1 +o4 o4 +h h +uenr4 un4 ee er5 +iaor3 iao3 ee er2 +iour4 ii iu4 ee er2 +iangr4 ii iang4 ee er5 +iou3 ii iu3 +sil sp +iour1 iu1 ee er5 +vn5 vn1 +ir1 i1 ee er2 +vanr1 van1 ee er2 +vanr2 van2 ee er5 +air3 ai3 ee er2 +uangr4 uu uang1 +enr1 en1 ee er2 +iour3 ii iu3 ee er5 +uenr1 un1 ee er5 +uenr3 un3 ee er5 +or2 o2 ee er2 +anr3 an3 ee er5 +ai5 ai4 +iaor2 iao2 ee er2 +uanr3 uan3 ee er5 +uanr2 uu uan4 ee er2 +uen1 un1 +ua5 uu ua2 +uen3 uu un3 +iii4 ix4 +uor1 uo1 ee er5 +our2 ou5 ee er2 +uei1 uu ui1 +vr3 v3 ee er5 +uenr2 un2 ee er5 +uanr5 uu uan2 ee er5 +iiir4 ix4 ee er5 +iiir1 ix1 ee er5 +ur2 u3 ee er5 +eng5 eng1 +ingr1 ii ing1 ee er2 +ii4 iy4 +ve5 vv ve1 +? +ii1 iy1 +ao5 ao3 +v5 vv v2 +ing5 ing2 +i5 i1 +iou5 ii iu3 +uen4 un4 +our4 ou4 ee er5 +io3 ii iu3 +ar4 a4 ee er5 +ingr2 ing2 ee er5 +ingr4 ing4 ee er5 +ir3 e5 ee er5 +iaor4 iao4 ee er5 +ii2 ix2 +uanr4 uan4 ee er5 +enr5 en4 ee er2 +ianr3 ian3 ee er5 +uei5 uu ui2 +ianr4 ian4 ee er2 +iar4 ia4 ee er2 +uair4 uai1 ee er2 +enr2 en2 ee er5 +iii1 ix1 +ver3 ve3 ee er2 +ianr5 ian3 ee er5 +ong5 ong1 +air2 ai2 ee er5 +angr4 ang4 ee er5 +iii5 ix2 +ang5 ang1 +iou1 iu1 +uar4 ua4 ee er5 +ur4 u4 ee er5 +iou4 iu4 +iou2 ii iu2 +in5 in1 +uor2 uo2 ee er5 +uar2 ua2 ee er5 +uei2 uu ui2 + +anr1 an1 ee er5 +ar5 a1 ee er5 +uen2 un2 +eir4 ei4 ee er2 +ingr3 ii ing3 ee er5 +aor4 ao4 ee er5 +enr4 en4 ee er5 +iao5 ii iao2 +iii2 ix2 +er1 e1 ee er5 +iaor1 iao1 ee er5 +ueir1 ui1 ee er2 +inr4 in4 ee er5 +ueir2 ui4 ee er5 +uan5 ai2 ee er5 +ir4 i4 ee er2 +ur1 u1 ee er5 +iour2 iu1 ee er2 +ar2 a2 ee er5 +an5 an2 +iii3 ix3 +ver4 vv ve4 ee er2 +。 +aor3 ao3 ee er5 +iong5 ii iong4 +u5 u4 +air4 ai4 ee er5 +ii3 iy3 +our5 ou4 ee er5 +inr1 in1 ee er5 +uor3 uo3 ee er5 +van5 van4 +ur5 u4 ee er2 +aor5 ao4 ee er5 +engr4 eng4 ee er2 +ueir4 ui4 ee er5 + +angr2 ang2 ee er2 +ii5 iy5 +vnr2 vn2 ee er5 +enr3 en3 ee er5 +uar1 ua1 ee er2 +vanr4 van4 ee er5 +, +uor5 uo3 ee er5 +uei4 ui4 +aor1 ao1 ee er5 +uen5 uu un4 +anr4 an4 ee er5 +iar1 ia1 ee er5 +vanr3 van3 ee er5 +uei3 uu ui3 +! +io1 ii uo5 +spl +ar3 a3 ee er5 +our3 ou3 ee er5 +ueir3 ui3 ee er5 +ianr2 ian3 ee er5 +ueng4 uu un4 +ianr1 ian1 ee er5 diff --git a/ernie-sat/prompt/dev/mfa_end b/ernie-sat/prompt/dev/mfa_end new file mode 100644 index 0000000..70c1237 --- /dev/null +++ b/ernie-sat/prompt/dev/mfa_end @@ -0,0 +1,3 @@ +Prompt_003_new 0.0425 0.0925 0.1825 0.2125 0.2425 0.3225 0.3725 0.4725 0.5325 0.5625 0.6225 0.7425 0.8625 0.9725 0.9975 1.0125 1.0825 1.2625 1.3125 +p299_096 0.7525 0.7925 0.8725 0.9125 0.9425 1.0325 1.0625 1.1925 1.2625 1.3225 1.3725 1.4125 1.5125 1.5425 1.6525 1.6925 1.7325 1.7625 1.8425 1.9625 2.0225 2.1825 2.3325 2.6825 +p243_new 1.0225 1.0525 1.0925 1.1325 1.1725 1.2625 1.3625 1.4125 1.5125 1.6225 1.6625 1.7925 1.8625 2.0025 2.0925 2.1725 2.2625 2.4325 2.4725 2.5225 2.5825 2.6125 2.6425 2.7425 2.8025 2.9025 2.9525 3.0525 3.0825 3.2125 3.4525 diff --git a/ernie-sat/prompt/dev/mfa_start b/ernie-sat/prompt/dev/mfa_start new file mode 100644 index 0000000..a975f8a --- /dev/null +++ b/ernie-sat/prompt/dev/mfa_start @@ -0,0 +1,3 @@ +Prompt_003_new 0.0125 0.0425 0.0925 0.1825 0.2125 0.2425 0.3225 0.3725 0.4725 0.5325 0.5625 0.6225 0.7425 0.8625 0.9725 0.9975 1.0125 1.0825 1.2625 +p243_new 0.0125 1.0225 1.0525 1.0925 1.1325 1.1725 1.2625 1.3625 1.4125 1.5125 1.6225 1.6625 1.7925 1.8625 2.0025 2.0925 2.1725 2.2625 2.4325 2.4725 2.5225 2.5825 2.6125 2.6425 2.7425 2.8025 2.9025 2.9525 3.0525 3.0825 3.2125 +p299_096 0.0125 0.7525 0.7925 0.8725 0.9125 0.9425 1.0325 1.0625 1.1925 1.2625 1.3225 1.3725 1.4125 1.5125 1.5425 1.6525 1.6925 1.7325 1.7625 1.8425 1.9625 2.0225 2.1825 2.3325 diff --git a/ernie-sat/prompt/dev/mfa_text b/ernie-sat/prompt/dev/mfa_text new file mode 100644 index 0000000..68a33eb --- /dev/null +++ b/ernie-sat/prompt/dev/mfa_text @@ -0,0 +1,3 @@ +Prompt_003_new DH IH1 S W AA1 Z N AA1 T DH AH0 SH OW1 F AO1 R M IY1 sp +p299_096 sp W IY1 AA1 R T R AY1 NG T UW1 AH0 S T AE1 B L IH0 SH AH0 D EY1 T sp +p243_new sp F AO1 R DH AE1 T R IY1 Z AH0 N sp K AH1 V ER0 SH UH1 D N AA1 T B IY1 G IH1 V AH0 N sp diff --git a/ernie-sat/prompt/dev/mfa_wav.scp b/ernie-sat/prompt/dev/mfa_wav.scp new file mode 100644 index 0000000..ad5b9d9 --- /dev/null +++ b/ernie-sat/prompt/dev/mfa_wav.scp @@ -0,0 +1,3 @@ +Prompt_003_new ../../prompt_wav/this_was_not_the_show_for_me.wav +p243_new ../../prompt_wav/p243_313.wav +p299_096 ../../prompt_wav/p299_096.wav diff --git a/ernie-sat/prompt/dev/text b/ernie-sat/prompt/dev/text new file mode 100644 index 0000000..026aa9a --- /dev/null +++ b/ernie-sat/prompt/dev/text @@ -0,0 +1,3 @@ +Prompt_003_new This was not the show for me. +p243_new For that reason cover should not be given. +p299_096 We are trying to establish a date. diff --git a/ernie-sat/prompt/dev/wav.scp b/ernie-sat/prompt/dev/wav.scp new file mode 100644 index 0000000..c0f8a1c --- /dev/null +++ b/ernie-sat/prompt/dev/wav.scp @@ -0,0 +1,3 @@ +Prompt_003_new ../../prompt_wav/this_was_not_the_show_for_me.wav +p299_096 ../../prompt_wav/p299_096.wav +p243_new ../../prompt_wav/p243_313.wav diff --git a/ernie-sat/prompt_wav/SSB03420111.wav b/ernie-sat/prompt_wav/SSB03420111.wav new file mode 100755 index 0000000000000000000000000000000000000000..fe44397f4d5c4648ad13b67e270f64947bd17a21 GIT binary patch literal 194966 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

@<&3;Gl7sJ2=gsnygX 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 &=gxa?TV6pLh(8Umq8+CuSWV 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 Tmqm{jZC9BcGrL;x@8A9brD=;a literal 0 HcmV?d00001 diff --git a/ernie-sat/prompt_wav/SSB03540015.wav b/ernie-sat/prompt_wav/SSB03540015.wav new file mode 100755 index 0000000000000000000000000000000000000000..5f5daea5016acbdf60e637a977660ade2550b898 GIT binary patch literal 245240 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

P5IpszV-}{6E8l1b>D01BR`Lx6kez*s}?#F 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%0DAV@h7q74H=3L|Wj| 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 Cp#z=( literal 0 HcmV?d00001 diff --git a/ernie-sat/prompt_wav/SSB03540428.wav b/ernie-sat/prompt_wav/SSB03540428.wav new file mode 100755 index 0000000000000000000000000000000000000000..3e90b005147bf7fa46fb3c33c2fd57fffd7ff890 GIT binary patch literal 189672 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�a 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 z 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

    y*Ab`1p>_GH*HSa6nj%g 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/prompt_wav/p299_096.wav b/ernie-sat/prompt_wav/p299_096.wav new file mode 100644 index 0000000000000000000000000000000000000000..1686ab30ef296f3d09a78f97ee31386a271dd0b5 GIT binary patch literal 258576 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/prompt_wav/p323_083.wav b/ernie-sat/prompt_wav/p323_083.wav new file mode 100644 index 0000000000000000000000000000000000000000..ff1d4997a82bdc9d7839f49de118cd0f21d1beac GIT binary patch literal 286832 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ℑ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 z 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 ~`O5Q|;h5&kh$XfFYK#QVW~ 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 D?qJga literal 0 HcmV?d00001 diff --git a/ernie-sat/prompt_wav/this_was_not_the_show_for_me.wav b/ernie-sat/prompt_wav/this_was_not_the_show_for_me.wav new file mode 100644 index 0000000000000000000000000000000000000000..3f4f109da8c33b01c0ea32c429033e5803a1ab89 GIT binary patch literal 63404 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 z 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 Dict[str, str]: + """Read a text file having 2 column as dict object. + + Examples: + wav.scp: + key1 /some/path/a.wav + key2 /some/path/b.wav + + >>> read_2column_text('wav.scp') + {'key1': '/some/path/a.wav', 'key2': '/some/path/b.wav'} + + """ + + data = {} + with Path(path).open("r", encoding="utf-8") as f: + for linenum, line in enumerate(f, 1): + sps = line.rstrip().split(maxsplit=1) + if len(sps) == 1: + k, v = sps[0], "" + else: + k, v = sps + if k in data: + raise RuntimeError(f"{k} is duplicated ({path}:{linenum})") + data[k] = v + return data + + +def load_num_sequence_text( + path: Union[Path, str], loader_type: str = "csv_int" +) -> Dict[str, List[Union[float, int]]]: + """Read a text file indicating sequences of number + + Examples: + key1 1 2 3 + key2 34 5 6 + + >>> d = load_num_sequence_text('text') + >>> np.testing.assert_array_equal(d["key1"], np.array([1, 2, 3])) + """ + if loader_type == "text_int": + delimiter = " " + dtype = int + elif loader_type == "text_float": + delimiter = " " + dtype = float + elif loader_type == "csv_int": + delimiter = "," + dtype = int + elif loader_type == "csv_float": + delimiter = "," + dtype = float + else: + raise ValueError(f"Not supported loader_type={loader_type}") + + # path looks like: + # utta 1,0 + # uttb 3,4,5 + # -> return {'utta': np.ndarray([1, 0]), + # 'uttb': np.ndarray([3, 4, 5])} + d = read_2column_text(path) + + # Using for-loop instead of dict-comprehension for debuggability + retval = {} + for k, v in d.items(): + try: + retval[k] = [dtype(i) for i in v.split(delimiter)] + except TypeError: + logging.error(f'Error happened with path="{path}", id="{k}", value="{v}"') + raise + return retval diff --git a/ernie-sat/run_clone_en_to_zh.sh b/ernie-sat/run_clone_en_to_zh.sh new file mode 100644 index 0000000..85b013c --- /dev/null +++ b/ernie-sat/run_clone_en_to_zh.sh @@ -0,0 +1,21 @@ +# en --> zh 的 clone +python sedit_inference_0520.py \ +--task_name cross-lingual_clone \ +--model_name paddle_checkpoint_ench \ +--uid Prompt_003_new \ +--new_str '今天天气很好' \ +--prefix ./prompt/dev/ \ +--clone_prefix ./prompt/dev_aishell3/ \ +--clone_uid SSB07510054 \ +--source_language english \ +--target_language chinese \ +--output_name task_cross_lingual_pred.wav \ +--voc pwgan_aishell3 \ +--voc_config download/pwg_aishell3_ckpt_0.5/default.yaml \ +--voc_ckpt download/pwg_aishell3_ckpt_0.5/snapshot_iter_1000000.pdz \ +--voc_stat download/pwg_aishell3_ckpt_0.5/feats_stats.npy \ +--am fastspeech2_csmsc \ +--am_config download/fastspeech2_conformer_baker_ckpt_0.5/conformer.yaml \ +--am_ckpt download/fastspeech2_conformer_baker_ckpt_0.5/snapshot_iter_76000.pdz \ +--am_stat download/fastspeech2_conformer_baker_ckpt_0.5/speech_stats.npy \ +--phones_dict download/fastspeech2_conformer_baker_ckpt_0.5/phone_id_map.txt \ No newline at end of file diff --git a/ernie-sat/run_gen_en.sh b/ernie-sat/run_gen_en.sh new file mode 100644 index 0000000..c89431c --- /dev/null +++ b/ernie-sat/run_gen_en.sh @@ -0,0 +1,40 @@ +# 纯英文的语音合成 +# python sedit_inference_0518.py \ +# --task_name synthesize \ +# --model_name paddle_checkpoint_en \ +# --uid p323_083 \ +# --new_str 'I enjoy my life.' \ +# --prefix ./prompt/dev/ \ +# --source_language english \ +# --target_language english \ +# --output_name pred.wav \ +# --voc pwgan_aishell3 \ +# --voc_config download/pwg_aishell3_ckpt_0.5/default.yaml \ +# --voc_ckpt download/pwg_aishell3_ckpt_0.5/snapshot_iter_1000000.pdz \ +# --voc_stat download/pwg_aishell3_ckpt_0.5/feats_stats.npy \ +# --am fastspeech2_ljspeech \ +# --am_config download/fastspeech2_nosil_ljspeech_ckpt_0.5/default.yaml \ +# --am_ckpt download/fastspeech2_nosil_ljspeech_ckpt_0.5/snapshot_iter_100000.pdz \ +# --am_stat download/fastspeech2_nosil_ljspeech_ckpt_0.5/speech_stats.npy \ +# --phones_dict download/fastspeech2_nosil_ljspeech_ckpt_0.5/phone_id_map.txt + + +# 纯英文的语音合成 +python sedit_inference_0520.py \ +--task_name synthesize \ +--model_name paddle_checkpoint_en \ +--uid p299_096 \ +--new_str 'I enjoy my life.' \ +--prefix ./prompt/dev/ \ +--source_language english \ +--target_language english \ +--output_name task_synthesize_pred.wav \ +--voc pwgan_aishell3 \ +--voc_config download/pwg_aishell3_ckpt_0.5/default.yaml \ +--voc_ckpt download/pwg_aishell3_ckpt_0.5/snapshot_iter_1000000.pdz \ +--voc_stat download/pwg_aishell3_ckpt_0.5/feats_stats.npy \ +--am fastspeech2_ljspeech \ +--am_config download/fastspeech2_nosil_ljspeech_ckpt_0.5/default.yaml \ +--am_ckpt download/fastspeech2_nosil_ljspeech_ckpt_0.5/snapshot_iter_100000.pdz \ +--am_stat download/fastspeech2_nosil_ljspeech_ckpt_0.5/speech_stats.npy \ +--phones_dict download/fastspeech2_nosil_ljspeech_ckpt_0.5/phone_id_map.txt \ No newline at end of file diff --git a/ernie-sat/run_sedit_en.sh b/ernie-sat/run_sedit_en.sh new file mode 100644 index 0000000..c3d5a74 --- /dev/null +++ b/ernie-sat/run_sedit_en.sh @@ -0,0 +1,19 @@ +# 纯英文的语音编辑 +python sedit_inference_0520.py \ +--task_name edit \ +--model_name paddle_checkpoint_en \ +--uid p243_new \ +--new_str 'for that reason cover is impossible to be given.' \ +--prefix ./prompt/dev/ \ +--source_language english \ +--target_language english \ +--output_name task_edit_pred.wav \ +--voc pwgan_aishell3 \ +--voc_config download/pwg_aishell3_ckpt_0.5/default.yaml \ +--voc_ckpt download/pwg_aishell3_ckpt_0.5/snapshot_iter_1000000.pdz \ +--voc_stat download/pwg_aishell3_ckpt_0.5/feats_stats.npy \ +--am fastspeech2_ljspeech \ +--am_config download/fastspeech2_nosil_ljspeech_ckpt_0.5/default.yaml \ +--am_ckpt download/fastspeech2_nosil_ljspeech_ckpt_0.5/snapshot_iter_100000.pdz \ +--am_stat download/fastspeech2_nosil_ljspeech_ckpt_0.5/speech_stats.npy \ +--phones_dict download/fastspeech2_nosil_ljspeech_ckpt_0.5/phone_id_map.txt \ No newline at end of file diff --git a/ernie-sat/sedit_arg_parser.py b/ernie-sat/sedit_arg_parser.py new file mode 100644 index 0000000..d0c4296 --- /dev/null +++ b/ernie-sat/sedit_arg_parser.py @@ -0,0 +1,93 @@ +import argparse +from paddlespeech.t2s.utils import str2bool + +def parse_args(): + # parse args and config and redirect to train_sp + parser = argparse.ArgumentParser( + description="Synthesize with acoustic model & vocoder") + # acoustic model + parser.add_argument( + '--am', + type=str, + default='fastspeech2_csmsc', + choices=[ + 'speedyspeech_csmsc', 'fastspeech2_csmsc', 'fastspeech2_ljspeech', + 'fastspeech2_aishell3', 'fastspeech2_vctk', 'tacotron2_csmsc', + 'tacotron2_ljspeech', 'tacotron2_aishell3' + ], + help='Choose acoustic model type of tts task.') + parser.add_argument( + '--am_config', + type=str, + default=None, + help='Config of acoustic model. Use deault config when it is None.') + parser.add_argument( + '--am_ckpt', + type=str, + default=None, + help='Checkpoint file of acoustic model.') + parser.add_argument( + "--am_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training acoustic model." + ) + parser.add_argument( + "--phones_dict", type=str, default=None, help="phone vocabulary file.") + parser.add_argument( + "--tones_dict", type=str, default=None, help="tone vocabulary file.") + parser.add_argument( + "--speaker_dict", type=str, default=None, help="speaker id map file.") + + # vocoder + parser.add_argument( + '--voc', + type=str, + default='pwgan_aishell3', + choices=[ + 'pwgan_csmsc', 'pwgan_ljspeech', 'pwgan_aishell3', 'pwgan_vctk', + 'mb_melgan_csmsc', 'wavernn_csmsc', 'hifigan_csmsc', + 'hifigan_ljspeech', 'hifigan_aishell3', 'hifigan_vctk', + 'style_melgan_csmsc' + ], + help='Choose vocoder type of tts task.') + parser.add_argument( + '--voc_config', + type=str, + default=None, + help='Config of voc. Use deault config when it is None.') + parser.add_argument( + '--voc_ckpt', type=str, default=None, help='Checkpoint file of voc.') + parser.add_argument( + "--voc_stat", + type=str, + default=None, + help="mean and standard deviation used to normalize spectrogram when training voc." + ) + # other + parser.add_argument( + '--lang', + type=str, + default='en', + help='Choose model language. zh or en') + + parser.add_argument( + "--ngpu", type=int, default=1, help="if ngpu == 0, use cpu.") + parser.add_argument("--test_metadata", type=str, help="test metadata.") + parser.add_argument("--output_dir", type=str, help="output dir.") + + parser.add_argument("--model_name", type=str, help="model name") + parser.add_argument("--uid", type=str, help="uid") + parser.add_argument("--new_str", type=str, help="new string") + parser.add_argument("--prefix", type=str, help="prefix") + parser.add_argument("--clone_prefix", type=str, default=None, help="clone prefix") + parser.add_argument("--clone_uid", type=str, default=None, help="clone uid") + parser.add_argument("--source_language", type=str, help="source language") + parser.add_argument("--target_language", type=str, help="target language") + parser.add_argument("--output_name", type=str, help="output name") + parser.add_argument("--task_name", type=str, help="task name") + + + # pre + args = parser.parse_args() + return args \ No newline at end of file diff --git a/ernie-sat/sedit_inference_0520.py b/ernie-sat/sedit_inference_0520.py new file mode 100644 index 0000000..09ca3e5 --- /dev/null +++ b/ernie-sat/sedit_inference_0520.py @@ -0,0 +1,1086 @@ +#!/usr/bin/env python3 + +"""Script to run the inference of text-to-speeech model.""" + +import os +os.environ["CUDA_VISIBLE_DEVICES"] = "3" + +from parallel_wavegan.utils import download_pretrained_model +from pathlib import Path +import paddle +import soundfile +import os +import math +import string +import numpy as np + +from espnet2.tasks.mlm import MLMTask +from read_text import read_2column_text,load_num_sequence_text +from util import sentence2phns,get_voc_out, evaluate_durations +import librosa +import random +from ipywidgets import widgets +import IPython.display as ipd +import soundfile as sf +import sys +import pickle +from model_paddle import build_model_from_file + +from sedit_arg_parser import parse_args +import argparse +from typing import Collection +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union + +from paddlespeech.t2s.datasets.get_feats import LogMelFBank +from paddlespeech.t2s.modules.nets_utils import make_non_pad_mask + +duration_path_dict = { + "ljspeech":"/mnt/home/v_baihe/projects/espnet/egs2/ljspeech/tts1/exp/kan-bayashi/ljspeech_tts_train_conformer_fastspeech2_raw_phn_tacotron_g2p_en_no_space_train.loss.ave/train.loss.ave_5best.pth", + "vctk": "/mnt/home/v_baihe/projects/espnet/egs2/vctk/tts1/exp/kan-bayashi/vctk_tts_train_gst+xvector_conformer_fastspeech2_transformer_teacher_raw_phn_tacotron_g2p_en_no_space_train.loss.ave/train.loss.ave_5best.pth", + # "ljspeech":"/home/mnt2/zz/workspace/work/espnet_richard_infer/egs2/ljspeech/tts1/exp/kan-bayashi/ljspeech_tts_train_conformer_fastspeech2_raw_phn_tacotron_g2p_en_no_space_train.loss.ave/train.loss.ave_5best.pth", + # "vctk": "/home/mnt2/zz/workspace/work/espnet_richard_infer/egs2/vctk/tts1/exp/kan-bayashi/vctk_tts_train_gst+xvector_conformer_fastspeech2_transformer_teacher_raw_phn_tacotron_g2p_en_no_space_train.loss.ave/train.loss.ave_5best.pth", + "vctk_unseen":"/mnt/home/v_baihe/projects/espnet/egs2/vctk/tts1/exp/tts_train_fs2_raw_phn_tacotron_g2p_en_no_space/train.loss.ave_5best.pth", + "libritts":"/mnt/home/v_baihe/projects/espnet/egs2/libritts/tts1/exp/kan-bayashi/libritts_tts_train_gst+xvector_conformer_fastspeech2_transformer_teacher_raw_phn_tacotron_g2p_en_no_space_train.loss/train.loss.ave_5best.pth" +} + +random.seed(0) +np.random.seed(0) + + +def plot_mel_and_vocode_wav(uid, prefix, clone_uid, clone_prefix, source_language, target_language, model_name, wav_path,full_origin_str, old_str, new_str, vocoder,duration_preditor_path,sid=None, non_autoreg=True): + wav_org, input_feat, output_feat, old_span_boundary, new_span_boundary, fs, hop_length = get_mlm_output( + uid, + prefix, + clone_uid, + clone_prefix, + source_language, + target_language, + model_name, + wav_path, + old_str, + new_str, + duration_preditor_path, + use_teacher_forcing=non_autoreg, + sid=sid + ) + + masked_feat = output_feat[new_span_boundary[0]:new_span_boundary[1]].detach().float().cpu().numpy() + + if target_language == 'english': + output_feat_np = output_feat.detach().float().cpu().numpy() + replaced_wav_paddle_voc = get_voc_out(output_feat_np, target_language) + + elif target_language == 'chinese': + output_feat_np = output_feat.detach().float().cpu().numpy() + replaced_wav_only_mask_fst2_voc = get_voc_out(masked_feat, target_language) + + + old_time_boundary = [hop_length * x for x in old_span_boundary] + new_time_boundary = [hop_length * x for x in new_span_boundary] + + + if target_language == 'english': + wav_org_replaced_paddle_voc = np.concatenate([wav_org[:old_time_boundary[0]], replaced_wav_paddle_voc[new_time_boundary[0]:new_time_boundary[1]], wav_org[old_time_boundary[1]:]]) + + data_dict = {"origin":wav_org, + "output":wav_org_replaced_paddle_voc} + + elif target_language == 'chinese': + wav_org_replaced_only_mask_fst2_voc = np.concatenate([wav_org[:old_time_boundary[0]], replaced_wav_only_mask_fst2_voc, wav_org[old_time_boundary[1]:]]) + data_dict = {"origin":wav_org, + "output": wav_org_replaced_only_mask_fst2_voc,} + + return data_dict, old_span_boundary + + + +def load_vocoder(vocoder_tag="parallel_wavegan/libritts_parallel_wavegan.v1"): + vocoder_tag = vocoder_tag.replace("parallel_wavegan/", "") + vocoder_file = download_pretrained_model(vocoder_tag) + vocoder_config = Path(vocoder_file).parent / "config.yml" + + vocoder = TTSTask.build_vocoder_from_file( + vocoder_config, vocoder_file, None, 'cpu' + ) + return vocoder + +def load_model(model_name): + config_path='./pretrained_model/{}/config.yaml'.format(model_name) + model_path = './pretrained_model/{}/model.pdparams'.format(model_name) + + mlm_model, args = build_model_from_file(config_file=config_path, + model_file=model_path) + return mlm_model, args + + +def read_data(uid,prefix): + mfa_text = read_2column_text(prefix+'/text')[uid] + mfa_wav_path = read_2column_text(prefix+'/wav.scp')[uid] + if 'mnt' not in mfa_wav_path: + mfa_wav_path = prefix.split('dump')[0] + mfa_wav_path + return mfa_text, mfa_wav_path + +def get_align_data(uid,prefix): + mfa_path = prefix+"mfa_" + mfa_text = read_2column_text(mfa_path+'text')[uid] + mfa_start = load_num_sequence_text(mfa_path+'start',loader_type='text_float')[uid] + mfa_end = load_num_sequence_text(mfa_path+'end',loader_type='text_float')[uid] + mfa_wav_path = read_2column_text(mfa_path+'wav.scp')[uid] + return mfa_text, mfa_start, mfa_end, mfa_wav_path + + + +def get_fs2_model(model_name): + model, config = TTSTask.build_model_from_file(model_file=model_name) + processor = TTSTask.build_preprocess_fn(config, train=False) + return model, processor + +def get_masked_mel_boundary(mfa_start, mfa_end, fs, hop_length, span_tobe_replaced): + align_start=paddle.to_tensor(mfa_start).unsqueeze(0) + align_end =paddle.to_tensor(mfa_end).unsqueeze(0) + align_start = paddle.floor(fs*align_start/hop_length).int() + align_end = paddle.floor(fs*align_end/hop_length).int() + if span_tobe_replaced[0]>=len(mfa_start): + span_boundary = [align_end[0].tolist()[-1],align_end[0].tolist()[-1]] + else: + span_boundary=[align_start[0].tolist()[span_tobe_replaced[0]],align_end[0].tolist()[span_tobe_replaced[1]-1]] + return span_boundary + + +def get_mapping(phn_mapping="./phn_mapping.txt"): + zh_mapping = {} + with open(phn_mapping, "r") as f: + for line in f: + pd_phn = line.split(" ")[0] + if pd_phn not in zh_mapping.keys(): + zh_mapping[pd_phn] = " ".join(line.split()[1:]) + return zh_mapping + + +def gen_phns(zh_mapping, phns): + new_phns = [] + for x in phns: + if x in zh_mapping.keys(): + new_phns.extend(zh_mapping[x].split(" ")) + else: + new_phns.extend(['']) + return new_phns + +def get_phns_and_spans_paddle(uid, prefix, old_str, new_str, source_language, target_language): + zh_mapping = get_mapping() + old_str = old_str.strip() + new_str = new_str.strip() + words = [] + for pun in [',', '.', ':', ';', '!', '?', '"', '(', ')', '--', '---', u',', u'。', u':', u';', u'!', u'?', u'(', u')']: + old_str = old_str.replace(pun, ' ') + new_str = new_str.replace(pun, ' ') + + + append_new_str = (old_str == new_str[:len(old_str)]) + print("append_new_str: ", append_new_str) + old_phns, mfa_start, mfa_end = [], [], [] + mfa_text, mfa_start, mfa_end, mfa_wav_path = get_align_data(uid, prefix) + old_phns = mfa_text.split(" ") + + if append_new_str: + if source_language != target_language: + is_cross_lingual = True + else: + is_cross_lingual = False + + new_str_origin = new_str[:len(old_str)] + new_str_append = new_str[len(old_str):] + if is_cross_lingual: + if source_language == "english" and target_language == "chinese": + new_phns_origin = old_phns + new_phns_append, _ = sentence2phns(new_str_append, "zh") + + elif source_language=="chinese" and target_language == "english": + new_phns_origin = old_phns + new_phns_append, _ = sentence2phns(new_str_append, "en") + else: + assert target_language == "chinese" or target_language == "english", "cloning is not support for this language, please check it." + + else: + if source_language == target_language and target_language == "english": + new_phns_origin = old_phns + new_phns_append, _ = sentence2phns(new_str_append, "en") + + elif source_language == target_language and target_language == "chinese": + new_phns_origin = old_phns + new_phns_append, _ = sentence2phns(new_str_append, "zh") + else: + assert source_language == target_language, "source language is not same with target language..." + + if target_language == "chinese": + new_phns_append = gen_phns(zh_mapping, new_phns_append) + + new_phns = new_phns_origin + new_phns_append + + span_tobe_replaced = [len(old_phns),len(old_phns)] + span_tobe_added = [len(old_phns),len(new_phns)] + + else: + if source_language == target_language and target_language == "english": + new_phns, _ = sentence2phns(new_str, "en") + # 纯中文 + elif source_language == target_language and target_language == "chinese": + new_phns, _ = sentence2phns(new_str, "zh") + new_phns = gen_phns(zh_mapping, new_phns) + + + else: + assert source_language == target_language, "source language is not same with target language..." + + while(new_phns[-1] == 'sp'): + new_phns.pop() + + while(new_phns[0] == 'sp'): + new_phns.pop(0) + + span_tobe_replaced = [0,len(old_phns)-1] + span_tobe_added = [0,len(new_phns)-1] + new_phns_left = [] + left_index = 0 + sp_count = 0 + + # find the left different index + for idx, phn in enumerate(old_phns): + if phn == "sp": + sp_count += 1 + new_phns_left.append('sp') + else: + idx = idx - sp_count + if phn == new_phns[idx]: + left_index += 1 + new_phns_left.append(phn) + else: + span_tobe_replaced[0] = len(new_phns_left) + span_tobe_added[0] = len(new_phns_left) + break + + right_index = 0 + new_phns_middle = [] + new_phns_right = [] + sp_count = 0 + word2phns_max_index = len(old_phns) + new_word2phns_max_index = len(new_phns) + + for idx, phn in enumerate(old_phns[::-1]): + cur_idx = len(old_phns) - 1 - idx + if phn == "sp": + sp_count += 1 + new_phns_right = ['sp']+new_phns_right + else: + cur_idx = new_word2phns_max_index - (word2phns_max_index - cur_idx -sp_count) + if phn == new_phns[cur_idx]: + right_index -= 1 + new_phns_right = [phn] + new_phns_right + + else: + span_tobe_replaced[1] = len(old_phns) - len(new_phns_right) + new_phns_middle = new_phns[left_index:right_index] + span_tobe_added[1] = len(new_phns_left) + len(new_phns_middle) + if len(new_phns_middle) == 0: + span_tobe_added[1] = min(span_tobe_added[1]+1, len(new_phns)) + span_tobe_added[0] = max(0, span_tobe_added[0]-1) + span_tobe_replaced[0] = max(0, span_tobe_replaced[0]-1) + span_tobe_replaced[1] = min(span_tobe_replaced[1]+1, len(old_phns)) + break + + new_phns = new_phns_left+new_phns_middle+new_phns_right + + return mfa_start, mfa_end, old_phns, new_phns, span_tobe_replaced, span_tobe_added + + + + +def duration_adjust_factor(original_dur, pred_dur, phns): + length = 0 + accumulate = 0 + factor_list = [] + for ori,pred,phn in zip(original_dur, pred_dur,phns): + if pred==0 or phn=='sp': + continue + else: + factor_list.append(ori/pred) + factor_list = np.array(factor_list) + factor_list.sort() + if len(factor_list)<5: + return 1 + length = 2 + return np.average(factor_list[length:-length]) + + +def prepare_features_with_duration(uid, prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model, old_str, new_str, wav_path,duration_preditor_path,sid=None, mask_reconstruct=False,duration_adjust=True,start_end_sp=False, train_args=None): + wav_org, rate = librosa.load(wav_path, sr=train_args.feats_extract_conf['fs']) + fs = train_args.feats_extract_conf['fs'] + hop_length = train_args.feats_extract_conf['hop_length'] + + mfa_start, mfa_end, old_phns, new_phns, span_tobe_replaced, span_tobe_added = get_phns_and_spans_paddle(uid, prefix, old_str, new_str, source_language, target_language) + + if start_end_sp: + if new_phns[-1]!='sp': + new_phns = new_phns+['sp'] + + + if target_language == "english": + old_durations = evaluate_durations(old_phns, target_language=target_language) + + elif target_language =="chinese": + if source_language == "english": + old_durations = evaluate_durations(old_phns, target_language=source_language) + elif source_language == "chinese": + old_durations = evaluate_durations(old_phns, target_language=source_language) + + else: + assert target_language == "chinese" or target_language == "english", "calculate duration_predict is not support for this language..." + + + + original_old_durations = [e-s for e,s in zip(mfa_end, mfa_start)] + if '[MASK]' in new_str: + new_phns = old_phns + span_tobe_added = span_tobe_replaced + d_factor_left = duration_adjust_factor(original_old_durations[:span_tobe_replaced[0]],old_durations[:span_tobe_replaced[0]], old_phns[:span_tobe_replaced[0]]) + d_factor_right = duration_adjust_factor(original_old_durations[span_tobe_replaced[1]:],old_durations[span_tobe_replaced[1]:], old_phns[span_tobe_replaced[1]:]) + d_factor = (d_factor_left+d_factor_right)/2 + new_durations_adjusted = [d_factor*i for i in old_durations] + else: + if duration_adjust: + d_factor = duration_adjust_factor(original_old_durations,old_durations, old_phns) + d_factor_paddle = duration_adjust_factor(original_old_durations,old_durations, old_phns) + if target_language =="chinese": + d_factor = d_factor * 1.35 + else: + d_factor = 1 + + if target_language == "english": + new_durations = evaluate_durations(new_phns, target_language=target_language) + + + elif target_language =="chinese": + new_durations = evaluate_durations(new_phns, target_language=target_language) + + new_durations_adjusted = [d_factor*i for i in new_durations] + + if span_tobe_replaced[0]=len(mfa_start): + left_index = len(wav_org) + right_index = left_index + else: + left_index = int(np.floor(mfa_start[span_tobe_replaced[0]]*fs)) + right_index = int(np.ceil(mfa_end[span_tobe_replaced[1]-1]*fs)) + new_blank_wav = np.zeros((int(np.ceil(new_span_duration_sum*fs)),), dtype=wav_org.dtype) + new_wav_org = np.concatenate([wav_org[:left_index], new_blank_wav, wav_org[right_index:]]) + + + # 4. get old and new mel span to be mask + old_span_boundary = get_masked_mel_boundary(mfa_start, mfa_end, fs, hop_length, span_tobe_replaced) # [92, 92] + new_span_boundary=get_masked_mel_boundary(new_mfa_start, new_mfa_end, fs, hop_length, span_tobe_added) # [92, 174] + + + return new_wav_org, new_phns, new_mfa_start, new_mfa_end, old_span_boundary, new_span_boundary + +def prepare_features(uid, prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model,processor, wav_path, old_str,new_str,duration_preditor_path, sid=None,duration_adjust=True,start_end_sp=False, +mask_reconstruct=False, train_args=None): + wav_org, phns_list, mfa_start, mfa_end, old_span_boundary, new_span_boundary = prepare_features_with_duration(uid, prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model, old_str, + new_str, wav_path,duration_preditor_path,sid=sid,duration_adjust=duration_adjust,start_end_sp=start_end_sp,mask_reconstruct=mask_reconstruct, train_args = train_args) + speech = np.array(wav_org,dtype=np.float32) + align_start=np.array(mfa_start) + align_end =np.array(mfa_end) + token_to_id = {item: i for i, item in enumerate(train_args.token_list)} + text = np.array(list(map(lambda x: token_to_id.get(x, token_to_id['']), phns_list))) + print('unk id is', token_to_id['']) + # text = np.array(processor(uid='1', data={'text':" ".join(phns_list)})['text']) + span_boundary = np.array(new_span_boundary) + batch=[('1', {"speech":speech,"align_start":align_start,"align_end":align_end,"text":text,"span_boundary":span_boundary})] + + return batch, old_span_boundary, new_span_boundary + +def decode_with_model(uid, prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model, processor, collate_fn, wav_path, old_str, new_str,duration_preditor_path, sid=None, decoder=False,use_teacher_forcing=False,duration_adjust=True,start_end_sp=False, train_args=None): + # fs, hop_length = mlm_model.feats_extract.fs, mlm_model.feats_extract.hop_length + fs, hop_length = train_args.feats_extract_conf['fs'], train_args.feats_extract_conf['hop_length'] + + batch,old_span_boundary,new_span_boundary = prepare_features(uid,prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model,processor,wav_path,old_str,new_str,duration_preditor_path, sid,duration_adjust=duration_adjust,start_end_sp=start_end_sp, train_args=train_args) + + feats = pickle.load(open('tmp/tmp_pkl.'+str(uid), 'rb')) + + # wav_len * 80 + # set_all_random_seed(9999) + if 'text_masked_position' in feats.keys(): + feats.pop('text_masked_position') + for k, v in feats.items(): + feats[k] = paddle.to_tensor(v) + rtn = mlm_model.inference(**feats,span_boundary=new_span_boundary,use_teacher_forcing=use_teacher_forcing) + output = rtn['feat_gen'] + if 0 in output[0].shape and 0 not in output[-1].shape: + output_feat = paddle.concat(output[1:-1]+[output[-1].squeeze()], axis=0).cpu() + elif 0 not in output[0].shape and 0 in output[-1].shape: + output_feat = paddle.concat([output[0].squeeze()]+output[1:-1], axis=0).cpu() + elif 0 in output[0].shape and 0 in output[-1].shape: + output_feat = paddle.concat(output[1:-1], axis=0).cpu() + else: + output_feat = paddle.concat([output[0].squeeze(0)]+ output[1:-1]+[output[-1].squeeze(0)], axis=0).cpu() + + + # wav_org, rate = soundfile.read( + # wav_path, always_2d=False) + wav_org, rate = librosa.load(wav_path, sr=train_args.feats_extract_conf['fs']) + origin_speech = paddle.to_tensor(np.array(wav_org,dtype=np.float32)).unsqueeze(0) + speech_lengths = paddle.to_tensor(len(wav_org)).unsqueeze(0) + # input_feat, feats_lengths = mlm_model.feats_extract(origin_speech, speech_lengths) + # return wav_org, input_feat.squeeze(), output_feat, old_span_boundary, new_span_boundary, fs, hop_length + return wav_org, None, output_feat, old_span_boundary, new_span_boundary, fs, hop_length + +class MLMCollateFn: + """Functor class of common_collate_fn()""" + + def __init__( + self, + feats_extract, + float_pad_value: Union[float, int] = 0.0, + int_pad_value: int = -32768, + not_sequence: Collection[str] = (), + mlm_prob: float=0.8, + mean_phn_span: int=8, + attention_window: int=0, + pad_speech: bool=False, + sega_emb: bool=False, + duration_collect: bool=False, + text_masking: bool=False + + ): + self.mlm_prob=mlm_prob + self.mean_phn_span=mean_phn_span + self.feats_extract = feats_extract + self.float_pad_value = float_pad_value + self.int_pad_value = int_pad_value + self.not_sequence = set(not_sequence) + self.attention_window=attention_window + self.pad_speech=pad_speech + self.sega_emb=sega_emb + self.duration_collect = duration_collect + self.text_masking = text_masking + + def __repr__(self): + return ( + f"{self.__class__}(float_pad_value={self.float_pad_value}, " + f"int_pad_value={self.float_pad_value})" + ) + + def __call__( + self, data: Collection[Tuple[str, Dict[str, np.ndarray]]] + ) -> Tuple[List[str], Dict[str, paddle.Tensor]]: + return mlm_collate_fn( + data, + float_pad_value=self.float_pad_value, + int_pad_value=self.int_pad_value, + not_sequence=self.not_sequence, + mlm_prob=self.mlm_prob, + mean_phn_span=self.mean_phn_span, + feats_extract=self.feats_extract, + attention_window=self.attention_window, + pad_speech=self.pad_speech, + sega_emb=self.sega_emb, + duration_collect=self.duration_collect, + text_masking=self.text_masking + ) + +def pad_list(xs, pad_value): + """Perform padding for the list of tensors. + + Args: + xs (List): List of Tensors [(T_1, `*`), (T_2, `*`), ..., (T_B, `*`)]. + pad_value (float): Value for padding. + + Returns: + Tensor: Padded tensor (B, Tmax, `*`). + + Examples: + >>> x = [paddle.ones(4), paddle.ones(2), paddle.ones(1)] + >>> x + [tensor([1., 1., 1., 1.]), tensor([1., 1.]), tensor([1.])] + >>> pad_list(x, 0) + tensor([[1., 1., 1., 1.], + [1., 1., 0., 0.], + [1., 0., 0., 0.]]) + + """ + n_batch = len(xs) + max_len = max(paddle.shape(x)[0] for x in xs) + pad = paddle.full((n_batch, max_len), pad_value, dtype = xs[0].dtype) + + for i in range(n_batch): + pad[i, : paddle.shape(xs[i])[0]] = xs[i] + + return pad + +def pad_to_longformer_att_window(text, max_len, max_tlen,attention_window): + round = max_len % attention_window + if round != 0: + max_tlen += (attention_window - round) + n_batch = paddle.shape(text)[0] + text_pad = paddle.zeros((n_batch, max_tlen, *paddle.shape(text[0])[1:]), dtype=text.dtype) + for i in range(n_batch): + text_pad[i, : paddle.shape(text[i])[0]] = text[i] + else: + text_pad = text[:, : max_tlen] + return text_pad, max_tlen + +def make_pad_mask(lengths, xs=None, length_dim=-1): + print('inputs are:', lengths, xs, length_dim) + """Make mask tensor containing indices of padded part. + + Args: + lengths (LongTensor or List): Batch of lengths (B,). + xs (Tensor, optional): The reference tensor. + If set, masks will be the same shape as this tensor. + length_dim (int, optional): Dimension indicator of the above tensor. + See the example. + + Returns: + Tensor: Mask tensor containing indices of padded part. + + Examples: + With only lengths. + + >>> lengths = [5, 3, 2] + >>> make_non_pad_mask(lengths) + masks = [[0, 0, 0, 0 ,0], + [0, 0, 0, 1, 1], + [0, 0, 1, 1, 1]] + + With the reference tensor. + + >>> xs = paddle.zeros((3, 2, 4)) + >>> make_pad_mask(lengths, xs) + tensor([[[0, 0, 0, 0], + [0, 0, 0, 0]], + [[0, 0, 0, 1], + [0, 0, 0, 1]], + [[0, 0, 1, 1], + [0, 0, 1, 1]]], dtype=paddle.uint8) + >>> xs = paddle.zeros((3, 2, 6)) + >>> make_pad_mask(lengths, xs) + tensor([[[0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]], + [[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], + [[0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1]]], dtype=paddle.uint8) + + With the reference tensor and dimension indicator. + + >>> xs = paddle.zeros((3, 6, 6)) + >>> make_pad_mask(lengths, xs, 1) + tensor([[[0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1]], + [[0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1]], + [[0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1]]], dtype=paddle.uint8) + >>> make_pad_mask(lengths, xs, 2) + tensor([[[0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 1]], + [[0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1], + [0, 0, 0, 1, 1, 1]], + [[0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1]]], dtype=paddle.uint8) + + """ + if length_dim == 0: + raise ValueError("length_dim cannot be 0: {}".format(length_dim)) + + if not isinstance(lengths, list): + lengths = list(lengths) + print('lengths', lengths) + bs = int(len(lengths)) + if xs is None: + maxlen = int(max(lengths)) + else: + maxlen = paddle.shape(xs)[length_dim] + + seq_range = paddle.arange(0, maxlen, dtype=paddle.int64) + seq_range_expand = paddle.expand(paddle.unsqueeze(seq_range, 0), (bs, maxlen)) + seq_length_expand = paddle.unsqueeze(paddle.to_tensor(lengths), -1) + print('seq_length_expand', paddle.shape(seq_length_expand)) + print('seq_range_expand', paddle.shape(seq_range_expand)) + mask = seq_range_expand >= seq_length_expand + + if xs is not None: + assert paddle.shape(xs)[0] == bs, (paddle.shape(xs)[0], bs) + + if length_dim < 0: + length_dim = len(paddle.shape(xs)) + length_dim + # ind = (:, None, ..., None, :, , None, ..., None) + ind = tuple( + slice(None) if i in (0, length_dim) else None for i in range(len(paddle.shape(xs))) + ) + print('0:', paddle.shape(mask)) + print('1:', paddle.shape(mask[ind])) + print('2:', paddle.shape(xs)) + mask = paddle.expand(mask[ind], paddle.shape(xs)) + return mask + + +def make_non_pad_mask(lengths, xs=None, length_dim=-1): + """Make mask tensor containing indices of non-padded part. + + Args: + lengths (LongTensor or List): Batch of lengths (B,). + xs (Tensor, optional): The reference tensor. + If set, masks will be the same shape as this tensor. + length_dim (int, optional): Dimension indicator of the above tensor. + See the example. + + Returns: + ByteTensor: mask tensor containing indices of padded part. + + Examples: + With only lengths. + + >>> lengths = [5, 3, 2] + >>> make_non_pad_mask(lengths) + masks = [[1, 1, 1, 1 ,1], + [1, 1, 1, 0, 0], + [1, 1, 0, 0, 0]] + + With the reference tensor. + + >>> xs = paddle.zeros((3, 2, 4)) + >>> make_non_pad_mask(lengths, xs) + tensor([[[1, 1, 1, 1], + [1, 1, 1, 1]], + [[1, 1, 1, 0], + [1, 1, 1, 0]], + [[1, 1, 0, 0], + [1, 1, 0, 0]]], dtype=paddle.uint8) + >>> xs = paddle.zeros((3, 2, 6)) + >>> make_non_pad_mask(lengths, xs) + tensor([[[1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0]], + [[1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0]], + [[1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0]]], dtype=paddle.uint8) + + With the reference tensor and dimension indicator. + + >>> xs = paddle.zeros((3, 6, 6)) + >>> make_non_pad_mask(lengths, xs, 1) + tensor([[[1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0]], + [[1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0]], + [[1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0]]], dtype=paddle.uint8) + >>> make_non_pad_mask(lengths, xs, 2) + tensor([[[1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 0]], + [[1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0], + [1, 1, 1, 0, 0, 0]], + [[1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0]]], dtype=paddle.uint8) + + """ + return ~make_pad_mask(lengths, xs, length_dim) + +def phones_masking(xs_pad, src_mask, align_start, align_end, align_start_lengths, mlm_prob, mean_phn_span, span_boundary=None): + bz, sent_len, _ = paddle.shape(xs_pad) + mask_num_lower = math.ceil(sent_len * mlm_prob) + masked_position = np.zeros((bz, sent_len)) + y_masks = None + # y_masks = torch.ones(bz,sent_len,sent_len,device=xs_pad.device,dtype=xs_pad.dtype) + # tril_masks = torch.tril(y_masks) + if mlm_prob == 1.0: + masked_position += 1 + # y_masks = tril_masks + elif mean_phn_span == 0: + # only speech + length = sent_len + mean_phn_span = min(length*mlm_prob//3, 50) + masked_phn_indices = random_spans_noise_mask(length,mlm_prob, mean_phn_span).nonzero() + masked_position[:,masked_phn_indices]=1 + else: + for idx in range(bz): + if span_boundary is not None: + for s,e in zip(span_boundary[idx][::2], span_boundary[idx][1::2]): + masked_position[idx, s:e] = 1 + + # y_masks[idx, :, s:e] = tril_masks[idx, :, s:e] + # y_masks[idx, e:, s:e ] = 0 + else: + length = align_start_lengths[idx].item() + if length<2: + continue + masked_phn_indices = random_spans_noise_mask(length,mlm_prob, mean_phn_span).nonzero() + masked_start = align_start[idx][masked_phn_indices].tolist() + masked_end = align_end[idx][masked_phn_indices].tolist() + for s,e in zip(masked_start, masked_end): + masked_position[idx, s:e] = 1 + # y_masks[idx, :, s:e] = tril_masks[idx, :, s:e] + # y_masks[idx, e:, s:e ] = 0 + non_eos_mask = np.array(paddle.reshape(src_mask, paddle.shape(xs_pad)[:2]).float().cpu()) + masked_position = masked_position * non_eos_mask + # y_masks = src_mask & y_masks.bool() + + return paddle.cast(paddle.to_tensor(masked_position), paddle.bool), y_masks + +def get_segment_pos(speech_pad, text_pad, align_start, align_end, align_start_lengths,sega_emb): + bz, speech_len, _ = speech_pad.size() + text_segment_pos = paddle.zeros_like(text_pad) + speech_segment_pos = paddle.zeros((bz, speech_len),dtype=text_pad.dtype) + if not sega_emb: + return speech_segment_pos, text_segment_pos + for idx in range(bz): + align_length = align_start_lengths[idx].item() + for j in range(align_length): + s,e = align_start[idx][j].item(), align_end[idx][j].item() + speech_segment_pos[idx][s:e] = j+1 + text_segment_pos[idx][j] = j+1 + + return speech_segment_pos, text_segment_pos + +def mlm_collate_fn( + data: Collection[Tuple[str, Dict[str, np.ndarray]]], + float_pad_value: Union[float, int] = 0.0, + int_pad_value: int = -32768, + not_sequence: Collection[str] = (), + mlm_prob: float = 0.8, + mean_phn_span: int = 8, + feats_extract=None, + attention_window: int = 0, + pad_speech: bool=False, + sega_emb: bool=False, + duration_collect: bool=False, + text_masking: bool=False +) -> Tuple[List[str], Dict[str, paddle.Tensor]]: + """Concatenate ndarray-list to an array and convert to paddle.Tensor. + + Examples: + >>> from espnet2.samplers.constant_batch_sampler import ConstantBatchSampler, + >>> import espnet2.tasks.abs_task + >>> from espnet2.train.dataset import ESPnetDataset + >>> sampler = ConstantBatchSampler(...) + >>> dataset = ESPnetDataset(...) + >>> keys = next(iter(sampler) + >>> batch = [dataset[key] for key in keys] + >>> batch = common_collate_fn(batch) + >>> model(**batch) + + Note that the dict-keys of batch are propagated from + that of the dataset as they are. + + """ + uttids = [u for u, _ in data] + data = [d for _, d in data] + + assert all(set(data[0]) == set(d) for d in data), "dict-keys mismatching" + assert all( + not k.endswith("_lengths") for k in data[0] + ), f"*_lengths is reserved: {list(data[0])}" + + output = {} + for key in data[0]: + # NOTE(kamo): + # Each models, which accepts these values finally, are responsible + # to repaint the pad_value to the desired value for each tasks. + if data[0][key].dtype.kind == "i": + pad_value = int_pad_value + else: + pad_value = float_pad_value + + array_list = [d[key] for d in data] + + # Assume the first axis is length: + # tensor_list: Batch x (Length, ...) + tensor_list = [paddle.to_tensor(a) for a in array_list] + # tensor: (Batch, Length, ...) + tensor = pad_list(tensor_list, pad_value) + output[key] = tensor + + # lens: (Batch,) + if key not in not_sequence: + lens = paddle.to_tensor([d[key].shape[0] for d in data], dtype=paddle.long) + output[key + "_lengths"] = lens + + f = open('tmp_var.out', 'w') + for item in [round(item, 6) for item in output["speech"][0].tolist()]: + f.write(str(item)+'\n') + feats = feats_extract.get_log_mel_fbank(np.array(output["speech"][0])) + feats = paddle.to_tensor(feats) + print('out shape', paddle.shape(feats)) + feats_lengths = paddle.shape(feats)[0] + feats = paddle.unsqueeze(feats, 0) + batch_size = paddle.shape(feats)[0] + if 'text' not in output: + text=paddle.zeros_like(feats_lengths.unsqueeze(-1))-2 + text_lengths=paddle.zeros_like(feats_lengths)+1 + max_tlen=1 + align_start=paddle.zeros_like(text) + align_end=paddle.zeros_like(text) + align_start_lengths=paddle.zeros_like(feats_lengths) + align_end_lengths=paddle.zeros_like(feats_lengths) + sega_emb=False + mean_phn_span = 0 + mlm_prob = 0.15 + else: + text, text_lengths = output["text"], output["text_lengths"] + align_start, align_start_lengths, align_end, align_end_lengths = output["align_start"], output["align_start_lengths"], output["align_end"], output["align_end_lengths"] + align_start = paddle.floor(feats_extract.sr*align_start/feats_extract.hop_length).int() + align_end = paddle.floor(feats_extract.sr*align_end/feats_extract.hop_length).int() + max_tlen = max(text_lengths).item() + max_slen = max(feats_lengths).item() + speech_pad = feats[:, : max_slen] + if attention_window>0 and pad_speech: + speech_pad,max_slen = pad_to_longformer_att_window(speech_pad, max_slen, max_slen, attention_window) + max_len = max_slen + max_tlen + if attention_window>0: + text_pad, max_tlen = pad_to_longformer_att_window(text, max_len, max_tlen, attention_window) + else: + text_pad = text + text_mask = make_non_pad_mask(text_lengths.tolist(), text_pad, length_dim=1).unsqueeze(-2) + if attention_window>0: + text_mask = text_mask*2 + speech_mask = make_non_pad_mask(feats_lengths.tolist(), speech_pad[:,:,0], length_dim=1).unsqueeze(-2) + span_boundary = None + if 'span_boundary' in output.keys(): + span_boundary = output['span_boundary'] + + if text_masking: + masked_position, text_masked_position,_ = phones_text_masking( + speech_pad, + speech_mask, + text_pad, + text_mask, + align_start, + align_end, + align_start_lengths, + mlm_prob, + mean_phn_span, + span_boundary) + else: + text_masked_position = np.zeros(text_pad.size()) + masked_position, _ = phones_masking( + speech_pad, + speech_mask, + align_start, + align_end, + align_start_lengths, + mlm_prob, + mean_phn_span, + span_boundary) + + output_dict = {} + if duration_collect and 'text' in output: + reordered_index, speech_segment_pos,text_segment_pos, durations,feats_lengths = get_segment_pos_reduce_duration(speech_pad, text_pad, align_start, align_end, align_start_lengths,sega_emb, masked_position, feats_lengths) + speech_mask = make_non_pad_mask(feats_lengths.tolist(), speech_pad[:,:reordered_index.shape[1],0], length_dim=1).unsqueeze(-2) + output_dict['durations'] = durations + output_dict['reordered_index'] = reordered_index + else: + speech_segment_pos, text_segment_pos = get_segment_pos(speech_pad, text_pad, align_start, align_end, align_start_lengths,sega_emb) + output_dict['speech'] = speech_pad + output_dict['text'] = text_pad + output_dict['masked_position'] = masked_position + output_dict['text_masked_position'] = text_masked_position + output_dict['speech_mask'] = speech_mask + output_dict['text_mask'] = text_mask + output_dict['speech_segment_pos'] = speech_segment_pos + output_dict['text_segment_pos'] = text_segment_pos + # output_dict['y_masks'] = y_masks + output_dict['speech_lengths'] = output["speech_lengths"] + output_dict['text_lengths'] = text_lengths + output = (uttids, output_dict) + # assert check_return_type(output) + return output + +def build_collate_fn( + args: argparse.Namespace, train: bool, epoch=-1 + ): + + # assert check_argument_types() + # return CommonCollateFn(float_pad_value=0.0, int_pad_value=0) + feats_extract_class = LogMelFBank + args_dic = {} + print ('type is', type(args.feats_extract_conf)) + for k, v in args.feats_extract_conf.items(): + if k == 'fs': + args_dic['sr'] = v + else: + args_dic[k] = v + # feats_extract = feats_extract_class(**args.feats_extract_conf) + feats_extract = feats_extract_class(**args_dic) + + sega_emb = True if args.encoder_conf['input_layer'] == 'sega_mlm' else False + if args.encoder_conf['selfattention_layer_type'] == 'longformer': + attention_window = args.encoder_conf['attention_window'] + pad_speech = True if 'pre_speech_layer' in args.encoder_conf and args.encoder_conf['pre_speech_layer'] >0 else False + else: + attention_window=0 + pad_speech=False + if epoch==-1: + mlm_prob_factor = 1 + else: + mlm_probs = [1.0, 1.0, 0.7, 0.6, 0.5] + mlm_prob_factor = 0.8 #mlm_probs[epoch // 100] + if 'duration_predictor_layers' in args.model_conf.keys() and args.model_conf['duration_predictor_layers']>0: + duration_collect=True + else: + duration_collect=False + return MLMCollateFn(feats_extract, float_pad_value=0.0, int_pad_value=0, + mlm_prob=args.model_conf['mlm_prob']*mlm_prob_factor,mean_phn_span=args.model_conf['mean_phn_span'],attention_window=attention_window,pad_speech=pad_speech,sega_emb=sega_emb,duration_collect=duration_collect) + + +def get_mlm_output(uid, prefix, clone_uid, clone_prefix, source_language, target_language, model_name, wav_path, old_str, new_str,duration_preditor_path, sid=None, decoder=False,use_teacher_forcing=False, dynamic_eval=(0,0),duration_adjust=True,start_end_sp=False): + mlm_model,train_args = load_model(model_name) + mlm_model.eval() + # processor = MLMTask.build_preprocess_fn(train_args, False) + processor = None + collate_fn = MLMTask.build_collate_fn(train_args, False) + # collate_fn = build_collate_fn(train_args, False) + + return decode_with_model(uid,prefix, clone_uid, clone_prefix, source_language, target_language, mlm_model, processor, collate_fn, wav_path, old_str, new_str,duration_preditor_path, sid=sid, decoder=decoder, use_teacher_forcing=use_teacher_forcing, + duration_adjust=duration_adjust,start_end_sp=start_end_sp, train_args = train_args) + +def prompt_decoding_fn(model_name, wav_path,full_origin_str, old_str, new_str, vocoder,duration_preditor_path,sid=None, non_autoreg=True, dynamic_eval=(0,0),duration_adjust=True): + wav_org, input_feat, output_feat, old_span_boundary, new_span_boundary, fs, hop_length = get_mlm_output( + model_name, + wav_path, + old_str, + new_str, + duration_preditor_path, + use_teacher_forcing=non_autoreg, + sid=sid, + dynamic_eval=dynamic_eval, + duration_adjust=duration_adjust, + start_end_sp=False + ) + + replaced_wav = vocoder(output_feat).detach().float().data.cpu().numpy() + + old_time_boundary = [hop_length * x for x in old_span_boundary] + new_time_boundary = [hop_length * x for x in new_span_boundary] + new_wav = replaced_wav[new_time_boundary[0]:] + # "origin_vocoder":vocoder_origin_wav, + data_dict = {"prompt":wav_org, + "new_wav":new_wav} + return data_dict + +def test_vctk(uid, clone_uid, clone_prefix, source_language, target_language, vocoder, prefix='dump/raw/dev', model_name="conformer", old_str="",new_str="",prompt_decoding=False,dynamic_eval=(0,0), task_name = None): + + new_str = new_str.strip() + if clone_uid is not None and clone_prefix is not None: + if target_language == "english": + duration_preditor_path = duration_path_dict['ljspeech'] + elif target_language == "chinese": + duration_preditor_path = duration_path_dict['ljspeech'] + else: + assert target_language == "chinese" or target_language == "english", "duration_preditor_path is not support for this language..." + + else: + duration_preditor_path = duration_path_dict['ljspeech'] + + spemd = None + full_origin_str,wav_path = read_data(uid, prefix) + + new_str = new_str if task_name == 'edit' else full_origin_str + new_str + print('new_str is ', new_str) + + if not old_str: + old_str = full_origin_str + if not new_str: + new_str = input("input the new string:") + if prompt_decoding: + print(new_str) + return prompt_decoding_fn(model_name, wav_path,full_origin_str, old_str, new_str,vocoder,duration_preditor_path,sid=spemd,dynamic_eval=dynamic_eval) + print(full_origin_str) + results_dict, old_span = plot_mel_and_vocode_wav(uid, prefix, clone_uid, clone_prefix, source_language, target_language, model_name, wav_path,full_origin_str, old_str, new_str,vocoder,duration_preditor_path,sid=spemd) + return results_dict + +if __name__ == "__main__": + args = parse_args() + print(args) + data_dict = test_vctk(args.uid, + args.clone_uid, + args.clone_prefix, + args.source_language, + args.target_language, + None, + args.prefix, + args.model_name, + new_str=args.new_str, + task_name=args.task_name) + sf.write('./wavs/%s' % args.output_name, data_dict['output'], samplerate=24000) + \ No newline at end of file diff --git a/ernie-sat/tmp/tmp_pkl.Prompt_003_new b/ernie-sat/tmp/tmp_pkl.Prompt_003_new new file mode 100644 index 0000000000000000000000000000000000000000..c7432dac7c6f3261a37ffc994f852dd7ea89fcd8 GIT binary patch literal 66623 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 PU*UiMr2qW0k{tdQSisMF literal 0 HcmV?d00001 diff --git a/ernie-sat/tmp/tmp_pkl.p243_new b/ernie-sat/tmp/tmp_pkl.p243_new new file mode 100644 index 0000000000000000000000000000000000000000..33075eb1cf1c5ab94bd85538409ed0c23cc90e0a GIT binary patch literal 113242 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

    *MO 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%0DwFWF=q>z{n$Xq!1~1N5 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/util.py b/ernie-sat/util.py new file mode 100644 index 0000000..45f10e7 --- /dev/null +++ b/ernie-sat/util.py @@ -0,0 +1,239 @@ +# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import argparse +import logging +from pathlib import Path + +import jsonlines +import numpy as np +import paddle +import soundfile as sf +import yaml +from timer import timer +from yacs.config import CfgNode +from paddlespeech.s2t.utils.dynamic_import import dynamic_import + +from paddlespeech.t2s.exps.syn_utils import get_test_dataset +from paddlespeech.t2s.exps.syn_utils import get_voc_inference +from paddlespeech.t2s.utils import str2bool +from paddlespeech.t2s.frontend.zh_frontend import Frontend +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2 +from paddlespeech.t2s.models.fastspeech2 import FastSpeech2Inference +from paddlespeech.t2s.modules.normalizer import ZScore +from yacs.config import CfgNode +# new add +import paddle.nn.functional as F +from paddlespeech.t2s.modules.nets_utils import make_pad_mask +from paddlespeech.t2s.exps.syn_utils import get_frontend + +from sedit_arg_parser import parse_args + +model_alias = { + # acoustic model + "speedyspeech": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeech", + "speedyspeech_inference": + "paddlespeech.t2s.models.speedyspeech:SpeedySpeechInference", + "fastspeech2": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2", + "fastspeech2_inference": + "paddlespeech.t2s.models.fastspeech2:FastSpeech2Inference", + "tacotron2": + "paddlespeech.t2s.models.tacotron2:Tacotron2", + "tacotron2_inference": + "paddlespeech.t2s.models.tacotron2:Tacotron2Inference", +} + + + + + +def get_voc_out(mel, target_language="chinese"): + # vocoder + args = parse_args() + + + assert target_language == "chinese" or target_language == "english", "In get_voc_out function, target_language is illegal..." + + print("current vocoder: ", args.voc) + with open(args.voc_config) as f: + voc_config = CfgNode(yaml.safe_load(f)) + + voc_inference = get_voc_inference(args, voc_config) + + mel = paddle.to_tensor(mel) + with paddle.no_grad(): + wav = voc_inference(mel) + print("shepe of wav (time x n_channels):%s"%wav.shape) # (31800,1) + return np.squeeze(wav) + +# dygraph +def get_am_inference(args, am_config): + with open(args.phones_dict, "r") as f: + phn_id = [line.strip().split() for line in f.readlines()] + vocab_size = len(phn_id) + # print("vocab_size:", vocab_size) + + tone_size = None + if 'tones_dict' in args and args.tones_dict: + with open(args.tones_dict, "r") as f: + tone_id = [line.strip().split() for line in f.readlines()] + tone_size = len(tone_id) + print("tone_size:", tone_size) + + spk_num = None + if 'speaker_dict' in args and args.speaker_dict: + with open(args.speaker_dict, 'rt') as f: + spk_id = [line.strip().split() for line in f.readlines()] + spk_num = len(spk_id) + print("spk_num:", spk_num) + + odim = am_config.n_mels + # model: {model_name}_{dataset} + am_name = args.am[:args.am.rindex('_')] + am_dataset = args.am[args.am.rindex('_') + 1:] + + am_class = dynamic_import(am_name, model_alias) + am_inference_class = dynamic_import(am_name + '_inference', model_alias) + + if am_name == 'fastspeech2': + am = am_class( + idim=vocab_size, odim=odim, spk_num=spk_num, **am_config["model"]) + elif am_name == 'speedyspeech': + am = am_class( + vocab_size=vocab_size, + tone_size=tone_size, + spk_num=spk_num, + **am_config["model"]) + elif am_name == 'tacotron2': + am = am_class(idim=vocab_size, odim=odim, **am_config["model"]) + + am.set_state_dict(paddle.load(args.am_ckpt)["main_params"]) + am.eval() + am_mu, am_std = np.load(args.am_stat) + am_mu = paddle.to_tensor(am_mu) + am_std = paddle.to_tensor(am_std) + am_normalizer = ZScore(am_mu, am_std) + am_inference = am_inference_class(am_normalizer, am) + am_inference.eval() + print("acoustic model done!") + return am, am_inference, am_name, am_dataset, phn_id + + +def evaluate_durations(phns, target_language="chinese", fs=24000, hop_length=300): + args = parse_args() + if args.ngpu == 0: + paddle.set_device("cpu") + elif args.ngpu > 0: + paddle.set_device("gpu") + else: + print("ngpu should >= 0 !") + + + + assert target_language == "chinese" or target_language == "english", "In evaluate_durations function, target_language is illegal..." + + # Init body. + with open(args.am_config) as f: + am_config = CfgNode(yaml.safe_load(f)) + # print("========Config========") + # print(am_config) + # print("---------------------") + # acoustic model + am, am_inference, am_name, am_dataset,phn_id = get_am_inference(args, am_config) + + torch_phns = phns + vocab_phones = {} + for tone, id in phn_id: + vocab_phones[tone] = int(id) + # print("vocab_phones: ", len(vocab_phones)) + vocab_size = len(vocab_phones) + phonemes = [ + phn if phn in vocab_phones else "sp" for phn in torch_phns + ] + phone_ids = [vocab_phones[item] for item in phonemes] + phone_ids_new = phone_ids + + phone_ids_new.append(vocab_size-1) + phone_ids_new = paddle.to_tensor(np.array(phone_ids_new, np.int64)) + normalized_mel, d_outs, p_outs, e_outs = am.inference(phone_ids_new, spk_id=None, spk_emb=None) + pre_d_outs = d_outs + phoneme_durations_new = pre_d_outs * hop_length / fs + phoneme_durations_new = phoneme_durations_new.tolist()[:-1] + + return phoneme_durations_new + + +def sentence2phns(sentence, target_language="en"): + args = parse_args() + if target_language == 'en': + args.lang='en' + args.phones_dict = "download/fastspeech2_nosil_ljspeech_ckpt_0.5/phone_id_map.txt" + elif target_language == 'zh': + args.lang='zh' + args.phones_dict="download/fastspeech2_conformer_baker_ckpt_0.5/phone_id_map.txt" + else: + print("target_language should in {'zh', 'en'}!") + + frontend = get_frontend(args) + merge_sentences = True + get_tone_ids = False + + if target_language == 'zh': + input_ids = frontend.get_input_ids( + sentence, + merge_sentences=merge_sentences, + get_tone_ids=get_tone_ids, + print_info=False) + phone_ids = input_ids["phone_ids"] + + phonemes = frontend.get_phonemes( + sentence, + merge_sentences=merge_sentences, + print_info=False) + + return phonemes[0], input_ids["phone_ids"][0] + + elif target_language == 'en': + phonemes = frontend.phoneticize(sentence) + input_ids = frontend.get_input_ids( + sentence, merge_sentences=merge_sentences) + phone_ids = input_ids["phone_ids"] + + phones_list = [] + vocab_phones = {} + punc = ":,;。?!“”‘’':,;.?!" + with open(args.phones_dict, 'rt') as f: + phn_id = [line.strip().split() for line in f.readlines()] + for phn, id in phn_id: + vocab_phones[phn] = int(id) + + phones = phonemes[1:-1] + phones = [phn for phn in phones if not phn.isspace()] + # replace unk phone with sp + phones = [ + phn + if (phn in vocab_phones and phn not in punc) else "sp" + for phn in phones + ] + phones_list.append(phones) + return phones_list[0], input_ids["phone_ids"][0] + + else: + print("lang should in {'zh', 'en'}!") + + + + diff --git a/ernie-sat/wavs/ori.wav b/ernie-sat/wavs/ori.wav new file mode 100644 index 0000000000000000000000000000000000000000..d50fcb59b8c4828138648e57c4dad168b874b4ca GIT binary patch literal 166076 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/wavs/pred.wav b/ernie-sat/wavs/pred.wav new file mode 100644 index 0000000000000000000000000000000000000000..0210a420898d81f5d90068a8691572c30df25cb3 GIT binary patch literal 189910 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 ZlvWxOp+M6=x9l{)uua#HP6rla>c( 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≠@xM-a)=UB+Bp3S2a?W3dAy`LbXOWL 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%0D8>i|+?47HWzVI@QQ|2j)l);!t zzrzXCR*shogIG6Kel1t|&*!exfwQM8m=E1SMEigmr6xRIS5etcG=q>QyBwJ%f8aOn zgk+)p$hav-79#KAY`q0ST7Nv_;?`xen7P^rG}h_)^?$XYS{e9c#;PZkTzMsE0O?AQ zR!A=e)#ebQ1$B@9m+1s2({1cKErc9lwD=2L-ys=}E z$r%gBQ71f?t6;g+#mPS(8B-6D0P+uL|8LBD_{4AES?ka-Y}M`h2Cba-UR|U%SL4)R zHAD?pi>r;X=ZywOsJ0rQ(rO`ff-0*UwFG@9X0@y#;7;&XKZ~#DdVo>OsA9Aby z8{zX$XFjtRxK1G16c%O*?}U=#DDjY(i%xbcTVtCnju7t&bp)AzjXqLSxNal4X>2w# z73(S&YLWrqNX?)GP_~PZ7m@uHi?ph1SR+M2jwuRv@N=X|6+vFjZ)8hO#3%S)B5zHs zoZ{dgd@{G0-OPB?jw$+OV}ua^4oY*7Rc3({_fp-8-Mz4SAC9qxaE9em{FFevBi)o0 za8o|PN{Uu{sRva?8>l_kDuFzD3>w|1a6x^6m%ofL6THm;GaA*~VI#rVr3dM&wRr6# z?(HPrHx*h>QP~5f&rq!id{ZNhEBIWCf?ImbOhC%kLt;DGf%-{}#PhMSVIZTC!bzdN zcvGxm+X~gvQ`=43HCvi3#$MhYYJY8;0=B_!F;JW){J=BX3k}3{HiSLI6k#^g(eyQH zJXMgoNzNkMqvGMoD@dLSz^PN)l8|Wh8Tni!0AwCGvnV9@hQOuO9|=>lt$NmHv%mQR zPez1ZaD_fluM6FyOS`Wfg6B9%8?IH*zQ98%s9Th}P{h2FuY-AU9)yr@vQ23LsvM0y z|GpZbb<`HXV<~BMuws7c@mLRQj1=QOh?)P@wyNO=hUI_x2Ar8?+%V=F)r~ZLp&kK^ zVgmFNQ`8oyi?^u>+81rT-U83{xj6_~&?D2>+9RoF7GIpd#3dlXBR_i*`6*5~rHg_w zH4$Cm&15n7j1sZuoJA7qP~D}f;mk7YoB6@5nhVe2132TF z>wfw@R8%doj?&cgs26s@k9x;5B-{}$Q?!(DHlD2!pt7JJpG&+ zMcJu+xZyd-2S|SJjJGfw?^{>6QQp8!yAe*~iRM()N7IqH*28RWR>PU+GEN)a4Fl|p zFw|_NwJbcLqv{rQ4Zb$3+i?%t4OPS>wVrA!7nGTBF}GFPq2k}8d{+|i`xjLfjI?c9 zI-HsP^v(J$(5Rw~w#Fjx*uNQ;5sL3o5Y(eIm&S%)MpYF6A@I{V?!*vx|p4vs7p;l94sNvKFsu!jrGueIIMSho1 zRa`0_6wiy9ViP>A!?vEb`(m`XMo18{`P=+Kem$JMEqO0DflFoEv#HD?)S?N@FM1?; z;5Dfm$g!$UhNF&sjSfH$B-=2t1xfI@x|9+K*Pqms>RV%7l>TPwt+Dwg54ZM2=)I>bNpKxzZ)UJUoSOZSp zqsR|qjDL(q@T@L0_M-|+H8PA$;}f3R5~CF~{O92(DWd@c^r}6`Y zdqSw#OWX^_-92%?I8iJw{uY)AaiC()X$>c_mqI;0F@Lr10$?QkQ>{uk|jmLgA5bL`+67wQKwmFFGwr6--k`0?N z6=&TotdffJcK|BxrI-#pVtb>9yBxdwJ-P~gmFhy-C>NPZ-XyP(DdaC` z-Oqu8F&J<7CTQW;(>;-WbrOH709*oh@Eli?eULBqfS3o~=UuF__2zHvT%GhCny8%s zW#g6_q>TpIv@+f*7jhX#;GJ4z48)Bz2UXKJJyCC__t2+-;`1J)mVdBn`WvG`5o-)L zjI2MypIM}j)a&U1NLAZ_UC>ZBpqe-cC*UGkl$T0{q{qHFzB*uN4Fnaom}k2Ctt-(L z=^Eo&?jC}am>;TQyd;{_9oPVVjqnNTmMHrp&;!m3i})4X2X+R#g#Ezw

    f`I46yS z<-&F0D`s=k1x{$ecf{^go2|^&!ZTXP1TqilgY+)21UY&Y6%3zmN3tBU?tHl24#v%Y zIF(CHrr*<*nBmMqW)4#kzT=umKrBxEOAaS1k%43;atm*vSJwwwa1~JXH#chQowVWV zdFUQ#7^GjoY~IWEeMb zPb!JD#vZy&zXOfnEUlAPNc)C;uZ0?@-cedBk7a+kHK>fa-lZTfD9f+x#cTu#B0noq80w~29n=K?AQ-4A*i6cMBbeDQPGBL$ZAP#4K&

    rz`Iy{I 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/wavs/pred_en_edit_paddle_voc.wav b/ernie-sat/wavs/pred_en_edit_paddle_voc.wav new file mode 100644 index 0000000000000000000000000000000000000000..8a05b71046ee44808bdc0228277610f686ef81ca GIT binary patch literal 198476 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 z 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

    d43*DrQ zpK3W}p}b6XLp?hWys7GHGv&6lL8#1M3&f#6zE8|4trO=8PJRkhzE=Z_g`Ltb__;SA z=gp_QMr!RUjRQMjv1KV(5{32C;O3el(U4(uf*RZX5QeG+E#y92w?r}`V17WmH}=UbQkBx_?<7<$WJ{m=ONVr!|fG*+A` z%;3xMU-^wcZgfhmZr~GergBgyW$%0vJ|P*f*Puw`cR%I*5`No@A;bfo1=GD zh#wGe`7e0aW%tgOvX}X4prbZ`PYR^@RbLC=Dc=u&h>#{NQ^yd!j1X|jzX*cGVU)$4u*W`CZfO^cn#eWGLGZ|e3PBVKgWWESEKL2PH=!!~ zV@*NtF@@X04dRY)G1f}xyji(JOccEoiO4#riDOZTjxwhiyp~`6Al(;Q@Y@4=;0Rw< z@bilUt9<_KqgjKqE@Vgf?)!fRf_ZOXParQp8iUTm@*Tyeeg=tV6m+U#AUVFY>?KDr zg{?hsy83N<$!%iJqnlZR^WpCuw0%jJETbw4ZItNEV6uYWr5+~>$n%vhLSJH2SevdqWXX}G|kB?Y@H)=|ZlR&RdRK_Hs6YyG(BdZSgWr2HQ-Z@dEny7J7MjY0nr=qq??N zZYK4UiYaroN5&aUQvx6#ZMMLBK@3Ib*aP?FZ7PfY&ZeScI2O68UoeqQhX!pCSOsC^ zdFVxQqx0VeG$&Etq#l%4OCewnj1}v{=lwcR#Gl7E*X!{<@SgD1_kZ+T0%HTW{qKEO ze8YSj{fGIwQgd~W-V_8&0n^t>Mp5&v1^>6W&OMnc&?MG}ej(Pn6uG@GZ3S!r?mhSb zBBlyOF@fy{rCcHOscWrj>tl-Rn z8DBEyW-3|7;SHYQujF5e$>?%#KW{#-!*|r*lRqNXfw%Gm^lDW>Y1FhGMm#ZuHrNK% zmiC#BPtNh~cuyx!WzR0pA&=^w=BXUKBeYz2ctlLZo3QbrM99M6(?OLy=UrbNXYGry zmd7Fy`m7_by$^SgYHb;V+H4gRT#LaV+m2+l2{;4SGqd&IDy@Xd#pNT)5O`pNp_{UR z>~|H^t8F-8hZ2L$RO6fZl+dZeOfNQ)4FbjI2l*1akJDU?b>=;GLJt!5PV0x%Hu6#N zfM643K3EvZ*AE=?_wuiXk0d#8E+F{_`bYTs`1bgV^Sy*8LMbUqsiW@KW0C@({g7SFVa_@iNqgtWNU}Jw9;}|)MsMxB@)-&0 zbELOonm7TKScFi8?;Tj?ujU(+?Z{e~c_P!Ez1&;dx6#+$H^G~hJ;+-e?1Z3zA5X*8 zz&5@uKZYM9beCEvhU(Stm|t+}Sx?L+&r_-F65DZSGxrJidnkHpyJFq#J>7%Lgd7O{ z5LP*?b=ZqgF7!gkui%*AF~Jcb?}K`I-n#p`RybDL-`IB9j@iyz8$<1Nnt6s@vH{kv zI+mlzQn-j7KmsuX%;)K55)^DU=y&R(GR!o$S&m>ID?^Q-ev*~Rwb12Q!0Vd@UhGNC zmjlR#w3El+5oiu>+f1XoegtkXL0XHdV!F&rpQP7PT`@)o<`?*j`)+xgW9Ht(UngJ- z3<~@TEcNgACHQLwoWe4(wmb#?lHSN0o}iXTFN}r$dIWJ9TG}+CGojGqhC#?hdo7uS_Rb$84%hakWwLIg9irP$Gb4vUB^Yb z@4NH6hq?E!)VRzvLovuvASPC%zG*#nSM2Im947Dv%!dz<1@l2{bfkN#b8T z9UJ&5sB?I*ZSG1n;WX}|R8kJ8@1e|`XS5@-$m%$Se5I?gOPI;5#3k78+50-0y6U-F z!LQlQy%s*6Gw>L%2%6=g-9_A&-KSioT`!!=oV4o?X4$RnU68C?-ddQ`*|A(6c=Rek zD?EgGiK%gE`Y%n>EpVTGA2A1LMvp-?<-oMyIdco{qNk|B=R#@!8xw*s zdOObJDP#c<>|%`qrT}VKgfegK@fY+D2rT3GpyC*dsqrM~B$z$F<*!N)`GB0E7C{%~x!TBBgww?nJ=r_~ zUu$7=3DJ$%Vwr6@j}D>_v{M)II+|i8&{p4%+WofrL_1-u)+Q*4YDv8hTt(m1e&AU^ z38V}{1&mx_@QE6$ihNhfk{{t`c)2?!-^0`|a%FjwGD1D7%*Ki<;69en&g&O2gRvQ- zNRix6?4s*1v%qX<&2GhRGn=!aYgf=d%qBw5lVacNIOO>4Sb@3Lb@->8j`xld_MP^p zj(ql^NS4oG%d&pA)vyLL!`Rc-@zw%NHoKME4pq>0@R%DyIlqfuLB9nH;x|3(rt{F*Th;9w(YY%i<<9qY+^PWjM_&43bqj%P#GxHdjBV zl|#?{i_!>byJw}r=vGt_!-S{8Byp)wOW4Vu1^=d=kjLLcXe4dsBly))Bk2i#u9{R? zj#JJfNX09EPzEX)@;3AXr>GmW9jJXW!AzQn3Mvuj+#mW`oY(RhfAyRCCUZaPS{-Tc zWsxeo0Q=)7s36Yi*VGB{z6{bgsvdaquBuVSar7{=wB6{(l!RX_0(mP7F-cL3QD$y! zsTPioyTzD@&QKZSmeI`oj`Ncr`6~oE$-T_q#2)0Y4uJ}QV;EYYMsg*Yjx>oAk;z@- zcvRs_xK6gV_Ib9l*5BL$JL}N7cW_A8cC@z*;O5znpyR#+y1H9jI<=Sm$z?Mt)0l0> zjKFN91YL=04a(L+bg##NRsF%zmMjHV%2N6`R{8|W&ZK~5+8b5RTuXmy5i!opNe%#q zv_2mmFj&S}dk<_02eh(!4}ZVF@~8k^i3{9haUc^YtQdwsTO$Y*QYg7hO!9 ztUVH=gsbo&bQOn-DN=6vp;lRLAmoOJXNtU5CgHKlRQrQgSW@*FG#xD z=&P)SGlAEefg0Ejo&2&;nQp>fpBrSj*EmHLBGQa!n0cq^XZ0f5BsfC%7&VCoauL0# zWr+S&*{9_+s;G&UTZTuSVRo}@(A#Ra5K4F)znc2dr z-AHe=wzsFSIjGglOY3;fL*F6SGJ~uOm=5S!pufU=p=!Y8yPJLlZly-V(>IAJMqy$R z(~NwFl$&eV2L^$tR~OorVc@x%<`&Cdqa%`8vdw~UE=iV(=wbgh3m7%DSD3@)(%+b} zz76`PTIh>5P^(Ef!S{ZrYT{|NH26A0l_N+GpQ+Ws3Qz^4(}J2+`41$q=h$~2D^oGI ziB`73_fgz91)u3FxVZOd-{g7dPemIQb*uJ6+Xa1MHoS0eEsreqw2CTgYUEC{jZ#+G zhB^Q)I{AvxpPXZ)NjcOc;wbS~eJb}cb`u-4=W=o5p^02E?ULLH)myT9K;BPuCO4~> z^_%o8{e?1$IY?I3TbmDwyyy+0;xjjpsnj*gI zK8B2EN^wPy@jZpvh8Uho)@~rYGt63RO*)vi(RJuUTzj?_(+m{UAgC2T!y~kg`$(5I z_7XJQR6B?h`eM)`rxR23FP07TbIV#(KS!xaL@BMa7LB8y1xFnP7SG1(GMKeU|FRvE{j2K#3GwJ3U5)dojROSQLBo7zV7R;K8L z1#A>dd`_sR^cTb{W1DhERFQ5S^;wskrO2G8Rktg$+FRCZP`Km z{ioE&{@6!PB9f`m#0Kn0g~<@&Kg(&5eOeLGaKAOQyg*iWRkHx`n#m$kjZ|VMRflMS zUMML0mg{6FGsm)1ZAlkr7io7@j=D*1KqsX&@{Y63U6vSl#Y3oe`fjz3xk1^QAdpf8bGa{+b2uqhs6v85W= zk+hjR~^(q5Xg4IM`<_uNEyhc0O8}xYUg3*Uu!9J&#!0-ByqwJH} z#W=GSv8lGQmRWitQ`g4R^DX&E(b|EXXGYSyxO>cBaxP}8a~X~~rY<(!h(#@}_avHw zCX_@x1q~tHl0eor&ZW@7I4?x)_JCL)i8Ia4pB0>GEo0 zH?>{2NFOzv8>z>rna4s?n5?m?A9~10W4v-iyJ<-zs;e)wB2*y@t!~vBvZv8qIj^nO zzbmV?TcDqs>I3Mh62ZyrE)$Z^NG8UqM}(K^FJcUNQQadhHp9p${jJggIsmI#Umqr~ z)6QG|Xsg86#!t&kd9+;67^*FmtIOrok@^RXSM1snV}{a2+M|vzmPqXcE4r^wr2XP& z?V~x%NLL2P3=zZb)OU)CWi_2kSCxL|Xv`s@q%HSo>M4 zpFM}Z%4N0{*n-cQ+w5-YGf@&5@E<{4?_ey$iM)e;*9@i38E#eAv&}EsW3{IK%<`EK z^yzr=mywkbTRsSD{&mZLpa2Z8tVF+Mj#(Xb(+kWe9$QwU7xx4V{d$&4NOu?u4sK_2 zfpJ@ZqFj;JqF?Nk_lrf8pV}I=qi{v4r8kmG@m9emUy+)jt6ohap%QB$ZI<@S%h6>_ z6pKrb)z)gbLSyQX1Vv3c(j1)XDCMo*Q)ktVaPH03zv$bP!E&my8{EuI5{VjNwR%DH z`}3=;@k^Y;%hCmTyl|TzEgnpAKUxZb*QIEOp>IP1I0IlI|LSlfWvkdv80j;ASZHQg1nxe%s5yOsEi?1@^K zpN4AL%2s0+!hBCD+pxmaGrFmb^bN)pP`9sw!QGN5k24t$mhT<&wPiJRo{k`{YkuPt z(}G^7_crcn9Wq57eegny@~I!|sX3>9}t9YIQdCybJwK~F&n z)%h0ULU9y7j=v*rkXI}3l$HpE1D^xkg=DdYGD6N4 zdPx?wwLC;n<#9#>q*vSmEiJ*gZP^GtP6~F0UQ`qpVVh$6gc`VmEnuzYc;eEW-yL^c zg+0AJr-N1puM3F=gRo6VZ>ak^gpCca7dpmM%2OotURa0F;vrJdt)L&CI*ugnm(}h# zg>|O_-H3b7O`!L}ZNCfW!%djkZo`}R05yAvS{gdGhFWKRmLcQRH3W$Qwe@k(^yEQy zJ!LGh^rIUwe+Y|Nl<0tUt|+J>Ey+RP{vW`cwlP_h7zAeWK*O$m(Ss~QjTQ21ahkeB z>nM+v_rYQMLi`^~*8m?^`n4~|%*1KZRH>2F?UpIFw%x65+qRL~wr#sxq=-4zy)zg8 zr~7@s`32ogGBYpEd){*%oc35fbNxTKgT9{LWPdFGimUAp`uCzrU5;Dn^Z9oN4hKs@ z*I_bhBrnCT=!N>wal9ei4xYui*;LFE8i8TeRxBzk2p#|@xG7&671s^GCApyh`4%dM z*S-IEWgaCcP2tkG;;8*r^lyc}+-0s0a0LgUC2>o7 zNx4u>J}8|eZ1j4j9Q#gP%Je8?fT6hNqV|?SH10HZH0(892p=689v&A~Km1BWFuZHn z+psnf*%A99UW9AIu3L6kzF78Jc7}{H7dM?V{$;4A)vKDa>(Tp3Wt4O$syoe5r-_+j zQ(zq41ble+HFy!b_+5a7_ChHrJNUr?+#t*c-k_b-7&&7ict+d6BaUN2*`{nyraFBV z8abb+UG!UOEp&+~;3o!?M}RsgAC6t)MV~#9o8T(~b(IY7T2CeSDc5q>5~pBaXm1H_%6HoU`yR(Xu8Qszz>UVE z>ZfymaEE&P`$g(<`Pg#R0UD{^{~QgL;QL`6Bn)5DV^`bCV0%#QpX-pX>)ToBSHG|4i=^i=m< zbqqZL3sar_3wp@)3SqdBumk# za?o--TwfNlDrFO8OJyWJ(b>@_ zV}?hkhp({+A(t%=LfeLsVOC2QbGSZJxeNOHw}F8f3P!d}6e1_{Ov!sAKSOlnqV$^l$8aTnxo<_ckN1q{o{MmL?0-81RN59fEw1LM|7AH!ILo__ zdx!d0B5Ggb+Xo&5o(B`*n35$oqdvi3q7l=8A;9^b$0mRob3(C{T?fx94m{+)>6cU) zsy!6D56BABg_rjlsCR(nWvrV;3FO8>- z?F?_VS5+<99b_5#0u)JJK!>6@y5GmZ=NSlge$l`Yu95$~uev{!Blu2%=0ZQ|Z^8o? zk6}y#^x@m!*3*Qp#V%EzP@A%CsJR$*orBbwC$S~8dEPbZ)Pn1B;x5Rk1)g7>*3 ze=+}1^fQk80-mL=(vHElW!9gChQeJ1GYh^KL=>`x3kw4IOY{5Yd-KZ{9+8>}xJ-@?nxI&aCrk?{`CWX8;AUuo92O2haiOwMLq1QvVrrm&(3+YIo%rs| z5ye-;mS3#I$c&p!R{o8u0mtp`lvZx1C!BorVvjkH7h_a^g9h42aDihsfwW+S zHP9&EPaZ%t{eo(oq!Kl+biDqo;i6$Z6xv3H^bVUDu_bbGWMeS9{21@3=*7|HqYp*C z4_Ad(h;T&qi#ioCE_AYawox|pGqyE4^o5!be!Tl1btL9LOst>NlePuoDwe!k@3Z99i zSHXYj12kXah%6~a91DiXaDF>?+CRa^qOY(MTFpv5AwsUTB zbabRRdOEMUs(My9IRb0XPf=P`VVZ^LwKNP<2+?Ny?`F5=zZmJy34yc*G6>Co_dpgq_2;+7;raz{Ym_$Ktiw)Ueo>H zQ$dT_Zvv<4v#7m_@*%P@T^9OY@96pTMS23$pOx4$3IkM~zbcZU<-A(kLN`x0Mt4oO zQ-9yE+j!mB-uTpT*wD(DVoEpv6|%})$#lw)tuJYC88#aS7$+M_=*_xC+Me1r+NqlT zsuqeUW;xX2_5$19ha3RZwE(r7(!v#>K6gTwzaGDfi$X@c33EU}-9H+--qrl|&?k8W z9lhF6PoLo9eY488Jzh}oNzAF^K{2d1Ry--;f%%zXu zE=8)UDu2RDp$n?+<*41z&xt}m^}YBJS|_7{Nx#b9;J*9I`|tW50PUUTTkd0g>%3#U zd%RIl;&%8lfHkY?OY*HnPFtBX0@qp%NVBs69~@)e1$zj2!V0kB4oV~tl$q#W90$UG zEtv{OlK0eZdL+{hSpIL%v_$~Vw+|c(s5A)482Of{5eA@K@j~uO+#t%3*T8alMs=Z` zs9%0V@6QERZ8s&Q+NH`?iK6+i_dg@WCiOMmGu51~` zLD}Gewt)Bq57DhqB`+%W5zYpUz;Hi>`g#PE>bG+JIELHmuj(gYDzCw+Pw;E}FTn`D zg;n1eD)%e=n}N+d2nE7qsEfbhf?xuS0{@4%Y*Mm?cFGI>zttfo?{m#Z1gWX)}jOVdsJO50VJtsA9x>T4US8wkT`eS3WX z8W5Fr$FzBx0`()+UFAK+N0x1MYuYz@s?{c1$i9 zI6DGsa3&8x=d@CwLZA+``%n1fqT?kg?S1rn?>ctLr= z7wCbwf?Y5xGk`=t0E|s5c-N=>@hAb3bpTlVZJ_{DlI{xB-td~8|)tL@$up)|0=;O>nMv)C3Y;m#)u|WQaSNu}!2rT9ZAsyN%c67F~;1^H< z9l*g{d6@5Bfa^jNe{pcPuK8~I9{H|fH#mKv{#Yf>uYu}Vs$3ijl@Ev*xV?(xSl}S*z&rN@boVCH4e8(1RN$OI#$x!-DsLe3g-f7UB0oWyQd~_pgRuLpgte?-I`4rC>uh^0fvB zcNqRv;ETeEFwTF{pX#Ua)90WIzm5+H91IkL>e`!NZ78ax3)O%FuZXqNP?pfG??&pV zRBAua$<2XIKBY9PH)+CkOY}Dk`;B8T=5NhiLVkwywOqGkSU5{`s5$hbWx6F0(mG_5 zIoH&}w9lAt&=_30+uFIBhH75<33^%@wl#AOI#3>JGVr#W!45bJEu5xsfTnQEKKRFD zQ$COWnNm6c6jg$#M6Ufr*d+`GkL*IQAM`=rVKNVzA)cc z?-lPM?*Z>x?+&0bLoouUe5L(s{9pZvsD+f}m+($-*lq+Yr~pw=3EB=%D<%9#H;bk5 zuGR2>QbJA12>d=nRi?^P;fValz@9D*Hfw$GS*yuckVE_whu{|07#Q&3;60=TwgHE{ z53_I@{a=^=iocKF^hi1474#&Iz3x+AcI;8Ev^35-|5Pzc*DjO*eip*z~uMPYl=8QKu_UDsHnG zOcCY;bZpAwKF}9v`V-JWZHIfh2{-j~;6s#$kL4YxFe>B|QX6P@+!0f-JBq@aatt)Z z@`H21)wuxP*ipU_ufj>*mE-(7{I&dVeVcvbeBFFCdcAq*M0e>e+_*k~1=Alp<1ny{$Dv(0 zh&191t43UfQs-gx$q$I-;E*^FYH8omO!*F{Wf!Dagw<241b8>YZ{ zrmW9^dGOWJ>GdKjKp(adDKFcU|XEWIhX>(@Uj4flVuXz%H-f5 z9lr&_Go0*|&{gw`-J~73H#L>_%OAib?FRhkOrU(>3;?gX**KHmfu~s-?A`fbCcK9> zatYxB(D8MkzWEUNj|KsWKf_nUDLs>m`%n3PCzyCMu_Jf?cP4MZsyP9^L^AS?Jn+Cz zVqLTjY)8DMf(?UfgW196P_GMxqS|AzrgRQm-z&(3l;lhD9Ca5giYkir%0g9B%^q#K zPHW%{PmHro3Far}Q6WOeFv~qlzQv2!bkQ=^VzO)vDH?LtoD2+MY12gGJi{1$OIuxE}cg0rba;#wHy`1U40xnlXrk``wc#7gnUyP32a3+ zv?Ys+XMsarhkWD!7~%){WIi80H08Lr{s~|KuJM)fy@az-OK&xAC2z8KFtAB)ypp#h zW@n1;1hSJ>I5We+`TPvT$X-4ToSPPbnSs4v@`XXi@hG(aDnMy$zwpPcY&?{9|Avos zGW^V{0=1k9A7u$#iwoc`4FpPGO=QYPpd(&g&H)axHni%tpnt(Z?QRhG9A|>fuufWn z`8EmJUOKYU2gvIlaZgdNypOZX21aN#ejojTia-m+XYj%(b^{Z| z9HE;6FSr@~R;t=>96Fk2zB|f{$2jZ{@;EUnM)6DC3lT;atZt}{uoY(^5B-t z0~7IB;0a=sABx_!k@Zdw9)NCx6?sZEs34z!THR>mign>!r+~KGVK84n!vojP3J2=v z;49pL?&}g{dJScb{1yz6u5euZ34O)U;2wQMhlLgPVvT2Dwf9Gjy(F@V$UrpuQb>Y< zvnq!E(n`!ivEXE=(s99BP|e$keaVVV#U=2{)S_Q74~X7>kxNuUq&^6T`%d5nhLUrj zgEfKD(f8|^D|5A_8WN6xJ&uR_2e!4@tRGmg&MPE~2PhUxI*4uz5 z-=Q0!9s_7yV-Rn3q9bA#oVpVXz9zj336*Tm6pl7)T`BhyglV;)W zlY%%g4ms|Sz@)%>-0M2<;%r>_y8@^J6M<3kpDgj zzQ$ef7w|oVV8VU4Ikc&F3tw@9_6CmUA?kF|*av;kg^dv?63|slC#V3m85@vqyd+C1{Kg+|tLk|F z{h*JXf|)5mUniNEiWv42yQVqR15S~@NF!Ar8gwaOxl?o!-3P9Zm*B<90=3zdnZfMF zZT36kW%O)GwmRE@Z3fkV_TV@7gi?PWRK7dlB(KSq1Wx4OHN$%jg5`7( zQGJ5c1uHrZoYWjt2d;pByBNExJsd>ip-}Bc{qqSBZM%h4Si_^B!QTzuFn`0ls6O;7 zYeKuSGG?JHJO)eSk$^d_1Qq$eFxxGl{oWV9F-uqrwALNm!+b)Bn25hTT3iXO&DY?Z zYjDmt0!C~y5cd1fFMNTq_MmH6T&^Xz#$1hrlJX2NWS2m9VinMQo3Q@(gS~h|eu9XY zE&Ji18V96VQ|yP~Q14w0gv=@63|@d2oCWYSN$SaHWZG59hEQ$n2|V^B#Ioh+IPbvD zIRO>f%g}AP0mS${@-Y}uFM!ql1pV?KIDs;u#9T-^ppE1s0b9X?2iFgfN9dZfK$RPz zE*?o0K^3GtRFRUX8em#Af#!S*JpQH{1B+Y(%&Ri^2_t^P1E0Wu@eJ3I`)|hVj75BF zPF5r1Nd^)T8k@LS!W7%&Xl zB~!3RmY@>11sT~s)U!__a-G9G-1_5MiS_Xt>zzlV!3=HdDsZ>&h*Mz#cpn?&ec-kK znT2=q4{*<%asW!#deq~JAzmlqJv6{RXan!=UdSVc5n~Z8=0W{yIh5RX;Ff#}jHJhS z*BRi(@`M(dP)Xh)0Qq`^c2bcyg1PND$Jq-^ZT?^qY_fv9;5&Cc7< z7e0fXdWhHuFZ!+Eo52qouW}|a9!R%7n2EpfE@}`Jz`==uwuT0`NkQhovM&JFJ_l8h zA5i}JB)`EZJdkf95?+vx3pRX&Rcf1lGtMJ{b z|30t8t$nq;8IMgk=eFQCHskA7tn}^p*ae-}J@||Jaq=IALhmt*`&m5Kb*PI!$BpJ2 zDwTOS5jc3%YX}2;Z=%6eF9Y}2MBHj>;ZYYe(G;p}e?!rv74+geK+m`{ycGWL(F5x7 zeNaE@PYi+X$ylfxOa{|y7BT<7*;)@2?KaeUc45vAL0|qXILp_e{qhJ1#n%|ge=(xJ zF^d^QJ|0etH3!U~Ku}oK6tvBi;DXVlg#RWYv&4~#M*v(gCqDe}YyXenfbV?x&WjH_ z&NM4N^6}YIT!o0Sl2}13!(%ySe<}9hLVR6>5ts{| zx>@*`j>lAt${38#VBDJeVNG|#D7JwfNi&RXL)_x)5_RyX0VVNj&~K=Uld&?QQX)|i zpOgOkca8sMqCWK68vZvIEioHy@pro8qxXM*vk&H=A7)}O9)s{P?7s(m=HYrf9`~aO zxF=17h=z+ zgA4TyQR*Fbz)O6*$8GNOfB$+a-@={h{C`>mr?FnoAYz}v*P~dyM-kHwVny$gcjNN` ze18;3)zk2@zWCpoevLK#5r69=;(IEdBNLA_JX0E0xdTy##H-L@orhwbM-#D#m1S_U zRmZ6Q4US?LWbj?FqTAzL{2ATO_;+`VKT~J@))gss4ljX7vnCNCzZ#H{h8g7(9YIFe?}!|;&qOK z;z$ZktFbt_f5_9Z+wNeGRKkk%Vb`A|(!l4XLIXqXJ@T5$u35*bBX|qiC$Z3b>j6!oOlMW;f+(m|Gi8{yzAb7Z}5A_|Mk>Zlo$) zxc{uG*Qhkc<2QrIbn3|x`g<+qp;)!4SZ^0`s@{Zx)iOlX7~(oIjcLg7w&Rsg$IqF0lb?Eh#mBQapD@z++&FQMKLdX(KG8#&cv-~F&J4A_OP1h0sQbYxhW!HWjKiE z;4UAIQ(zWWcNtj)?gL2F1YpTZh1jchUjEBqn$?|r!@PW`2L9p8}y zAIBasVl=JT*H$QQUc*itfLqc7o9KX?>;6_aqEtRhOtV1W~Y zA4p47WW(Y6(FJ+bHK`-kJoG4A#^Ef5V;*=oQe0kh{(Ynw%~SwNDOYA4%!|q z$P{RUA4Uec5sGvNz}sDf49G-mL#DJ3r|e#E*&}coyO34I%DwQ4hhmL=kXzxN9YR)t z=EgT<14ra#L@W?pZ^*gGR$1hge#}mP@*Jv1ZYb`jf#-4)wf@&Qw`SmLES_rva=c22 z3|p{DyTS>k4yqai&dWiRo$5_BCmRrLFyhOgO?O*v1ct^wbSb(@yP&evm>7dOJBgU9 zl1D?^WDeHP3TS4YK|j6cTTR@?4AA~s z2RDKaIM0)j*;gmt;GXgr_l2#f-&RH3J%!jfTe=VTp65X5)|6I>pHRUFMW#C$)r;AZ z2`lbCMy@o_z15)WVuHf+95|m$pufQFCJPa-9q!RTk#qfnjOjSyU@^EfO(G7%PreJ# z6fY2+Dgwdv1bQI>{0%MXMPy3DJ}8Q_vjfzBej*>bhs>lbIS`rWMmVei?Fpy6<)lQ_ zrVUUrZ;7n*6QcePVh3rWc9EN~!*s}2UC2Iq;2B=Rjc5WYH`9@sKNoL{RivWWFF!B> zS$N(_a(6it8B!mxMh_wLtS1kJn&C8z{J-#R|AEyyQ9J`44TlQBPttmVpL5qMYhMEqHU=bemu0T@QWBCo@3 z?<(|%@-fGm;xH){r;ZyH$*W>3oO2FTYuCvAxj6gNp_5dA`s51hiOKSzEl z-H~p|{jj#j5u@cfxK&)0hRUsxX`e@2Xa@bHhQvgm72V)zx+w}Q(^@Rasu>+L5TdX~ z!{A9Y7`2~xpoxDY%lb^bC7V%sR6}&v29dpp`LaiDN|wbAmmSN z=TH&oAs0YRXd)Eu9>W_d96h32xbfD37U54p53KNdaJV+1BYhvWoM*tyZGa};4`H&% zLIr&V)T0v6;|oX6;IYt7{31L@^bqY0ta zOc~}UwH(n}0t=O*uR-~~fEqv-g<^dc_(nyU*T{G_&?lLm@Yb5fT&90PC+#OSgvx`) zY!!MNT^ssoZKxF7kHc}l-3tZaq2zL`(Hl6u_3&4FfeOMpoD+wjjQ9kjv;n&&471S- z>!FuC92rSas*Bk71-Hw|hzC`n1@;}irkCOXu|C{JHj81vx1WQ5%x?5z*P$0X4jmtN z@B*-Hr0~aYs|WmL($M?efbm)a=fm&l8N|a=Wg9%pZs0dQVw~rQCs9wbiZ@ZQEcYBaKR5s|bkcFbj{WbGz;kweI5_&N%!j=&k(8Mp4cR4e*8c;MO8A-Dllq4VfW zylV#-gR{U?zC+h!)-XwMQ!2@Pp`Sv>xGgjscR|Im7<&ac9}XTrtLc28BRn*~24LVi z;5rilHRlL8$(3e~(|b@wPJ#<#E;SRYu@+T|Jb}E~fcxn;IB}#RHderyd>^;EV=^KC z1vi+|*vb8{4^uJ!tHf{Uf1ShoZ3p$s-57zT!B(i@3)oMcDndTz%4Y65+qc3{`n zWV*r!tRZ`cdCfjobWwn7#D*x3z;k|;@|ALn>O0)&Z+I{PU=eP5Inx9imSdT z>!>=YR4T910sUH=@;dZh;F_nbrF1E#D$24JrY^Y3>F|a-1-|kx)Z#ZI?`Te*LASIF zmAp3*bk-fXmuvbcd`!!u;l6@HO~boP*2d z?)bm@=Au_P0bNp^Z?U(7H_1zSL-Bc-_b*@wegTcw6`r6K(cgOSe+qnc5|`qii!Rbp zbX=PN^%3;t`wU=7NBK+olkrnifQG8;@92N#ABs*#Hdn}%;}gM6PUklV>Y<987aRw@ z(FvkUoFEOtYi{u;^Ce@bP~fq%$rU(_OyFqeQSVScV#pylBi15Itqo<2|DRkGTl=?Nd@-G&{;hUkb_wY;@d4=oz{(9+5>FXRclbPc9u#v_KWx)kj#l=h3N zTdR&MNo8k6F0kgipoS&FYPA0mcdh}f$N>oE-B^-ObJ1QuZfJCW})xM8$ z!^siJOcL{pnno_c9rp%um$P78jTY(!Pw*qavl!_+=zZ@g3AfX;z)O1^ z;~g3Y;~43<;mCBKi2e8J!N1YcL6p6+oA_|w2S^#>Pp z9sGX|0#gtT)s6LVeb@s8*;bsE^-$egMEa4Hy_Ia};%~9--vP%>%%*Qow8U$o*6&sd+L5@qBRTE ziK=YHLv|zd?}kBHFdBMeT4oj<4t=FvP}WRA?a79*y^Bm-j~saqY7-oh4L-{*I*e(7 zI6Rmofq?;XPuW#jUip`zD7?RhGk1UjOCZ0?|48qJB=D?Ee44+%kMh2C$GVeTHJlS1 z9{U^nNP7)?HG3(0d;3;<8%GysAJ=mC9*@o2$N!3}AJ`K7C}fDwrE^dku8C*={9pD{ z7ali1QN1l9dNGs+Y0xeuM?6K;zq&eO!R9PTa4y>)!oeHHu< zp_lK424^Q=2VB7$LStlEt>7g5TmA>P*`35IoYUn|*=bMA!o9Bv6tW26W1w`P9^V=M zx_i-&%=WVW@BVrGaACTR|@$uaOIjKw+}2!vWLwAJpBDbN5Rz(}1cm&I*z zGP0;0a$AC+`q7QS2p8D~$~LOe>fstr(?XlBSqZn!15g3ju6)7X0w*tp=m4dHt-&O| zuRq`W3~K(9U2~ix9X0I@Y(8sETcqui^^o<1wVSPiJ;d?L5pY&^&-PUH)!|wO;)G9P z4KP8AK;gv89A-bTDX7)|g7e!>IATmAixCH|Js&-0-#`9&+$i44p9%B==60o61f7%VP*>ZBjA<#xxeaa<*P!M& z6u0j0I3ogZmTUz#oW0P^%kob4Ookd%sLKYoik9w2-s{{JVFj^?xvL`dh32E-Gh;3n zbroAyVtL6nrH+<5Qu0~xPeq>;@kE^nkF#(FllHUn93zrxa#864P)Uu^TbT&--Eg2* zZivH??O((A4utpDFX-+SL0&u?XMZ#lw#LyZ%r`awE!z-vUrk+Ifj-f=-6$Al8oc@* z`q?^C8=>Bj3*@htFBjz0lJLUK5W4XN13@>jA$#I!<8&9%3vs?vAKWs-MC~V7n?71w|7*fLtv9 z63+))@QgpkyWY*Z&N`Cq*R4mP%`~`BVclVkwr#hqw%2o7+>+;tuL5_F&kl?gYD&kT z*r!2Xu_)@u|B{cOX7T|QiX*sL=)fXcDUKKB0--fG@E9oSI{uG9@iu}oMwYXaQ|b6+ zdt@uLw{nGhw{RteNa7Q>DX5y;wh*$b|cuvBjS^>Si*PilTqi?+5!XF6?6>f-)kz1COR}&A&*XZS4 zp=&cSYzo*8yt;x`p&P4jZ45Eb2`Ob+Z;7+;=6usP<6T20*pg=z!|8>@C-~FX;bOcI zu0i(RR=V(A{=a#;yrH>ua$Dp+$W6<;n?JsQ2E*jO?Wm)_OX+#*{o;ohnE);@(VxD} z9#W1~hiEVBiWoG;8OCtcH?2%zv4Ro# zgYwqp7Usw~RNk=s!G-mp86-QSJimM=_!)v#It!nTSJZ9#1CU-V+1boc`W;*?X{dw_ zAihcs#0tV7aKTK03~ssKsA_AWp-k{#DA+qR<2RuQ|W5OXBR0OzQEK{Lo#p0`rI)0UHc1b zx57~cH!-q|{|K$SNxr(AaZtRh)ev-)K(&PmTb zlfS%hf&GoEtZxusR1l=m|5Eq)cXyMs1FW>{#@4f~7*) zDjkxhSKUypM)fV#M^+81e5Q=P*vx3&vQocO;g_EHFF03NALJj-U7h2}PRePWyDQI7 zc-wZ{Ip6cj_l`G-Q;80Af3~*ru}Z7?s_CwMuI;QluCHKxVX6^g3wdY>gnGhVk>Sxj zqUS_KMGgq>6Pj(#FtpQ#DBQoOzRo!VlJC_Jyu0zGK0&RHSBSh&O6^{G|kc`6G!FtMfHa z)w0#BSu>@2U4ROvC$x?|X*sBE$~2QM_){FW@~dUdPnXkd={GaktlL?(?2UOnZO>hI zeDnD;LAMl1-Dmr%ZmRca_G@NpZ|KGvdYC4gJBReL#Dran=n=iF$oJUpaZBS`#x;*U z9@99gNchN*;)a>(70fhwcwnoyf%B&IVt%pQm09|X#c37OtZ7X$W@ct(&du7Ny(X6} zm}EU^AMVU{{pC^m_H##qqos*N3{{G`qj;v?rn_Z`F?>f;_!~MgVZ4a@n zEqq_Fr641p%Nv{9HiypHniH4D7L>E5+DXsQKs#!UrnW^H<0?j$&8<|S+U1(*wYfT7 z>h!LCx5lxgniap6`WCw+G*SD5niN>*o@`x|cQJcqW^zWu^tAM}Om|L;f_b*~&QhM~ zzI%L76vuSX`{*wkE*eQwbMpkt+3<+yc16C#{))R@G&TNIvBSj<#%INr zh`t*(#r#D-LQODbr4!sDPp0F7RaekAw_o;#%y}6VGUjF^W!V2Mb3tt?s-WC2J1cBOvh_?D1Tq}DDw@w!`csrd{eAV{ z)wyaVs#dGKw#^EuV?5(-{hCp#qfT7Q*5 zi10yNMogqzDe9?es%gy+O)cG9eWYn$NPZ|2aVPR(^n)T-<37aqD1NPYmEtM!+vEBc zxgFIzJl!17JyIT|?um{0Hr`Fn1Y6~T=eglI$=O%38fK+tZp?g@nUb|JyLZmHoJzUj zdCT*Q7w)qX_F;~}t}h;nD;9K%4GD=%V$v1I)L*qd^%?rL*!_ESS9G^@6SWHUYgVG_ zkxB9cVHm&ATi-Ro{tkX$oAS%%{>=KFF*u`HhAuNL)0w$At8Dh8+)cL8o=!ZfOHy+x zTd_-5(=^ar+B^#9?oq=q!#w>K&3eUtYKq)csx5X2CUYm;zroj<;auW6=jduJmftsb zb>7$f)dh3&ecAIe+hz{Pc~Yozjq5#PZD zf)xi#9Z5e^3=Snt6u;T$N`r2ysZ(g(@VntPBb71cqQ{CSmrN@8y7j-agKCH@^oV~u^Vb$-FEyhAxHvv+2#%&MGyAS*5NT}EoUFFlaHC&QSz zBeO@=+-zU&iGppm$c*OL@OApBOV-&n71hra9;P*QRf-V~ z^6CDy-UY54dk^c{f)4q6^NQtl%59BvVtZDZ?9Mq8ay#bFC`_@had+@v3rrMK<#>9E za*CFQ?&fFRKbp$$15VJc)2vZ?sNd3X;R|#FO7WzBn|qq$gYCC9%yzx-dEVUYL7A72S*r!sD5=H{wx7SG>&H8Fxb!uHh72%TIcr8rS4En!lbHs$VCC|BWlxe{d> zm7G*;f9#d0tkBo`MXIXwVkn^eBmERQ@i&2H?(TmYAW#RaA-KSIoFOif-vNUnqeHfm zJj+a0R#6|(9Mp`|6&fpqzKxt8T_p#!;4*o4dt)6h=?ll0aiFwG4H=-}LM}7o-TsgQbaG3ALXL3#b z;lADO!{Bxvuru~sR;nWRIbT`$ zy|BM^i|xI=pG)nVz$Zw}$X#@CRSEs2ki77lk?W)LV=l(-DQYRUu=uz5fw9A)--O48 z9SSWM^1*ORldl-efImmKhAu;W;9$z53+h8JFativU!j~?0bWfSc-{AdTT6jrh3c3l zLz@BB&~(!bOLAz#u+!lKBjckdL_d$38+kFp6!CZ1OG`?~E7LYZiuM3lbio z92H~(%lUc!o?e~1j-#9HVqs#zp*$*gT6V>(O_|LRVRAFuXSGN6)}r94b*p{9Guch~ z1g5BkQy#-vS3cM*k%J&Pr!RfwtK$a)* zQ@KbxM9GW38P&cWw=i<5v4~ zpyTkwJ=Bq8D{X6NE3}GO<+bc}oi(6!`NF-^TZ;c3tclt|1*wo225%HQGhb0sbxFBE zv4b6`B6ZcZThu+&E7ir|txIVhYcJ@AYx9-MSY9z%MXRQ;b!ZW4=Y6T_lt7{mO_hT3 z1Ov9sarO=ytL&f}p}wXWpp`VOwJUTV^otDd45f_m#zv-kA!95fErn*6dAX%@_{nf8 zv}4G+kaD5zEKw$tL9MT>|Dt`Y3MeYGDrPOEB0Hkn<&)Bda>2^Lg;#6%aueax!E7Wasmw^-60(X{6bj^3$UHhObdD5+P)pz!HE_BXymUiVk7TV9- zm)Ij5`|YB=vtt6d(q|p%t^jmaRN#CS=k9^M=m`EJjwU9ME77TXFa3}uxXlhAo#^_u zf#*Rx;C14Fj_50Th1rrCRX3Y34S4z_@i4kEvBLa7wctu%QoF$=ZWef-gP{MMy`gBGset#72Oh!ZVBdq&d(oVr|fy!wgK2^4#Ec8_8- z8%-TYrAvd?IiIXbY{q_CfIiV~DM|bW1Z%C}Kpsje{>Hv=?+bUn%jE9ntYznIub}7K z*|y92*jgT5u9s|2ZO!bp><+uq7H_?7k9UVUTiGa^Xj=-$({YYn&MeQ%KkQ9!1%GS7 zCH#cjnosObeIYfXQ5579YCTa_d?gU*itHCAp+e=9&d3$Tiy5YPO@f*;Q;gag@C<|G@Ta40{* zw_>}>pe={0)&^CwcAp_i{}niNr^cl_sZ(h#C_97o)KNQCZBq4xe*X(p&z*2|`>Zad znxKq_yTV*$3_F;80FJbk8jK$9e#%LsJ};C<#F~i8O;s^YUPpG8>wvHF6g#^YT>loK zMjIQr2hqkY!JS+qxSS2+R|G%#0vedh1Xr$H~O6Mrl40PMhn)KPeGeueMGcA`F+Pxqj_#CP%`m4F_Nl3v1eK#i>oSwtSDn5TZu-a^NE7jsJy z&jMjVc2Yc3u4ifX4I_e)TbiAvdav%NzNR><>aDw?(x|GaI8{(HKqCOv{8pW+%~qF% z&g5P727PbM2i3o78&J;)Dq5pcl~(?Q$H^Gz;`~tVQPie(U@fGu+Z5yJaQO2CsQXNF zx*sqOHP9d7iJ#I1X^Bjet>Kb$oT>^nxs~9&K9{rLF2RV$(4)E`&L%6PGjUj)A)Z5g z`Xq)36M3y*4NMhi=$2XGTWJ->5Z#4Z;1jJ3_|aJn4-N}_;)Zh{rByO3^yCR>a@`U3 zN*U5Meg*$GoTwj3+qmJuspK4Li@ZY^EjA-R$VbIVQ2$hsnef=i3(l8&f=#_dwgyD8 zDSBORh%VsQHegmMz7y91MxhFQgG!cn3M#Q2GmGgW?Fp2UEQ-~t-(s6!M?yhI(lgK@ zts~#0mec3SaNyYI(Jxsat%cW?f-Im*sgjr!U>QEKv8t{_X~K(M!5XF=RgZ23W$#n; z1Ik6mLM3GiTb$J~Pv~5DX5}+oloW1iuULb+BRg0sN$8Ycl_|^;IZYhGwoyz`ZlyR> zFYWAi)em~JOi|Hv3u+Wof~-o#fv5Esd{eGa&jM6F6DuT)y*DU=iFi`)~xnRp7m|6!qH&`Xx4e-Siz z&Q0Jm-bl;{jFQvnkF;J~7jViYXbm+6y}fSYT7ia}=1G1f*MS&M6{BB6Te6eX00;!0 z*afupD7hqR^{2(vq!Z9cH8B`9oNVA>V#wz}t`Wq1atJ$^Xc3q%EuycJ-J#R+lR8Y> z&{J(KO=7=lnu})dKw=y;nnz3hgOiB9SRaj{oEL=-$am=|S&kYBe8D8~yj+t#N!Jl` zgP)}a@I(1YZ6VGCzlo2@pR|Iw4fKpChteZ}gSahvi8kyOM8vkl5`iW@&;#k|(9?WP z#!}~i?^+!=Cyi3Hr6TZ?zljcPIl2#_mgZvC@4~-x1v>Y=sIRg@s!Z) zpCt#+%H`o!`&sP5M@ixEiD?K8onrEKx)sp@>TC6}UQ3GYf-Qw=sDu+@Y|t;ypjr|2 zgnD8oFk!cW3qON1qZaXxoJ3K?Ht9IINR9(qi6PpN)8$RUR`MjcSUwT=aT|!GiqFK~ zfwAHwD2~5?wpdm20%eDe!wK<*Y(_oLCcX`BgyP~Vva4J^xQrOd#!v?kJyh`KiX#%B z82npmg0E{C+&K%70`o9Ahvo+;Bw00^gfCO@Aorf zroT!H$OY^j!V^49Zb3il5ZOpzp~}#MAX(Q4oPSD zX~ZKSzQ!{p2!pUr8bhR_@Aod)IoN|RQA#o>Ka|Y!142(VlV3@*$>ZQeeWxE&i=p9p zRLB>Hv;Q&^MHQ4F3#o0?3+Qn6lm-*AOe}RoE-R$T=&+MFq;Y&rqC8uS&J<$#{=@)9 zGx|NhJ3uR+v;BiRgZ~nt43G%8)#qS5Q%I+@PNt;N@Z{Se?HA5NS^gt>KUK&JvX^*G zZk5M~o8T|j0DAk$#50_E7W{ud(3b>riWn*$mlG6?DHE`CB}pB-R~o>-l=>)-5sd>w zfjyo<4yFynMxh5Wjy@(m6qNEYLP2gOj)C>~$2(0$HIn;_ChW{(Vgy3zt9~DnrNB^y}{0;0Rf_wl3*bPZ1wxde2 zJLDh1Y%!XK(;@IQA;5h9gKTjjdRfina^!xgx|B;?rDl=ck-?M#O0EjJ+Xv;2)M@!) zusU&@`iuA?xWr7%QiePQ?o#M`1J73+oYmFxchW7J;kV5Z7ohIlhqy_XB&P*|UxW62 z2(dnm3;7w+c+x?i1kQ^E24Sl5B6j>(+|^jN1ASlE z8BE2VTqjKcUm^+fxB|P>0sZ}C>8tv^cuN2lLsuZ;Y1?6gj^W>B)Mr5RSwvYrtr_{NQ6s= zkauJQy>=0J`afPjBjK$s0A-hndH#b4zK4|`LtmGT!J|?u%;O#t*TG9#CUyvDh!FY(*5OFGn0Q87O@AbLsPnWI zj!Vh%KS0pD5-$mYU?bz$NAmB$55i0}lJ?7Gfk+%0tS|2&?!u90osc9kghVIGCTNC# zlFf*VZ}C3A$t3jvH>BQ#5nQU~QW+q_fFUR60pGS7D)k1cmo!(5mWKhw6$J#N6DQ#$ zaWXJ=SEZf6-5!Tp?{qSQr~w6;ZFrrVs8eu8`3$t~dTJuE6B+&sIh~BcE6ss3&?%xX zYAQzD5qpuP2^t*J>*)3k10&CYdrLLospI46L^aHm!9r$Jg-bEFnrE~#E4y{~4tS+yl&O-&Tu~36( zPtPH$2j59Naa68DJwX?DtoVSKO^t`w+dyD*eezNC&p%OHrFup|=u?g!V!^tkH(#P;j^2l7hUJ8X~Wdx&c|Z6G(Sb8cq19 zRdRChr#OxjsHZ>=o_kj~yXZ^YPdmsJrE8*%7)qUxCQ99LdX5U`bjvYe*smkmCW#1ZA%QGw~!I&)P5vl(EVF3rt(9`9dr?LQt*i+LMPHp zR+a0CQzV{F#`AX&cL5V~MV>463$CRG!CN8%2+96Hh_(l2ZL_o-sKrp|BBcP4yO=0J zlk`fUdSAd{V+wNKbRvviNv)MT1X86FR5Ft-rwaGPcl1GOj+g@%;efmm9`LC+GnPX^ zu&#U~c!d~4Un1TjhD*Sq$0ORRiMv7}xs}>L-jV(m3glY!7G{9_Q!EXIYrU+X#**>S z*KR~rAx}&9gJDD~I0(fNiNX@#{vQM5TnI*gap1yh2oYiet)*v6*};d%%HFV_h_>QL z@+}-QqS0|L4Zp#8;GN}z;l2lo+eJt_#v+5bM81V?ZE5h5E|bk9U$7lAsJTE6C(7Ye z8{(YU5h~}ysPB?R9tLfRD7mks1tPjH9G%YqTWg_Wh#t~Lq8?abM}R*4f!p~$@(wYB zS|L{wCnIywliz^}E(64TGdRr;#oFAAJMsebfLD|KqzdAF%wTCK<4~B(Txb(*lg1L) zpjpvHnuIQoK!jrr*A^vYM%TdsXhUq276`dQZ+bJsiW`DeWDi*$&WTfiZuJ zFV2U~GyJ5$Rh}&+gU3{!Odwigw_lL=<0NYXH0FNf>w?gqa?{adum6#B4&afbT@>zO zGntHS+uCu)wrzXIwrv|bw(T9;Hg=e#yQ@q8+xee*;!e_CpYA>P+;fa~#z8@rI@#0B zF=nJ=sNl0Fffbqjt%3CT1&MT*u)A7jUE_o4v0df`gT3DDX1;}?+Q#@{q&HI(fFmxx`LqU0me z7^#l5iOBqlvQ&;0qQwkSIoYdhkTjvHqXA0V({XQ1AU(vbcCRo7hns8C3wmi5ihG2W zjMyF6=>`mU7i{fQxQ*`MUZu4=&|!gRg%vPY(3`T~_-S;7BYwr~Y?gzMH_a@9pV~iU z$q!MP8*QG)b9OwAgYAuV{M>Dx)c+Wtz~SatY0UL#;C<$-Yi^dKpLmAp#DAcunax~^ znnjp#Ro2R;2ZEFIp_Xi<4UM6<+>h4tLHvXqc$_xS#-c`-2n8uGIlVXd4W{M+6akdr zHoXWPDVdBSq3dQeXp|qlz0_nIdGS@==g1}SOf{E>EMROOk>I%u#6tjy3ve@bdS9~do4MovT-iUW@K%Zi`jDaDC-UGeLf3)+#MtUw{&73gV%4u71 zw@ep$7)%-3z{&F#CSGYHFBlP?S$aNr)oYE5`djY#c+}6l;NPoZ439Ncv_Iou^6&R_%W zz2?UKJiYcHxG_)+6}pBvXY05oE6HI$q3YWb{s*EtoplF`o9c*?z@;#k?6A2rW8z5||3p2gk-zI&d&Zoz%Qb~pl*%W05juC(aDdOGE>*55 z|0x;Nf_xsU+*i&iieu8$>1A4p@ku{t}x$Ui0-kfA4GrOZpSr_-) zwP>v5)jw$WP+z$i{0IN>3n-$jLjz?f?8-fX`si>J(>9^Sb^slzm)bF{pOys;-*-4P z{;NMR9^!<)*+@_3x(O|b^C;zh0R!9$!A zH#{zL+``x)u_@y+_y_rqq0o2LpB9a~>cO|#CcOdd`t`HCplso7i` zTx(n%+;`mBJ&irT+%-K_;5HOYAVpS*a79*$JRY$H_rhGhpy#~11vLuGCAz9OdpR$n zRo=_FPVIsNUQ_j;QcL+D!$$(Oy&`u}1}fRnpvbIFQ}>~oa2I8Z{;+7qp-=Ig?_5w9 zID@d9cdOr&xwz>jLFM*5J^I(=qf%M%URax-Zkb=Kh~aRnuW}!j>%T&sLNm1O!J@D( z>jobMR{6iiy}{w7Oc#OY(=y0acB`7;zOX` zO;KE%P85}k9c&eQ_;;|yKg8s1=`Ezwf)4qd_AjzjFP!@>L=8SOgK+^wqnGhZuZMEc zTfGRK`3a0~C?+flRnz`PO)z876a&KgGg3_p_b&A{@Q99zRFS)OXx3}k*`<-i$%W$r7p2iPy4fi#q zcE+oImQql;B#)7+pv{t0{)F3dNIES~M2EAGI#b<*cjiac#ZLIyc@pmUPjx?x3%65- zOIyVG88-SIlwEQw3s95KEv6t|-G$3`0c#+>#zl=`uz){>K8Eh1g?CR21WN|z;dxg# z@RL{mJnm6kC~l;`G>U$G149EX;dl$dZNWac5O>tNqbAuNt@BRWWt0kwpz*#bb|fMeJVEc}b2^=3*fZ6xyYISgxptx!5^#=3M`}Ml zWdFhYFRI*=t8tc8$JgRHY9mdgr&3wYi3)T;RFhZ84rLs!?zz<*s>PU{RIag$eNaB* zgM5wW(Mj>4*8D+=md1nk`{{DtOm|B|>zFywY)s}e&&a`^l3PEI5?%ma_`%>iw7p6( zPs?&IqEVRZ9LS2U=h(p4KuL7L`Ukh8k9i(_ofE;U!SBKRs9q*uKTC{`%O0aW{gwUb ze;oo^P=Fr0s`O#U&`DoVdy3^cBj5&d+>YjyHq2j>ji@ptMzbEH#!hDZiDKYAU=@$2)g93%fqM z&bb$O%6QLu)A}0u>iMer^7vkOTY4XO8hCEGo4Q}Q#<OX1^HL*HX z38D$2Nvoyi=uG`3)t1UoH@qrd!lkd2v<}y}_0)0w=!{&Jcgs8Y>LOnyMalL*`31Y+ z9=y#CNRfD}e})sc(Uo#1ltZ7ZM{<7+V-z{uNNNc? z?HqJP??w+|H@$dk>Crkw2i{RS?C#R<@q@Rws0?g^5&Z@xLN=KF1JJrSMrTn&6o9B2qy{|1mQt5k0%xajMQFoDo{$_}l|@D1ls0o+{sv3o@fKtD`vG zz0QWt%g)9q>HOs$?w;gs>rRP2=Rj96m*w0;pUYEqids!gud3=V<%rTniBc}h6L5R- z5?{=a3QAAWgqcl$)^_m-Ue$N$xcM$7WsUWfCP-7H@hH1=l^UZfUrVaOqti)g@Zgc9 z7wF{XfSr*Ar@cImJ9P8bV>Rs}AJ2hi+B$Qn*^E0Ai#B9o&ZEZEng3z-{*C@rb-Ey` z(oxhJ6|2E0Buxq}VOLB@Z%`ZVTVK5$*l$D7@x`b^J;w*4oH5S0MUF9vz#bxNwtbumHBrxW%^1^01=ewYCpweNymoCQQtgC;mDQFvdttV8)w$TeV zk?znMW>z{CZ_@KLg{UwWvnr&2v0FVq`z@g>0>-F%F~UQom?p`{b(vZaz`Bs+8_&2aLFeQ6HnlJU7jo>G0uP= zsV1(6xo_bRU5is^EGPbWyi%($J~tTCIXG>#B{s;Z1msibnva*;$$!a-;INejqr9MVCtH@Wj`^4>N= z3OY}RldBkDM+Z^)7zJLO9>jPJXlGI4hqY#Hae5DwwjPFr9I!Ax)WO{bX;U5@5$MtVI7LO5^ zk0;h_L~m)dS-?zb>c(ATKgwZqaT%F`0@nbZbs1ufTt-1Y)?!qvqi2@Rh(wP}N6FpG zOdMz&MAxc~c^FTYmevWzYYe@vZuaNJDD~t;5$B$e16BO%VovU~E>$OMc`v7dIX*_& zO?>=VxvE?yGCr(qQKk~JwL%ps9SVmRx~t#lG|-i7;tiy4rZ@598Exc%ZtavL{cy5jFg7e@);(?C}lsRbX$3*e8dO(vGPKB ziF*A8Wdsp(N_1LZbJm>1PoN845`KCwr_xJbQ_4eb^GAHb{Xd9`)pl_&{GrR@Gp-19 z`i4tOxYp6Dx)2@P6`(weq~7d@KY8ppv96d|jG<$304I8RAqUX~dh#fKWJTS1GC0f@ z`le%0SQv-bRYvm{9l0aXtxAXTtB(`oJ1gfW8gMW5*W^&L;b-5w!`eEdUqE5(hpy8z zDe%~wjP4lrtm530$MpP$RoTW^`%tk=$B0m~U`;!v-)T^Db`~$t3{l#?ah+(g#I>efZwcQb2%gomu~*CHTzro1SWTEa7ucmMp!xC;Ja0M( zX$~rb6280+xxf4yCG)dNPPCcds(GDtoV}f6oy&>Gr#ib4PrIFm=~XML{zMJGrIJ{= zNak8Zenz}9ls&Ei{<^zS638#61p7=OW<+tU0|9-Vn?*l+FUHO77_~kQEExf$$o?yZwfqu`hXj6L={p(QNJzHhr7_F z_ULaot6!5D-3*-}hCd7w>uKmsNYS&=xs->@w>-*lrS)p4`_<(9SjK5m%$RNbM?SmG z)M3>fBZDo%KDeCKlpK_HB$^qU=ny_dtg(vdqdB``dOF$T?8hhv%s@4y2@J1n;7KYc z>npJ1qbL;&L%X9q8WJB+>gY)y{SW5SP{t=0qDDX1h4;pfOnbd9e3|gq-1(4)kD8Xz%_hTP*{Ozo#S)c~H(0P4F-v3h@sU_Af1pDF# zPzb%!hQZ#%$;*OkgBR$<4aex;v>NzluLy0UyLo%)K58c)v_DUYK`#xIF=x3&N zhW+Xd>K^N>;cLZE$HY!VzlD<-qsfq=_J#?l5bB64Im~;8Tx{g)l99SmK5Y- z2l4y}p7~sqw|53l#cOZ()l!9AARcYa3s!ni-TZV$m%(wl3GU4EP&)rdjdGTCE^zvt z16+n{m|O77^2B=Dc{h5md0%?BdY5}kdmrL7{MJ3e{gSTe`p!CP4|1Y&QUTnXiiwZd zy}qFp5MI{YF?fvUMpfy~N^L3a1Z8a`%tYJA2Zo=?+QsfS(}+cLbRv!bL$z$$plgocH zZe`rx=*aGjI}xXY#0*4TIU`-Um4h+CY1&tA!we&am{~LPj{V=%pvUg{Jb23Pb!mTzx${E9WeGiBYbJ!n<*`|VS zbm8RdVD;b{L`AMHD=aZMdsx$SI7-a-QQwR*cIs`(J6@9EH4S9|;rpgtAggMwB|`&v zRIo#^FgQypa>l!0T?+#%(B{8JPBIRMfmES}`e4Ih^|4<&T8RCmmGWez4T`b}T`yb{ zP%rJ|ne4gfN#?EQJ>iY=&G5M*nnY}k*a5e{ZGTZG-qmYE*wC~V=1xLWY%>J&ZeT`18RK91^g~jh~Loxd*4-B`Bc>D7ymve!#<*?T#8dtBi{vlGwVuJFfoy?2 z{to^K#;Qu(rPyk*mtuOvWR6L~TXf6_e0+TP)rDe@$F1~_4D=0_M}_@Ls2e&8JB{k* z85qGGIB$-zDzc$)^w{o>j@nq%Krq!YE+le=24Y0|b;FNxUK1Yb-ZOfF|N(dbm}p=K7GaK93$)za3(Kvs_sXfx+!PJaQ5QI;LCN3=|C#zH<)25tXZ>#aJL_-b*QZ~8yvHW| zj{39j&y1K}vF*VIhXf`BC*#vO2e*znW*1ae_M(|HhNvb58i*%^lHyh|396qVsiwSL zehf;z9nR@-6nQH+b2y(dw@hW9(h%n88M%~vQOX5Mxl_1}lGGa%P)nipu#R{!v87rU zLEJZ@#xcq4MU0pm=J_NeH`S14`tQ&*vgdE)@7=YmT5Rw*YSN9tE0gn924995IJ=w! zeOV9E+z33Tac~fBC7;Or2cScdFLWfdoM(T^jIg^ot_n@1r*da}r^~sAc!n}|sUmJg zv_@BZK!OJe@U`AYs4r15QSYLjMoo_@5_KSfCqetj!4dO)yEwLQB8>8 zo{f?!%Rc7#61BGTx-;ZV?z)e1MM0G7t5Q8FkJ86|bSRDpJsc(MAK->(^_w9v6m|x$ z7C?DEE;hy+Q{&Ic-)(SaJNByuf%M6rBYsZ*x%sE(*P>tff9t=G}mZ4TH6V}Mt>ojxJ)dIhH`594d%$E9Wq5qL8^e-oF4=sll#6w~#denu$f0N+plLB{xTwu;!gR6tr@yw7p4f5h9vqf{G z5?+?5cd^mc{0qN^PL4H#S86EVQSyR`zj521s=oFS6(b8IIGvzb)cdGT357&s6WvUt zCVrD>b)s&Gz9rn3uvWsGQ5~Ze(~I^!;-T-fx1Xn`TXEfhqh4BmEiMzMX-3pE0tDTfyGOrtP5@n>Kd6lD)xEYu?|TFtq)MNS8&{i&i7 zpO#j=>QiMj=>5Ose}X(;N-2#LFQc*)Bu^`f!|#63yE=5O?9%QAzXg&9>igHlIpX@p zM#Y|vnH$qPCTEO0=HZ|Fe_SzLV!W~2V*AC3{ssOlpz?!)wX_nU!sv?JHF|=8u3K17w{2T?#-FqB=W>wL3nTl>KE|0Yv8 z0QS)o|HAu>?GN_e;Hr;X^4WE7~p}*+I*S z(b8W;cN2;EZ>u+*3te;F{c$y3<-G$(eXZ}aZ(77{9B0JH(-Gq$q9YND}H#x0yBLLlr}b!S^R?RW+Yxg4(C% ze&o;`+6(rtL*T_ptO5H3 zie7ERYV;8-lJk`l)>^Na;B8AkGl0N)O`DZOjflo;bifInRr03h1NuciKRZ$c_|}! z{3Oub+j1_t2UaPclt{G+HLa&=4d-CzVdq!p8>i1@JCnK^yP{ofT(w*w=Ucq|>fv;D zT3w;$0taiOJRsk5$TLB>mY`bPA1{?+@MdaIZQ4(i7G;eDc@2Uf#G&ZyFe9iz)}$&p z1J$<-;OHq)V9XBV|DK}|dc6mQ+^Ca(gaI8b)u0opfRu_E|6JTBP7?p7Gw3Jz$#c}L z)}ns53KipNs5{j(R+B$Xz}dEqb}VQGngnk7yZbBoll$$sf2kWLjyoDVBX)A^gV=gh zL1z1-0=)tU@Sd0ztf3Xa&nUl<3$?j5=*3AeFrEl^#Lv22xh=AKB`Q(>~<&gxrsnW}O|IJMER zV`Q`m-S&9vty#>R!XDEVY1@q;tB%llZG|?98P-y(#nqIRIf~5homLP8c0Z0#f}Vlg zV>)|mB}P;;GMMeaY0j8Mh|btN@8W#F~tOMORFy271{+7>*LtU6j-^ zim9m)To6{ExN{0U>j&hLZBf*ikH5=j*8CtcvDYv#o|u1`^Na|_vWgyq7fs^OSnUhB zp*wg4uZ|*tpnnf}MoZju`qJrhz<&=x;EF`YN-`a4gU&%>#Tju z(S?ZhlekRUC{I!*tCO8sTsK{n-J9KYJXbs&yg$9geA9e0eWQHCea(G6eEocNeMQl* zXzMlT0`a@MyYIOgq6YIBC#sxECuaCjQ56dcLmaE^L)I5~_;rnwRH62gV@%f)Y75XN z%1wj{P=<-M_#WRehbMuB?4j0w3_}LUdQ}U-(7DZbblwlL9 zi&3b2uR{O54H1YXrUlLFLhsN~=?{omJ>30D$#v!O)IB%B&`6|smDJ!Z*X1YjY2G3@ z{R^S`{#srmH>Ni!xttZHdsDmh0vW1PiYb6uKis5^mY zr01yTf@gzgh^M2chNrcso2P@PqUVOYw0o0FWp6FxJg0V5FDluT5%PMR32MR;$}h}w zd_*&}AG5C_T&ecPD}Agk>$9nOs39Q~i55~S6h{&;!UaS9L6vfVu{((TS8>iq<5`dc z7lSPHyT;*?G@l)|5Sl~3$N;9JtM>;sQXZ+8R1d^+th5$Kt-aDQX^XT59Jv+yTZHrp zT)_>I?Mt zTJSr!h%F+;nxB2l{iI-8Bu}60Kl|4o9bSA8di^^quS=PTT^*`x^(0Li!mhd5_qk>JsHA(mn<^ z;2xDS7{b%;OCWu)D&Cr>anbyOmYS1n`;yig)U77NC9QA`+#o$#WF6UcucKA}H(7T{aGks8wvI&?a3r|r zI!5oJ`G%-5w^ak?$|=jT%E7#y&b5VHE+Mt4V?^QU(Co~K=Xq6O5-4OsF%KG327LT! z@Rn;aO7yJ|g+-w0SsOg?GL;khjld2I+8T3X2zBBjXwRkK98UrZGC4K3yeOO&K+~@T z`1@?|tNh?mYpMNzw9}$z{L?W4$AJFy!NtPQstp4BTWX5xOldd|hp1_sQ7(X#CskWg zjasX2Q2Wqj?;;~P0hZpKy88!tshohlDkyg4c`ifM@+JC}mpP3`l5^MPyqrMo^D4;B zMVygN!&R^uwe@6VU8qqf3dT{9zJfp7A4aq__1XO})C{8A8QLo?BQ?RTq3@w4)S7Zp z4ftyO#ofF`^=J|5#lz51E>CqvVqXXf^Tae%wmQj%JcehTSZ%28RWmymJAdLw+uJq8 zwa~TPwbiu_?!a!>7CxKcYT`=iqSMj&K^>|lSGOrSl+AL4Jdmg{mpB!D)RO2fSg@9*6X9Ge2vm4QsH`u2k&6x0^Ylw80AU7kfWE`rZ%l0oS{H2J&w&34Vig@xhRY^GX zW(l-Yvr*TogW~UA>QgzbNvPfitd7*id?0Yk@t!O%oFtZ6Nqx3E>plXe$_n;qm(oC) z0LS5z;#LzKnQUp1KES-B#kV&gN0}+T&JfDt}o(q9Qr3Gl=)ZPErQ>l3W>&n6|2= z9(Cq)9dRXg=XAGnS94e7eYAUod%k-Eewy{DW^X1&Oy;@;$ETB1bj|=BYE3;L3%#7r z#4==aUZSDb+|wzz2sVc`wustMV&e(b^p?1pKF1Yrb?791M|sI!j-W9n^V1?A`=^a8 z<_a@8imD#!CufP&T9XH4!j1MLs;?8p`BZMVifhFS^p%|;zMYP%eJ)Va*2F72*+0Wk z$|y1p75-HL{PU)O9kzn4HiawPO;8jkw+ejz&Z@!sKZ!HLC9W^1oVR8+c!^l<3*X4c zbAe3O5Sxo<#mv;(f3R8#MZIYkXXgMh6RL275U032pc#iX5r@aCp`6VB$9e&(_=QlUon;lUy^f2HD#9P3lPFUKD<=o= zpiikT!R6m!rErnaFjEP=lN{B@BjP)a(w?JN-iXC8Z~R zsZQLJOHL=hVK><<4WXh`kZQU>U)d9Q4HI#vtib8n04>#Ze0`2;(nZd|Ct{rV7-idc z2l`BWLVrKQ62t{f51J|{V&-o&~ndD z4Q>t$l1|jPms{JZqW*_s{t8g&GH`)avYV8aA0&4@>!%tOgI91%su20S*LULB)Co_v zd$6@$gR8s_p2hcdWN>V-DRJVTz}!F`oMQcc+n+p8k7#Hc{*XV>6Ak--eF~N)m-`65 zu{cynf2i*?+JUcpw8n8Nbz;^Z6lxJ24x#F^lc;YzIAb1l7DeN=tj?RRGH5NFcmHww zJYU^0?vw7V?(w+UmUdrp4R(EZ_D4x|Ke<~Tv{{BK$K+!4Djyd^C?S6(p7%Ib(zSS# zy6`W~$HmN~BVg4J;7rU0tBr<>QJWoq3wwU}+-!pj&uKi4suBAoN5}a%eP5}GC%15d zjR&U~4yvDv^Lf5}PtF0taZtIb1VA6Nt9jYsQ>um47BF7MtFzVVYFo~+UsM@K62nF+ zTj5`%kYz)wJBrOQ)N)FLXANVNezHf6rR(ws)n)~yifZ^qIpKxTod!c5?K0yj zwO1eP;E_~v_LJRR1aDWYNKX2mjNTFIu$q;^&WevyUb@=8Qhlo?>OQD9g@)On2E6Y8*E9+W9+QbRMceoa)!kOa0#)EAM9@=oft{{9-G!BLDVRYPz7Kz%08SGf@wySo zB|iwoSXuK~O(IGLy{O7wq9#$2XmSr&aS;5^0~V&q96BM}L&xdWe_MY@)T{)J~Vd z1^Q|gwWE1{1*xiDXVjL!UD(YTumSE;b1FZ7K-#u)ZoS9B=!fYgE;vbKHxP`vD)Y3t z*_CX$8fZgF?s{eN%L;Tt45YGGj?=p&4BXs|auod&zm0%#5B>AaFw9rLF5C;Ya{+XJ z1@+phL>W2pF8oE+QK#mgh_4D#Uw0D|zlU+2gNp7%I2|v^R^Gyr*pE-*1$^cd{@Z+b z6*<8`)=(36!1`=VmFEMF17nCRZ^2bd1p}csk>pYOHRFU*M1AY&cC0|B!&I0;holpr zg|YC+O2R8@Ow8U9hEYwq9A9N3x66*6bVf9IZRxx8TsqELX^fX@LMeb=@MkiM3*u%@ zpU&)fWy#kHu!ah<^Q91-#1jFo#AI_R#Y~)Owb5BD$G%vB&&qOD;jAvgBSl=+lf*wF zKdcE={7#It6Lvs9BB)BB0Rfx@j***mfL9hkM|O_gg*td)D&Z+mdwt7jj%TMVjEYVY zI9-025O*1&y^PsTBB?#-NuMPPz79(F8Fl(tQ?O*#Xd0mlkhVfb7YGgIT2crjX z9f;yv6Hm89`?R;!m#a083IFvJ{7zPCOig)2OTOEh8evy-vwK)0z`V!8z?l#4WiQu3 zBAk=f88ESLbjyUmXbsEFE|8t5pb}N#wp`u8BA3BidxnqVAN&+ua5NHdCF50=fq&GK zuIlY5F&}0{U4t+24Cf&ycOy4fUZFU;!j;Gfnt_B3ho!Wb3~e1*;ac+gwM3YQg=17@ z4zbR6aUI~=EgZq2VGG~c%g%m^s`WkG#9p)h-tgHSD%!V*P9Kt?->2gLk)MTs`ktTP z5w6BNkp2JTEzp$%O2<~1i0kloS_Y4FGh@0PPU+tG z_dDU0{^Qs{KlI7?QNBzjzMnBaM33|hIu@zAvVt#jJ;Kf7Kd!Jd)pwk!40s4$oUBCh z2nQb%p@f?SZdgLFv#`6Bi}w>@1-W39`)GQi^5h;LB%{bpXGx)n1NZF7`{w8J}<&| zQqujT!Zc6q$jlu{$7dd{RB)ft;WeVe8+*<5nVAsF^_g?&IqphVSlNd;agV}ZKZfJc z)p+-&n?y)w>2Ez8|Na1X{|KL5;xxO(@7&;VC-~WEX4FOg!dX6E<>PgJ_BftfeV@;s z#Q!%x{EbA8G(4+JtgxcsisiVX$)jq4EHz}Fc7sRKhxe`cswP)6&Ztg|X4rwM9bdKN ztN!e&lenhBDxDiYrc1d;Gr&)lFuFTING~vUJ00s8>HU0ujko>$WFC*$gvZcUMtc*k zH5209twzTCdyQq4^y2??;{S984;T>ddo_$ljAj<~=c^$+qAxMY9I6p3B*!!yl zu0Hv=@-trP8Kunh9OgwyB`4o0!pF3HERK^{3GP@Ge!ockBZ{)F!~RxvnH%N!TZQO6 zF3#sQ;c`av{qQ5pkl|M3yG3B=72#2}nMIA^dDi1+rKz=qz4*d+whPa%Co1|a<3I1t zs~ya14!etW;rS0|C5`5ug|8{xvsvU>|MyFq82^}2#2n*!-4l7;;XBrs?}q*2M!=~G zzw%y;VK4sTQ2u84tDf=hWqsrCd242Mb2R0v#y{iA@y|V)=U$ChQj4oL&$d?l&!V{l zVfUk~%=nZzeTDgS^pmrv1wiKF=&uODkTK(HM1{dBBiI>|QT6-HcO)Vq8(wLElleQ3 zjAie53w!b$^Z7J$`3+xx_XX*e2*6{LAWal5OT&&+SFL^Y&2y?YrPNvPgr2G6?T$bs8REBh$S z&A5a5T=8YG7;i z9jnh~014&Q%>BsQ<0DGje{sr*mc)J5i9)ksRGQ z8D`)V_BSpW`#5KEvFqI*G1f9%st)ma22PBW^tm_UJb4I0o5M~TpWUT* z6vAa|J!%4Z?ath@fy|Cccy*1pyMU+uVO~y$gE@oK8>@;W* zj3XwPPR%k0@xoF@E0bdpHODH9(JgklLUuja?BnT$=*Tmi%6~e?^XrH+*a@ENGW;@g z!uCncEE>$%EVfV5DN%tOYM~uq4@$%8OV5no=|}?Ku7KkYUgIU0Z!7HMR0c$9D@&LO zKkQKsy7}q27)w9VH_p{rj&0&U;!KA?bo3G*rs{YmW#^1;g5y#$b|8-s>DbH8e8&n< zHLJjWn;LB3Z#ubSP%=uyK77crU+9A`mu2N4BiTH;AY2yAN&S$a3_{(JaW z3Qo_d?36-t7|tzu#z(M*N6S*2k6?&kS>?FZ&Xt2X|e|1gh6^B)AFz<;eI!g*Hv ze*R(`x)1{N8~3*VW6#LUZ21p%*LOP{MP;+Em`!+I3E3(Bpc5Qz&!hu;JnP_yJ=c+u z`_a`gm+^JbbKZ*lAv@!l8T)t$*K57Liy(N4jx zQk2oHZr8NFl2JCZH?wDsaPZvevp-B68L(5JY6qEQK9c%#~!L+3*H?M6{Nb?)jQlP9l`d z;u@j}*)aq!r|M>2dooUJc^pprC!C*)^jloAAFu-_vTx{xLgmb1j#si%&X4k!Y*n;A znxjxhX&^2TCZo~N(I{XGa$lvYvQbp*uXyXMvGd^6oJ#C0jI`@o2hmc=2x2)zEMlKC zOtWb`4{VZShVYfwaoucajdL`Sii>;A$^7m_^BVWwPw=;b^{5c}Y%;S2b|q=XOh>jd&ey=tc0<@5V-JF)`&EDoqbj(t2pBMCg}Mswo4?sd5TEBj@X2 ziMZ43m6S}|w%4y|s?3d=@xO>6MX`T<2G*ie&1Wk)Sx!AW zTIeCAl)pLpmkE-n`1%u&{ zqmKB2)%4O%!|U$EDKm;SGlLwb2|7S^sMH;?vx4UDrTe@gbEt~-gW7PAF7~j4*+5hP z3plori7G-FTrno&^Zd|WX$V$1d8)IN`dMu0_y^277FCCw;&Q=_W=k>0Q^!uBy!1sp zA&jvLkwe$QXXY3u^B;Dg((DR#96iPEC@%E14wm zkb;NGekR!xr@HyT)l1tmjAiEN~$3Dj> z*8d7SyVcI9V-&H|NYkatLOIk`Dw|uaQtUA~V9p1uN5-enR3nRB9c`h6!X`&Cy9fNM z?^Zb4DP@1eFS?`o(dy^8AY5UOT#1L^X>!lvWPMX`ooY$Fxi@=v0oZ!;aVLqh|Di)U z5?0$e^EP$pq4>3X*sW_2-^{iL!I*jupFh9Z4n}xUd#^dztZP>j?l@Z5elwvZIu6)( z%p$l|ZAbYc%8|g{NN!okI*;N{E9TWrW1GI)7(ky)Lb9K$QCuSxfT_5e+TwGx%f5?aq}t*LVwDf5VdfTpQS(A~ ziszVw8QT}lwWao5DkCe+EtbGO8ELme$D=cx!SIalN*>QWpktz`^^>@Gk=+)I@hV~2 z8?z*5*mkn|+*Ae5bEgm3mxu_CSu&cP*NvB^50$HZ^fMJ^>dPZ=s>!L>VqN$Qk6s(b z$lcH|X4e|N9!tIXsFfV{SSMjIr&$MEbBu(MGMrPaJuLVJ!X@#ToJT1FC##B_O#TJG zyd>3~k8}n-7h)Wr1cy8fCf6Oh_J5LjD?(oJ12x#ZVC5kpue4RJ0=H+CoEua#1g^7^ zJHHm*O?u@moXmeYlVrkvJtcFVRc?1<~|Z%107{u2D{A$*S0IVS5D@1Ms~cn zMkVm6rsi7X3cW=QjX3>29(3pR?9_Yz;6109hm1-nEp#`=;f>UgJ#4AzLx<`WXjxNZ zlHLyuhKkl4)>}0c8t&_7jmh{5{zH9nE2F0Ao59NpgQ$Hn&ViT}rKjm3{NgKmdAf%r zx-Qb8u$LQ0HwXGQ<@K?lgkYwZLVxS6LD+KZho}O1= zH4%GyU{q#9<02RAz>vI0xd@ZO2_C&n9j$i539o>B z7~Jy~9!|-qn2(jB;UUOww=KCRloPf?DGBN%k69I zZR+Xl{s`~>jH|S(HZ_Z_>N7RBGqv-q+L!9VS{Ub_x~)b`@YKW`ER*RzJ1+hN)T z>6-M19%#w6alzC<5B%5EK_hTF;0-iGA^K*V;?LoK0k14!T(Y=!aqr@$`@i{<2J!|z z_z%Mc-HwJv2d!x+1$S}+9J~#%Dkp<%2N?;=8q7I9R7i!p;8!t7b@dz_HS6Hx)B#<+ zK(uj2!_Kw}t94KlV`jvx19Ii0Oo8Mt^^P zzZDl3XT>G=7x%ZN_g@T@4m8A*E`1<%pg>?rph2*RmO2y)U8KJu7o7EwHQ7-q>`hD8 z)sV$7M8e)Aa zaZSOLub|bo0JWqy#EHGh_PulqWwm#M;|5S5%1O6mYdTw((y@l38g>41`T_Qd^;%}_ z3Y_wxD4simS|Dk#J^b>U@m=*Tf_dR9`vX~m^MZ+}Ko;h0r&gXGqW?l+rgwch6^m0Z z7)ftkBCyNy5ojd{uVHoV5iM$`kUur0p!hNzVzLTZM z2pWnN#o}Tuv7OkOx5j)uP@F=C`7-|ZU?Sx#xB{l39#9W~v2gvfva%a-nUiv;Rkri#UHu35XQaxP>2EUeTsWpo{vN2Ae5=eh` zK4t|u2-j18;cdH(%D`>vI&Y{i)+8nvOV#HjC~;X>HRnNg!`COW_8=(YZvNj6vhk6; z9p&C_=QB_SqPvpJqU7YMiP>kL5xL(d_BusgbrsA%7tw4Z@a^_s%dM%&w4e^vk4oGK zvi^DSj<O+_IC8Bo|Wu_8>!AwpubZgLp2=%p=fPNn*BkSgOSX3SyE#oc;wm^jnvw_gi8 zu&?ekP8qXsRjYxrS^^@v{b*-=kZMp>yGbSRn{3Nb%3tuLt}9ij0nT%_b!Bt^a9{Qe zhMzjq_sN$a!WD7F*Uq=zy9FM0th1%MLH;I|5pLRRtPAw?PUgIstQXatx)1gBZgk8q zW^FaobHaOj5jr2*OU-#eXb>G-S@kRWLSu!w!Ph_1uc=7WJtcw^DR@ARQ2i z(Lq~{??WCm4xhq4e?^y97pkZA(H<=iwvmMjn+R9sy0HaLPBd}WYq-<fsTsq!#!0z5X1k){Lx@`1&Km>&iVh%BIom6@4Yc!m|oL92m`<^W0i3Rbg_(a%R`^dTya zIjm3AiRZI6M^pWs#5I+Q_flflt*p^QjMxFxbe5UZ;#coPs*Edfm_17E>Xqp?W$I74 z;B!=>Ue$z|-GHvH@>H-gFu$Fao1bRnr!`;^Ord`H3V+0!U`$tu1ZskRoFyN~0gG&* z@UP%tH4dgrT43!?paSfH?>SGtLzO=oZqPcIR6iA;npG`}E>SBw8btLbN)cO>X>?!} zVoc7VQPhG6@&~oU$#kh3)Br1r@4<3%!UJ0dUZD^Z_9F@jkdrLqmA8QRlAE41C#x}B zgZqra>t(v>*6{i#5;ydq?q7rYX(4*}(y_DUFpDs<^-=8W$30q2fBsE&Q_D;X!>%Pf zx|QTvuQ};bQQ2+EdR)s0{set14SqZtl>8)>jrY7_2U%ls)=3s28*iDp(&6izh@O>% zLZpy@E0WJ6VG6sbj@VSMemOpqGe1Yk?Gh*CUZSW4M7X^{siVQf6M!jPwYd_igzve@_;4Ki&G*lClQLVIK zIK=QK(5sf*O3Mr>2Di07V>pccYdt&LWqKLotOz@^U4*E+IlIXK5P_MXf}82VI>B>! z3@#j=pT&8OLwUA)s5*Q?Zz5dhZwCKlwy+gu^cy->Q_&CI5)R8+*egHK-z_7xCzoCW zU-S-rkwNA`A~^+&h;(orlf&<`VaL2e;qDL`5_pxMRiL4Su#B^!5Ujr&R2m0yb|q%a zcd?SnGUIQ7BKGFAOA6L@f*N{1?nORc=TF!j=b7Kz;L0sWCv_Tge>CS)KWgRO$QQ!z zyRoLbF%x?5-9fC6S@Gk3jhIq1qqs8_m__}-6T-;VCFb`pDtVFMw}n81!Zp0%Ai=A- zCl|mmo`RWv;lBJA|NbjD_XjZFhg9v(fH7=m^j1=RMX{dyxSY>d#J{cJtEGH*0e@*4 zi0xQV;Q`bF+v4QhfL*#y{8f*0xemKURS@9v?D9pq3NhF7aOLGKANf&n&ZnyUPJ7rr zBl&B~V51(Qj&+~i$0TbCfWd>Mf$}=oOEQzG*Je)|4py*-nTy6F5kXpJZer%9kGwED z72k5OXX?Rw=_w3lrOn6t`Y_{qC%&q$fDa`DTT9LRgm598W9<^&%>wKMDIkn*3yAm zNHOsJ2x_@ssZ(6wZf=2@yol8>p0jNTSz#aI{!W~Bt(mRO@Zhe)K2#NcMKsmc(%i8M zyj5l{*8~Nv%UuoM&$_Im%ABZ`;_qo0R(0|CdzzaXZB}sD)F60?$p7884C-xC*Nvst z`-wC14H4EoaL+5K!JPuh+t1z?K0h}T5p3f9cCbOTp&H+>-$&=TlH=g1QHUkAnWtc53V zXugQQ&~t$x2Lmi1{^qNn;vF<}+QJkrkKas26mWkFkLgI*LY;mBE3z$&j0|M+r-;LA zP?>o`j4_!xoEhb`bHp%xK~qy%KiDBRuu7W4B*_CRVH-c_Z8?Vq)GAb<#?V*Sow&6j ztiHNvb~WLg>uroMrowhy54Yqj+@u$%4FuuArJ^T34_zTu=#gm$GiW4zF&jBo{)KDm zC)-X-Wu-cNhQ4qW=M#S%;9lJaL5mAx%3#d-h;*VEu?CE1JF4S-?7^&z5%y?S$XM>+ zNOWTcF!FstX}WSx+Y@DWVAZta>d4iJ)!B{T8o>M*!z-P`8e7eZ*h4&ZhU(T`DqruI zPchUTWbmz2;Hrh$PpVUi{eR7MI(5#Ctc~Nutq&NTugp-(;f8aNi8=chF?0hW>Td9| zhtX#+5pKc~m=LRZ+raL(TiAo%YnTObh>zi!zZ1M=3rN=*VL2z_3`S)HtDrY3sx4V# zRr!ngsivo5Ey#k#TKGWRevR?k#d)xZNOUN%SW`~@5<~?ls56BtM-NyH2Ura&sqIYW zj`ZO^H0G|AV7ORTLm(Z7NFSEVT~MNXLvvi7z;w46826-_&E*8n>vE&PGhcb zpo4xJoT1g?Dl*E&sL@OkM-e{^gfrZY468T#8UtY!^~Q6$6|B_;MDhj2^z1@@_NISf zo2-v#^E6?9$VQE#AMbP$&d3A8Io9P-Dam- z567ez&n-W7_8{x|I6KQo_SI_a$H}-4;fnDcc8GuAn$BkD9>h#;O}9rCRzP9;KeBX(saD@cM{meN;o!q7f^jW&G8G$SVAK8xWE9%=~U3c;TxX71i+j@W=3H zU0FH(ta$N|{^G&wjOd4=v3GJ5 zeK+pdLgJUh>@QF8;?RT$?rDCIgyh6O8DJS!;jHcs7keQ)*B-i`F2gW=Bfh7`aEo=c zlbviPXvGLR)q1lknzH9r0)TW6z@MPP{mfldb`Cwg@AX6;8lA?!-D?`y$3?IxNL8@x1`ksmH7$E;+z1 zza7@*Lh`kdjNTybZGZNouJL8L|4HuYWPx zc|bTaGs0nvqc~%q6uqsGsaQU8!<5W|Y*dTNFf(c~Qa$0d4u{h`fE~0ulv3jFhwLz=KP$>9k>H?Kn_oUAjYzL`Kax=STO}y zG0i}4h7(n8Cd#}Adr%h=6FG&u`SQ`#l$E_FEAzh$u~9WvLL){gjMjybq4tbIL%v&! zII9AytT-d;7Qettyv#nei}Se~=krGN=quB~)|>q)2R!=9j>6W`P(k9HdU^_dt?>b! zj9k`Iu(3(#x;>)SP=-#slA!*rLbdhbXpbku)uar2+Haz+PVfe^v1?@IZ6}CNaq_2w zFd1^wv6vTKj;!=$U1E>!!-;tr^yL}n)<*b6$*9PcW$*uOCE|Qq#A^Bi(iUa<>2diB zOzsfTSuw*z0jnJvF%Qw2j0z43Rne6Q*A5Wk*FH+ZO z3x~H4A9K**-&^iXmrqsL@j>~eoLE_clFBf8xOynvl+}u^ltOE;gPH|}=U+-fwY!?w zxzSkzHQd>*$@EHHqFQl@KDk)*haG4<1>H&Ad7QuHv{Fgfzf0wMYFX!RwXE`99OGCI zx?!2e=s2_N;eo*^?s_Je^beMBwq!oHtEc|N^bqP{7xQ;27?XD`abGbZbm6q zzNoZN2heF)n7#xjRiIckt8=rnnd><2h|^pdU8!6fUG?0*+&4UPz4N?Tyq!Fs-G|&S z-Pha;+-=;c-RWIL(LZ>g3eLLDCeE(vDfyiE(=o-it(R6gBI-^sE|*%bjnw)Vt(caZ zs}rgukF^LCz}jioP~V;&JRD4?-O+lWyqy~5wk&j%{6y{f3tG>U=|k%SV&&!hK5x$^ z@;(ix>k+X)HO?Y0ILU0Xqy1ozk2$OIlgp+8QCkXYRU@uB3@Y}7T`>dCCWe#!3Mgw$ z@|yYRKE)YJslT6L^f!h63dx~g+H>uuwwd~mKR7d3F!+_uvTuRf!6o$T{y-;TW^g>( zE?KngbnXrf9i@uZQr{1{x6{bMdiev2Qi0to2l;4j)=PVyMQeDISMcwfD%=L~`oLMS zgSh{iP?*!<2WNQca<7y7C}q`) zYB^_Ty!u?u@7(Rm_{F86E4UpBt3TblJ-0n&JhG>Z`=rZuh1`X_b$n7pLf=}~Fy%B{ zdr2wfD(!vjP3ozwo)dCdaYj+=0$wpYrF5chT&VY^4oTGRS_I2zcS84#UgT>B;0z@-&l&V0 zSe@-Q>>9f`Kf95?%P6e1MXRSGxIz(O95}#N5Fv}ZGE^uE{&k&PG&z1;JMGROQPG@G zUb4OK)(vAyNXMJ8bMTIKUq6g0WAc#SUpjV8%%9j8|NY<*&8w9Q%!<=vlf-?9s~%_> zd>&j7l!KoFe+PSO`9mpDMNMHeMsq(uh>nJ%!AffgGpRj@R0VOju!I>i#!-=R7%Giq zr+O)E5=#sFVUAVfd+$-E$xBRYa6Z%(RCu4(Qro($e-Dt;Xz?nGRS%$1;20zE?a*7Xc_Z}J@VrARO{ zszX%O$mZVSuG;EKr6sC2%~34MpsbLh#fm~3M`F9Kxk%3z8cerwS1rG`H#j@6z<)I^ zZQKoX$t%a6idh|VC1z-BJE~%*{PX;G<6iLn(dd+C3v3F$(WZfR_oc#|G1MNtz$^L{ zW3$x)E~D{@!81r zP3$jKkn<@KsMF0;Tcg(g5-q{O&f+Ma&GoECv1F(B1!~}Bd^vq`M5@T*3G@W*qRu8L z8(BW$FyHwvVq}6p2?`|`9nsAD&OOLo#IxHo1;^~SYJGVT_~w1F9PF#xj;dA`;|3Lv z9cTb{)gpohYp4}EIyL=k;!egUj=d0*H@0q^)1T45BF-JRC^i;H-8_L0D71a0qcsy9 zSGM*(G?Ko!D`?girmJd!8BKIl3^ebJ-5xZc0BCwx3nMELW?s4m?^89qPXGN@_%1)C zByzM|2_*Fnj)l?a6}}<*ZzH}D3_3Flld(u(VMD1Ny)zdZ-x5ifi@eMfwjFE(OnWVJ}$m(|3;@1>H+L(ViEs7V1T*fEX+E zL%FEFxYIGy+8XA0ph-VopC2lu{X)N?MPL(GoM5IvCTfy>;tOn7Cw`qkhEIW zRX3fP`CIHQ-C~`+LC35qdrmV{csHSnF_;~wuUKCQYXRMMq(G;#pnxI}+?F-QU&c1{ za{n@y7|Zmb?BV}vFSKjg1QaKSqh(bcHF!5lWebAMf;G{cEXJt0(0eeo9HB*M!kGFn z@VaK8cXMF+HMf#-uFm2r1RA;vw(l(dUmW!a7h^V`zMKPKhD)eSG-TFQp_+diEb$W& zt4ds$oebs<-9S~*J4i)dGmM_PA!uY~1J6k=%|oSS5HoQwbF{eJQXVW{m%Ax%l(a;C zxzyfhhF5TwNB!WE^M6I11$fk0*MP^Z2Bj2OSX_$R;!YQ5ab4Wq9Tu0xt+>0pyF-xz z#ob-%G9CZllPurg`y|aInIw~QkK1G7CT!L)DtWH7ma#RlRkEeG1=;Rcdsrt~L-EFE z@XPL3-j?0^)tpK;WqZ7X{mnb6nqV_kGECPMBKC6xwCMqMouj^a#JO#vg6~%9A$4}% zqzck$htX+q?tu~&PupsrX+LK#kT#ZjZLQOar8P@CMRlRBWcDV}FXVgLXh#vMqSrx! zZsTh2ngJ$Oa8IDR>34TSa=8YP-BSWfj1zC2R0Xh*`S2Xi%^`d?AF15j25q4>+VpF9 zUV_ZW)) zR|5NTD3-!FtPb~}9JldJ#1L`c5pA{{lyVz`xM*zbw|VNFM25aZ$~}kGY#x|78JMTX zuYL*L^#a+hW6)dAQf(%KK0lSBmr-3Q1vxUeLHslskX6nb@*&}`VO*k8TZ*w56t*v& zDlVJOnNq3lSl?XA97yehm!^-VII5dy+1^8ShoUCx{ZrN2P4rP0Lk@!%O11%y&rfu- z%h*_vwuzh%B1-cn@?0vGf@o@QXY#xP9-GKz9PjQ)&1OI1g4*)#0;x4Sl$zl0$gO{h zl(v(43Ac!fNFy?_am zVqYkUPaqsR*AFY|7BsjQx=(lvd^!u2^vjXWI!r$w9&&|o% z;1uHtM7s;e^3(;J)Cg=+3$QO;^gdT?v>AX`DJ(HVu*9Fif@vfhr6E$&R4BOAd3%j^ z5=!PkLwp8(p`rg^|DOxI_E1gZHa6=-rPfUd*1nuTtuA)$iS&})jRo%vwxjdpyhiC> z!7qIHP{V*)Evkq$B3@78V!GogovNQl4fjPT+i?5i@&gp zWI}z^-+u;nX2b&3Sn+Ew#y)%opWP?27u_h3uTx78*j;g^kAd;4GT>L^qD%yFtxEf z)nxlGa=<$Q(-C0dTzoq_kxFmDIiG7^6VGhIqEr#9Q){g1J@Hsf!k4j$y6&fd>k0IT z?bvSDAg8V6{}%ins};#$0(^8J(oi4fK?lBD5ASzIvYWplL*2xCvI&km317J7+HEN;fElR( zUgwEA=lKxcF-9MYaeve!BDBGNIDzQ z%q2tlA{N+7jLXu*|(fQBtDkHE7%?PLwT3OKmWz6 zH;5?d*4(Wi_FEqs(<`3xB(}>XV9`))8BNJgERSy{4104%@JTZIBtPs1l;bp+sK@aa zY*MndW)eFzk9eN7_z+g%?;eL&vmI2X5wo{B|LfwfD2>%Lmy)sk8y+I@aD=kX2J}35 zKb{bw`I>qEhX1$mS4UzIxdz?6hRkq_&u6ij?*Oiw8M_%<;d(3KeybRC_T=oVK>h^Q z^^ZtDUZTu0`d)FpP`oi=iYL7zvu89^{U1CX3z2hn;1RrzetCoO4DYIBb%s(wN~(E( z=W{vcW(7Q$N&IFHS!VT_u_qLpK_SkphV{KaR+K^f4=4VmF@B)|WKi}6>f^B&%vADP z=Mc3Jfz4+Yd*<*~=Ygq5z|~XG+CAi|96^_lLXthh=S%#5f-m(rR@9|L3T?)IMtnaR z(tFrm&i5CvOyWwj$a#|NPk~NJEK7ySG0eo06RKB)krP-YV10i9{S&`&BERPq-1$5_ z`Y^S1Zt|<&u;&Mn7ALW4Y$irw2GH9FwR`|nPodeiM)!Vze`q}Z?S*VFBqrkztR6Fb zC%qZKw%k~gwsDPe_`OErajJ?GPzS7hftP6u*C@g@{lJNVy#D}A6nEXo(V1NBIR5Qk zT(1if{W$Qs23cg~@P^M}1~p)wr1QIgprJh+lOJh6-6QF%kV{{t6tryIS zC*Foogj(a`d&~kYS1!veY{s?zgs1!ACCiR? zEDVm2pV@ee`F4|NqF~Od<;(A0=B($w?QK97M|)kcHV1imd7;HGeFM?nL$v#eWYZ8Q zE3u1H$mPoojjN-534SCKv(koq+W|mq1Rk2VM9Adf`mOoJOQ7R@;Ju^4nWa2!6{tsw z_Y80A3$pJrymX1)no7J!Yy7CkyrX#6!-3cXVq%JuOZp5ipC8y%2RPO7Iq(Ty^)8|J zwDh5@i0AJo@6ZS=wt7F3XLuJVPW1&-r_<#7i@j;^k9uT${zV?b7vBhNF5PnNXw3t# zDxa?%S85>kQ1Sy;Q&lLK_q`C!VKiM^w`$Y)o$qmV+JPYr7xA(NKpJoj4 zHuK>*BYaDU(0mQ942Oc$;EiU*hmgwEbNEW*p&8`+6P=`)B(E`afImx&_7(32GLBxk_j&P3 z;5n~JWI{nI=M3`w<-PCoM^=bHt7)k1M7L@^{<7uT>DV#T>+)z?QTZyxbKh4|+Xin- zBVBpAP!Ho7%aXwyq)D&MqkE-IM-I_muGN!Yw?KQBN@DXhf!_IKV>QxTMgls8|I^MJ z*{wYFTYSVp^u&$9Vi4n zJ#a8lp-Vl(o%!j2LFEqpLW4`w!E=(nLH&H?u+F{(e$hnZ&LLAQk1y8yJ2w0m=y17+ z+kWNCOGZy&-wEwoU5LK3Hn*pftDgIw_mySKhf{+OkE4)3p} z+2I{WPQW&8T_W5jY4v35-bQ1vXg<>?p(T;pG5*9SND620E@dEl zV5?|pHa!pQ6wsFQTgH_N-+cN{Cl3ovga^mh+)i;C$)hv|r*a1gH%sw<^A;T`RZ zhU3O)Mxc4TN?9&D8WI7pa?espc&-5*~l$(;56Ui5K+FRngAfNiAtJMNw^Zy z%N%5gvbsXRuqqkQEwPG6>TY4x%c-xZ3q&ihkjq-zs52DR4FuMGsj^3>RKq;1&N~eQ zu-HyFETCTK2IE(%T@IxNZeuDH6(pWEKlQc;P-*WJ`491i8ph9ffBXzaYE_Rl%%^VZ zYJGEJIehe}h$k|m4-rjSwCjnnizZqq98U8N?$XBVCf=*OdxNVLR;>@NAa{FLGkSRz zb2M|rI$AgvV{I}}v-yQ%m2)vQeM>k`(uv}lV}^4Al|~2QyNaeuW(P8^GLtD-fetl$ zT;XI;y``Q|efMIQ)iu^x)HxOZ&l@_ygj1VZ?`T5r+O28&w2Ssr_6zhw7-4tYSERk7 zm+R=XN>tuIPbcBLj;oF~&SCVcvr*sJkNWp@iA5WPCK%}{I# z)xiCl`a`CH)<3M(O?S07Jq59~1<)z1i_4g{I{8fEq{J6VWwDUoO!iM|m@qKDYkXw< z*Z48cw=u(ckOC&zz!2}lZ6VW z0}N-3(bVy`Q8_YkSNkZvi_ zDwo0N%DOc1ya1b1%>K#PgYRg!(^b$#51C^6N?1NO8~pHjgN(=ij@|`Xuw~sIKv&8q?-#`BRCpIGXR!r6yeeBe@u!Qdk z@8i9(RbzI=3XJ$H~y?3@rYm?$lOqbX&F;~*g zqyxP7UP+^pE+@TDTAh@da4_z0?AW+0iA@lcKhP=rUFz|a>nZC~8>d}$__>;q!*Ijd z-&My`-M1V{*xyIxDbEXcch4yA3VMHkB;VIhdmj(vXkS&j*Jq+8{C#(zw>0#uh&S5( zuWPdNlQYU)l(>)q-ksPTOMA|GZ)g_lMlxeAQ3-2+Zl1oDv7Whvb(-y|t-aqe|B(T^ z1A+th1`Z1PobF8Ugpf|5zh^ognjE?ybaUw8Oy10$fBTTto$XDw!P#tCTp3aVCRn96xji7V=vb2dw1LAJSJdLfJ&>(SFyzi&!+vm?=Urv2} z_uc&S+mACp%rV2`bEXtL zqzV}J6Q#3Sx1WCGf6>*Tv%ac!GMp?6G_n-DO5<7LD(TD(HH@|Qv!|xMOAY0}m)GOOwr7M-%<5D}Nt#+&>?t+dYu4z?Wl}!#-OE|eqO8HTHm&uzS@s% zA$}$O#`|sX%jusk;6gy#z}@Glnl*1wshpRTL>9i73qxca&CI}fGa zNt_i=({&*ua)kpzZwnh@Wla`x;Y(<#$ zITAL-w@A2=m^1lAa>?Z5iQx&0>G#qmaeh)jO8?Yjc1PNLXKAu_b7O5y@=o+^)x>B$ zx^(p2vFU3d)s5FTrY4`(Sj;e9w@g#OcY_(+pPZZHbZ9x^=#sVuAEPbRnVcv2R8p0s zgv8{;(n-sbE+zeu{4M!G%FfjJ_Cslnsla{8F~B*+b(B8evECy13TM+Z;E4X1p&EHV zImmGSV3Bha_K2I+}DR zsa10Jlz`Mmc6(Y)*Ex^dXVtYPHX)L%?n-n69ZHq`<#a}9Whh9ew4dZ{H8T{{uh1^? zb@i-v<)z2{F8lh_ttlUq*CgM-!#O+oK+?R#c?tL8+r}S@UzhMUF>7*#l)qCu+NY%L za9nflA#2l!z3;rYIe8OzG_}a%cNqqo)|&scRIs!&=P>o7H}50ETf;(QbyGLGd~GB{ zsJ^kVVLTnSLi8W#=uwA)8N@QFU zdMMM7%*HGMS$vtBWcmBIroWBN>-w9z)oR0po#&$`9-FFbp@cB*G2oxx($ez!*_ zMaK2`nfb@PAC8#9@fi}v#$S$o7BeAsPW-jRjmaJPl{Jz#BwOtFot=Ht^fyfBEH$jZ z5s$Im^2w6TI@%g-jjQWNjZ|9 zC+$zZm3%YFlvq7p6SpMR9{VBgWBeoD*MQ`Hse96%JF$GzDfpo$7xL##Z=hzLwuC;x zaNm^C;%r)x_1i0AUmcfX^P?IdRl$5i{&lU)$|L9)p8qvwD zFwqV{)WIxd)EPfeaW|8(I$dJ7k;UJ`(3jp=kyOk-gGFXJ*0^=r9z>wl!Ujz}dL)9+ zh8c7liZ**JAFaWDRf&pd5NHh=k#1djTZZtE!x=}1z7I{8X>g{^nPz12&zw2)&CIJZ zFVAGj*ereSfDG15rX<5WV`VC*M(Eb~{&vUESufsxI(1g^&4lRKS~1&ywuq?`yF0dg zY>}AfKhMVKWvJj#ff_zXfX^{U7ki789LfFYFLpk$V`A>aJc*kVhb47Q z&Pt!SW+^pOdQgVTpUmitcz>I@Ghnm+7wf?`4OuU`N#IPFp#xDpUx~+=qq~6}(#U)A zVvG7+Q;-TaPux#kan35v1&({vM6LwKm_#p#X{o1DKd0uPo5y8)W@4qRjtKfT*Q3tx zKvx+!cNjEs1NusR?4jl9^m`61aj8;8aV7eem#Ed}SRFHy$MX(fQ3U?UYd~cS@!wss zo*bq={V{D(Vo{&zI^uaA1uRb(>*1ekPY;wP%-!#ns@Bz3k9EDRn%_6S#{S#rGFB>J zLcpVdY=I2|mj%8HtQIsZXlu~DpuY5`(gk$(i?)uYi_|gm8Tx@)%qNXM^!~a|ny%ES z@8vz}S%vPPb-pE|xxf8mYJt>@DW#HkC*4k(k-RMBP-;{AczYLnEn*MS>@U-PI#QfB z$<$xsw%{KtgQj)YHyPc(2iDfScq8B70eMW!W&|Fk*;xKgql4P9uKup+jwV=(N*oEm zpck2$-Kn8|n7Zjr=!|Kn{=#9pW)^Vmr$@z6bdZzO?GJDtbFHQtx1Ow)V^r6@N^joA zbgZrE?uMu71@WE9RNcMqUQR{!!c+u0Nxt?PrFyrGe!d^+Kst)vZe?9PsR;4amB(Ef zf8?Jm9q}1&#`hOZg$TVTpOR771mB;Q8ughyd=uq}ooz&f+IOl`)y6v1 z2al}*D_9pSl@B$=uuLB&B54#EKUvY)W>O2)VyI6?N-Z_hV=J-yT0SWjCEAy1_DTjJ+OBvO?B1OFxdI{ykp^Ubn;uynI%EeGkfo&;~} zZyaD4svoP{jAy@rW|Qxw_qFGwdyuQ1v#g_T+ARCm)G4XqsclksrDjJb+d}2;KkUh= z50P}1+H2FJWUj+NUhM~GXRIxA={5S@GoN_MN#v&dAdfmgs>fi%OCy#y2iG5hm9-;P zPmIWFVEKe(hf3BfK74+_!dMon@dv$(`fIw|Xgn5TH1Cp0wwkWBGZ{yL(i!6TsDcD- zZGrc8A@QL*iN(;6f76xhnMv5{dV@m)@m72$m*YOx+@IJ%dtv{528^VpkCl$LAyCVf zc%bJ(F+#8`w#8-_tqmi$ZXP)*OUWkQOb$hqt_%?l(qrTuxv5o%T3JG0ZM`vzg8 zS5xIXv#B#(I!>Ez5rq;+{K|OqDRY`R418Ka{OkwnvE;WFrzUw>YXNJ3^$n4uN1BhK^L!GIK{-g*H~9Q0@+S$27eZ%B2$ zBDQWfJxABM&JfS=mwP?&D20gqo`z@oF7b_d@uT%ZDj3ccIuh+#mALzG{&yvQe;wHm z=YT*oZzc+_&lRBK@CM=o&4t}76}UwL#ZT-hi4D9z8Pr3FzwgI?$-SwJAIZsg_ac`M zB=#W-G1g!3)P6$}F_KCA5LouXpI*?Do=ocI#J?}W8()mdIrV^6FL3G~;%@(?JIQ5m zss>iL20Up^VqnJMQ`m#I=`_!^o4NADYsQlm!pz%1tjc}j9kM_z@ z50A*eGih_-d2NpG{5#Q$#mJy&jW7EIIY%LMV(UjdLsx1>jDlKj!ya0SDD)N7OOXy0 zWsME!LD7>a^d8ifKT7Ooc_LLdnr@iB62s}G;y5MGP2Z?OejXSPHdQ1G{ep2ek?YN< z1)h^E^eaSEn+$V++(abnCfd_H;YPd!NnpTq=y{;`4!QIbsKq^!oW~}-l}M;jS9n56 zMtS;I*QCNwH}@FoYTu=5f**aZi$Z-067i6OPRhmbaHoS$JjYJvPn3d0Gh8Hh-H&{V zqEvyN2M#SD8f5^pz9H19D73K__pQ!$ZQ$LO8L)t{(mRWsnoi(F4XCOQIE|!NTmY2Q z#P!p0&(?I08w!L*dKTfUEluod6MKNo_HB34rb zOXm{!c_AVl+TaOVt(!_-%uM1_))D<@q_*06aPSKe?b!_F!N3cK=g9X~x-l1`?ol7& z*;g4i7*C>&=}dv>XSGa?(8_WV%^n3**HbILv#|)e`gLjs3?a6tB~^#FY5xJ zHx&qN<-R-V6W5(+lA*wD8b{A@^dGXMnvh3W8#{O%@(cCeheR~opo-fWVp^6nyGD`Y z{}?QM&H6GK{|%@L-_|n*iaHk_whs>WkR_VmaGxj&iM99)ow$aNz7V)K5_w}4P+%rtOohZ}gcn!ax z1w|5TvVk|4i(HsU@-A-hrdAOt;(%)`qrd%7qKE1s(fv+lOmFtwcWc2j>cLVSsthx{ zw5KIBY7jMR8nIP}D6Z~AkR*}=5)K3=P*Z9Hkh%el+JmpHI@snY zyj0=os9!~g-&guzx@TlFlrw~*5_TgBLm6keQok}XuUkcuU%z=M5?toD#Mx`O^WE2w318@$a8 z|C>WR$po+~8mL&2*lc)0zbL%#G?5O|xLX)htRm;_WA@10ms&pSfZidb(B4qYAnz;Y z(iQHu37mTd4n=bH*TCc}u$)3nnM9m50fLjD^tE|jt0F({rDlkeD6uDKAlt#c89;Ul zw7?Fxj3YWG)>9t*>qo}oc=GT5C13R)IO`#vQ{v&u;IHrP^LQ%&#qap0OHk2x08t7% zsm1QVYm|eWw~S;3jwb?qGJeL}R9OfhV|p{OL)%$TCi*r?_ZJY{u0OB8L|&F22re;v zG1R1nN)ux-qs?eyJ=wU)c-{Dr?ud_#U#YoLgla3f=(2l)F1wAW4fow}i|#9nsCbnR z>}#igPgSc8_y#L$cH$qm-~-{^^gerr#$;3_Z#R!Zs>wM>2M^Pio^ou@MMO%7vU;{m|Y}*GEy+SmUwBuq(>~J)(Y4e-jCF zljaLC3^jmW0U{#?c@v>>IXubKc6jgJO~u!vtBB4R$XJK(GmLmFsUF{qXr}MPpp3&R{*9Tv22QpUo_7zK zb2Br30(;(jb08(uq?^JzA}L?+4)1w9==cLU9>Mz~IsguM2We2^2roc$mmx7$Le9Jj zH=ai&mn6=XJ{V!%56q?J$eaz8YCYqDTQx;`3I`*1fo(y|mS8Ao02Na25J$Kby77!W z%qDQ$IpE(?c;FhcQI|p6b>u|EdM!NLzu?{#>QEP=qR&yJmRFhr#2GYV=|-0P2Xyg# zR16yhzq`$dBg(ypewY3LUUxtA@_~lFbhExlV}?dlk~(2@7%P}&P_xfvs%4%>$Ju0a zAxk~WM9WUgH8h2rmUmc?zoV=Fu-vCx+$gm7*XAYW5$0y*3Ur~3r|O(V-&>1=|k< z-t%_8QIR7nasRWyv=1zW$-X&E$Lg+VdC~Na-pc=c^cb7&zUkTF&4r9l>K~BP5UWs= zScSid75s_!J(PTm+t^e7hJMr|=JYwguDbRPp1GrBF8f+!gky)p>&WShCin0QwG1Dn2G|GKFX6MwN^ZgohuL+|osBq$ zs?1F-mJ;gtYi?^R!>>(rK!2kxMJ?NkcwA1A={W%(_<9b~T(o`>Y4ETGzOPUO^a#N+hk z8OqU1VFR|k2=sQzyMBRuT7{YkH?Rj5B7XCZHXE5VKXsX@LXlg47TLZQ5zTvvM$dw8 zAX@JtKjF9`n{koxp3zO#@mQc($h_Pf10K$@e6!TFMp~QO(rlCbZunjB+wIrcuaIA& z?S}0ly{iY>Y_=WNBGwg_m*#Tj5~fFn?E2zV5w~~?cs`M*dchs$P9z?p8UBu=_RzF$ zX%*7y+q0&Irrb|%meMwLioHSFo-}*fMTfz4$=%&MgQ{&$iNCF_$)O#i+l()zjNv0$ zTTiKJ9d0~JUd%-z67Cv`8I}^2;nq5N_w%Sb)Wds#4DaStlwD5GS&wsqGl8Dxr5zO= zO?js;)4J0Yu|b+EO$Y71>?#EoZFc8C-aqbbLqy6~W$xe5RHi1)Gb#$jk+ZXau2&t1 z9!aGN;%M}s->I860epQ0S3ily*V=u>H4M9E9@krEJ$e-%P5Wq{WN&D1V;^SkPF_?O z`#igq>ZtXpnEK4&c4Tt4ab9s&cWuG9sl{T^6|3nB&k64sx=RG1Wo079wIUcek$CEE zYz1No)FRvbjs12vm7Z=8oB4(wGvA3adI_g|&P*?@9fpLOpe>JIdMYtp*~p7ZM_oP# z(CkDm*m-brC0UcV&8;n;>5_W^-|1RgtgRxQ{+jv?^843sm)|kJgMKsoD%0_Ahi$m6 zs4dAl!1~^jOfE{4v6W%BZmZ_8w-uENAG>=ZQ8e&;b_F{ZrroxOr9DgQ>@YYY>_<`> zCnqL7NbZ~Z*uFe%d)i*AE=N0)-S50_i3G9{nP>C;qp1fpwb)OWgPTuv#SQzZYP=N8 zk1-UdBJvz6qx98v(bCBpj@g75z=h4p$T>dG5@HtX5bJ!0IM)`ux9ghL#35Iw>P>ZBQP!h~Bi}#`gHuR|lfjg9 z#1p?FQYqNi&wCSlX)F@Xdn{;I-0j^@=(|0Q+61|&25`!0cY3IdID;CE8=VxQ z9X4;Ir*27etmz0cQ!}dRKE@hQo>-`fXO@V5^5Yu zg~y>}nSZ2WbZ6qtu2FHIAiQ*i@xIYu3de)H(sa#~Vk(H7GnK6A6s)K1Ela6}9AgQ@ zYWk5&>v}}|HzKmVfRZ~Sv||gETLhQd5<4B{b0Z~h zLoS`dZ0w2VQyPn1GGB|BX;2k*8r-Fm!?~8 z0jekzbQO0MqV8%vs!dGBS9OFOxHqm8YOkiJ=VL)IyFZr4SMGf1I9sq$8Sp@j#SUe| zvX9?~YL;#B;Xc9A(}=ju_u7)WvA{G%S4`g-N_JXr$JgEk8TSa4J018lx*AskRWtDD zL9E1evZ{;I(R!u%vDtzrs-0!BWr1azf#-ymJI0Y^uoGxTRY2dlw-w1Y@= z{p-Lt&XXSacET382b-oHn|OLg7Hlj96w78;^z$xYaVs?Se&l~nB~D&q$y?(u&gA<^ zwuD&s#-R~5#p>$k{eg{QKNj*W*uNK`MK;6EnvP2H*Qlo#f&Xzhcvx4lPWc1TAa|%c zi#r$in8Tf$sLAy1VD`xWtnQr3zTC*dRp}W_jIjGF@^E>q5aYReBo>Ae%$$v6d4ytp zok1pomnt=j$=@o8)PD+28ir)Fp1RzA*yrXDr{|={>Huo;-^VUq(>M+b&Rt^+)mVy| z+9K`lFhyYp4GS||}5^l+4 zNi{!$4i7MAF<+*0=RTvInlNMO7~EL*gAAEQL|(U|YDyvBW^{|&z#t}r+Do? zpig+QnNeIn62>+vQ9O2<$j)s7J`TfAd&u>ijteH{bslUF0~H?nn)uz)c(hi5 zg&t2jYKLV($FGMLBYNmRK%pyiy$x2Do@km=&^zu>QAsMXN{vGcxd=Ke`M0S`HV}(U zcC>yyRs#oe@FOfjr|9*25Wmc5ICKRh+;3PHBe3Q-Ml-5}yweVz*xNk4#sI2hZ)e7qcMjXUY9zK``$>L;(IveGL4FC(XV8F4v# zjXUsH&gSeUcuibXx4uG6nZZs@9Au^ z#2>l+sl?P3%VPu_{20>oC%kE?3^Vq2GqyzyP|JtEv=bStN0HUy(1BfE16G70WHfbz z&W}fuJ_uje!^?XE!F%{13u_ybpK}HeWFByFBKFstM5egVQ~snn{T%&K{afHz3E#(f z?B`L2So${rX=hhRz< zu;DBmuNYqC>+V@lru6ufA5lwSEf8FUlyDl1OygZ<=3TbHYqAu7*Ky8$K`qw=-e5UT zd*0v{FzGEe>~9RaM-Rrkfaf)!+Zh=t2XrwOh^2xxu~=3ksepcrxv&Mx@Ej;$XDm5V zHMTZ=hielhQ=d=6p^E$6H=&8y$++%D#K%0mkvqvBeSv>JCzjxFEWv}ZM(qYNX=FZC zgKiHcA7&+E6_~YwoTfG8*_}h8j>Sfj1(~`b5he@CTe(LyC_T|urLnnoBqwtMS)RM7 zMEsNrM0v4#O`$^HJ8VF?^<|->1Ie5gsF~2qo8vVcgYR%X@s2+XnT=J^%=_T2T8Acb z4SDB15#Mi(55T)kctqym!x~~7NOiOBc(uyVy~9BjuCrk7Sn8nF1)Bp5NqAMxVb@*C z+*nFn$ZQ~3Tc1y_(ZAs-4iJ5?5)ao5>N(aU7CVJp?6qX|mxWudrE^{=wvX#rWjey8 zo@2Rei@)n9K8A&0SA9H4X5RdBc;ac~ip}t#z0kME#C5okV6$<&DE-3Q@_r|P1^e-A z+`@PG6-;=@me9rN%#OB5MqRL*SKu2)WEAlP-&XkCK4PI3f!Te~8zdWlG*ogO^WzGT zi)MzHpcKC{u%|*7FLCVvsCPKNlo{A`FH+r4Lsno}xNIBf;T#}F1{1cB6mTklyvjP@ z)igAotz-o}L}p1OkwoOBR5y8-bs_7r8*gFPIE}zeVUL65X zs|t?3#X`LV??Y{T@g8K=Ge{O|;ir?`gBc^h%SqVF7UH+qLLZ_-99_caiSB+_@VcSd zcRHA;*($vJMLqFD*eI7_JNp}6AzcR+!9@yV+ZG?yG%%_J zkhBorB-+~^rJ6-ebT=J+U+z=aZ=Y)ep0ataxzrq*&U!A+C#+DG3f1OX!boKM}AU)M2iQjAXK#( z(3Q>{7x1~ozWy5yi0G|nO2&O5X5%m@>S^pXKBW$7MeIz&u)OZpTp;S^ z8@Y8M)NHFm9n>D!<|g7nUW+`xk6hDZ=ogpqB}?b-pHxNlYW={`++-B~#*!aRE-#;R zGP3B5U_>1DpXWg27Bk^0OB5OOm#CR=2pnGv3|diHyDBp$BQbS0W>FS0TXfn~vim>t z98bv{jDog5z@~H&Y3e*PZXcPkTb2Cp^E_?j|MqXAy6#G)Yq9j)s1DR}Q4JxL_@Wr{ zv@hW&m%7<=@STsqwpA1As>gQ~Nw)iTJpW_RBU%uFQ2}`G!*^1-?hhr0}1mDdCP0bA*4nxK%%F%KhYXs(XfpZQg zW@8y#Z!WmM0Uo;rU)3Qz&nFc+{SYec@L2J-mLv+G6g0InaGQ} zchVF}8CBB7r z^fHm|xqraRvoTZ)a#k=-hQHDQnU-{P19(Sg+DK^7Rw@rH#RjvAT<~T1UT5IPiy#wz zDwL`-x_Wb{Ryll8xrt`W0o5&w7FZtns2G}H7?Pd^4@)xo#CJ5mA9$)_h#*XY=6&b? zclP{5cYMpcCN35^L@FX&gC^d?3ik{>QR+*7Q^se$>BcUkL6g+s>B`9+vtv6b!rlLZ z_q8KBsvc5h1D>fpH5^(3t-jcS#xn<{(#3YN`K31fZGM;37>Fh7+fI!H=^-wCl?zeR zBb-r{oYpQ_QzxJo&O$<1iM%tLII9)(qn-M`K--O0ut@ z&JPv1M`2IC%l_y112sT82aw99%>(qM|3f-rN`vXVn3WmeB4#UvieQ0M)yPIw;^IL0 zcdY|HE%gAOa-Ap4CFw$bn=9S`wkzQTTZ#8N3bfC`)erJiE7-e}SoC8&%`s*|6yqvB z*4-RgN_6@be#sKBRjM&8XIr*rUN7Cv5gu1ddnAx-xZnqlG@CoSsUTmMz@8b^DEiRL_|5WLheGRxh!hZIGxHswH zl!*87Gm)n;z%ZUAhIIFfVD6?vrptvdq!!QK7=F?k|9K;xx;ek62ft0Ep%uKd z&Ahp*z$l9MdQTa5m0BVfc&qz)&nJ0*XL-L{!JTcq$(=;{KZai4h7Mlj*imI3%mwqu zAr(_4fQX>^P{l=@Eq&uZGZQWVk5j;96?dHrW^TjQAhlJFbM-Ss%kEIlJjHp3*j^4~ zmoe{#(~V#_Fqy=6R`cf1bozc!d$23#&r#nwLYk7arvS2?X*_Zr_h z$M$i~IK+KVa`tBK^pyLZW1rmbG~e05wT^Mte(+;0%PKHrCh;9};4Z6KPk_%%N7oz; zmh?k<7y_=0f|E`Gn^tkg7PPaO*w$vS7L83RdLQLip5a;cuyu|*Y~u56e$yfTU**>b zL{75Y1zyjPuk^g_cz*3e;PsNvAHm))RB6gf<&Nxd2C2`I9egs=(ZtN>V$8J?%#A|K zgFv7uIA;cO8n{LX5pfyursQXy$8$0Lcl z>4scWMZv9F|53ZouE&|`n%(F+#??*uN zEc5#=NA3dGGo1H;d3*s_3;s!+qn*J1EOTBkA&T`wB8=X1)np*&0vFxjrEo#FLOqkg z%2f1?bimxhUN2oPGV**b@bWjHk`1Wj=6z=Y-Y#-a0@=#X97zv+0{JB-FjpwCRF(go zxsaaYzwukG%oQV_asxwwYko#%u502>MSxTeK4k-Lb%0x5g?i^^e-SA75N3r3pJz#+ z&;)oF1o}0BT@R>ZYlXg7VNRB32-j}Qoaza6YtPbFfoN0anNa*bK(-sly1@zl1X}H( zg!AEmlaazlpmR)ub`J;QW8m~F;Es}Mw}|}%_+~GT4dMLB9Gk>B6Ipt2w~qXu0zK`` zRlC3&n=p^-Gf!GEyQ(nL+VkxRT)iK5+=-G^&iy-Zb{FPPeV)D8;{ zh|v76@bZt$_#YfeW}U>m6pr!|Dlgr>UO<;#;z>Bf?A@oxZ<~dgX1^BzQfSl%V4HZ-m`qZ zfi@(4Fz$erH2;rLo?=^Yc`x)`XL9@`pHFke{e0sbR}>z3 zUZF9OoU@xDzj`abZ5RK=hjUoD_I{pdB|6;}e(?^TOmw?NidL`y$!-NlHt-Hsqa$qK zsdvJC)^Yw)MOP60VJ%nO$vDVWw{v&t+a~l$hP-9rh6j~TJNSH*cXp1?ds*a}_whtK zc|Lgq^7QijCs=P_|32mSZ{?2LxW^ivY7BDbzi`G$@XQJL5!W%6E1Yr~`sf_SQhviB zuD+G)?B#h5@c)3qH_vjv1Kjlp`#1BgWt=IzbSqb0rc_~@L=5UY`14fw^;kTVgINAi z__e%`Ws0`6nI~Jwc?*?$ujSWo*MVW(l!JgOP$rJG9 zEpzcbnD-P6`Uvj51_$H8I>GToMe1`XIPHb61V9}$ND^i`ZMwig>EV|EJ@61gX-1l~ zKsPL$C77EYnb4-3t0f-J2{#Q?zUQQJSui|D_)uP`PF8qqe&}RgDAFHrjM7L171;U% zz95_?FMCSCH;VANGIUL9&xOHrDk6`F99xbfg}7QVq>FOe(V9KppH!1JKvRprL6#$r z%;&9TgXR{4TGxQu88olK>dZ(Eg}6o#wfAEAEt$D)A?{a(Yw3C7RJ{EKIa|v}4@FEN zzAv9biGEk)gaCe@aL8be3nxvlNCOB6ybB%g$Bs0h;Yta-MJ;qv3vc}b^|Hf@O}yJA z?jRgo-m#lAJqm^U4lIN|Byhe^Q#*To@b(1ueWOOD1Dg7U{UW&~C~GIs`o^G66*Sqx zy>(Dcq0t)ti-aOFi$sM1_5jPDocx_Q|y#dbM3cSksTXJpNG@ZDxe3$>63Dk0<4 ziagMWMRfT_@PCnzx+5|4 zVyiWry%|RubCtijYbTDi;~crV*ovxi=SqsiT@$%REJyOZf?Fcf%aC6pzo`sIYj7{Q zpFDA8z9+J}Y}aO8pDT*yT2Z-TdHz>ZB#;{1L$>AWC6xD@hqsuUHzRK}H*a5{k&m^& zK{Nre*JR@jX8`6VBuSZNW}q6Zz%GD&en22I5DrxKW#=k#z5IMvq)m}#b8sz@Q`IYF zRj!tfYgicmeBa7qV_t*+&CEbQ3*X4jbIFh=$-bavopZt||X3osRGv?ub z($h#Pk;)#K7r_e72Xk$Ip3VB-v&%2=DU{Hu(8aIJ{twXBcStL*;N!R9_m82l&-wfb z+!Gx^Bo)yE9zt_PYmhUa!|~;;XHewN(ATfvQUX|H2g^medBk;uYCmP`9?JuUNID`Z zyyI+{7ZRTHXFsC82x-Q#ISp+|7DN=J2Wp0MEEwfZGMQkXo z!9UUN#5&TD?=}J}n(|5TN%X}QU|&bDMQ~NHwKW(d%irv8rQmL3zR_IyzDUq*z_*T^ z*@3M#d=`0H_RF<8ajYG*GREBBHu(Yo8QZpFPsx|e5^Un$ShmLu(yU)fxt1%k6RcyE=V zCS}0F;$UWJsFciPu^NXm+Xc7$nE5jM1z*ITAoe#S@KK>BTufwemoigmf&(lC{-S-V zdmS8cDDy_3Ej&YXVBtt_nA;-X%e;QcBKpHqX8QxA-UrOtd&=CEt;fvX*BpPs@n@X% ziX)=$%3K#cS?0RP!D1bH%Ip=~5^Y+r=`HJrobi+)(sUI6U$E~LSA3&jz)Q{*tdsh> zKN&xGHo>=-9Q^{Wd}H~f;6ogr1!n~}WL}G0kOn>oHVC&(WbaoN6+gbh+miUrZjQvW zEoZA*x!B)@@A~+*RQ65etqEV&fkQ3@8&Y|bshlqwyXZfHJ8{bM$rF6!=~R6BuG}Mz zD+m^*uuf#i{q0;y#aDR(;qjs;*}$qGG_yd~D!&vy_B+@mn!E5pl`aeAF9FpLQ@B}4 zMcxvLtCYeC#L^=ivZyi&vKBckA9E)U^sq2Titx<>?3E#V9k7Np;mGi8p-vkG_4!CcLxP zd=knoo{wMu#pV^lIoY}QZ^}Cmc|arxGw;X5+NMB9ER{NCpC4nb=f+C`SagF~BQ^<6jiW3e|tktd@B# zHlfGL9Dl`}ea4=L46(rqmZ&zLD7Iy;i@oz2+S5&zYv_#Ek>xL;lU`9Y(5q};Qnn*G zb^#so4BFRu7SVXm@k!Qa*>f2!;JmWP(bLL4*)RI=F?6QmEQb}n?GW0EXi+E8(~c{e zr|3V2(P#Fdc^zPn=oDhXJDmANmwR8}v*>_w#bb<G;t%gXP# zs^I!e(u_ z5P3+a@JqRunJXHVC(?0Uq0%OXRAs3=(x-sSp>rJNzWoPOlF}hK@6FXLghv078zD%r|gw#Lh3Pp zzO5dSY6W7~mT!n;EJK~uGM7YFQ?*vH`^lV_y|Rd$C-R#-wQ&AaezWSY6PhP;OYD9^ z?L_}nW!Gm4bcN>%4Z8;{?kcdj4V+X+N#%Rt(>H)eB=EVUSSEyi34OZ^<+!3iN??0k z8LB5qd`YrLXo^smE1WCTOsL2?*5Vz$z>%|1nsW-hI*6@Bwa;wDy0Q@)$$Bg+8x(uV zTC5|hu$_p#CxWq9v8pV>CbNi)g$1l-Uj&w$Rh+pJ8_asXxmo%CM*fRsW;>RZ&Fo*z z(M^o4?AfL4-@@Ib*8gUP*m`!de-}r0D|V{gidAMmbW!M__f@|&+HZ%IB0)t9RJXE{TxjpsO~o^hF@SCzLY@A{VV-lKW{B0Y%|^+=iN>dbz} z%obc0jX|WKFF;e~xKLD)F~kZWGKg40WTuLDRw$eNsdH9lp3Eefjp6}?Tk zaR_`mD?_A?TySIYqlgxj8!j($gvbe^TNPI1fMUoGg^?>n<`Ai0^mCC&DljUcS&H^7 z`lv{>RTQ0|3jZs!SM*Nt9*HfyrlJkhMGFvp;ZGLP4&;b@s>KqnEON9CT7>A>;zLr$ zU+4&;Jv3DGgL;aVA)11!Q#541=n$fbi}$sWqDjbhBP4FoKg0*sgmqJ8Eta;@vV{QMVk9rDaQ7^+XM z8~ennA(~h>ey#jU8KOVQ9ywF}?cJ2)ofNIDGwY7Zdy;dyFl0|B-k0b@fAbdGvX&vA z+9~g}y|UF-(cVOZlDFRyIEc+ebUAgO>=zxdxdKmtunI}hCB@<*`&G+}Xk1O$BSW?Y zt}+@cYuO_+LcNX(bM-#WxqfRF(d5+U?7(m7qWo6TB1M1f$)ZA9pwk~caWL;vv`2|p zkTDnyayV~!7~8VQ=b?;&i~-7-zqVwy$Q9IiB5?1`w!mAUB9QOTBE3We`u&w}%jZ7I zd9p{{QuoPPzR_Fxw#+hlUV-f|_^5E{sKBQKFi{~Tdu3bw)LEI^GQ&Fq8?l1PnX+!j zB9Iaowo#y@YP@1m6ML7uNqMustYUwGwJPSS7_8c{1pmY{rsB42Ni2-uwnR;czpW0R zkX~)?$$>h@ZMHG()~qN1-4>QG{j)ZIOL)hWwYIMj#0t`eip0o3nhbp)A$e zUqhi*RiIo#)x^3X8hUxCooGtM6x~X+E1{41p_F35%nd~qn^#u;hd^c1E8aJ;zlua4 zTw8{4a;+jUijFS&x%h=ef)nXd^jKA97AaV)K_bn)Q{?2AaBYdt5zXWYd_t9xgm;KE zeqG_mR~0^U4t^ui5yG*==dw@nGVOr3NumFxN8mu`>3)W4E6O-jH zBE{-b7qZ6xMl~Yft5DOsk`lotF>R91RD!tNlKd}DrhYL-L1h#swyl6KA2Dcoi4M#~ zG@8Vt$^RTgrR7l8*{B02L;n9-vMbwJmCx!~>iIdjN*=C|hwJ5MNX~D5?o^2QHo0rD z|L$H~d7@I}Sp7kyT{-fC%k#XIS*kFq5@#tRoTUcqaQed3W;7sgtv>4p%5QH%SBMtm z*~xq9z&q;5o9e>5>qo3fAKqYphP+RyRyT}yJPI$@Klsrm;X9j31j;l#Nm37KE^LlB5PexX1z#dCmFx= zVbNwqXFkX{pvZLM^;6|Sk^V$Jly@m!W*HlJ!yAw}#bdWhktnKCxp>_5uMvuwd!)zf;s1{9CO&jGu{?WOOaC*8x!$D=H4|2BxjL|N3b5FNXhbsMOG6TS!6C%T9=t2(!MIo zsxpz-_(fI}Z(mDAQdKn%(JWL6MdqdGHxgkb`jBMTS7cORHjCyY(W2s46fa*{)*^4H zT9#;AqIZceQ?=-dW+q;le9GrAMG_I2L_VwjGx5{>rf7PijWWcSt6He#lj`49r5CZdiOoZ79lv~nCafD`0TY{?is4%P&SFua&=YI1ri*n& zWE|B-A+{OuVTr^eR6;CkVp|h!R@G?5f)j_uLcHFxEtVUJBv<#UwhYyN_Kkhwbyq!M zV#)mhHxlk6G)ef9YVj2rN-QDrS+wZS99O+V5)}V~_!{Ds zCl>3RLs%1zlNn%4%wfbP-s8Ujamq?qU9~ZdC3k1tmH+ao8~cA9 z>A@L2IVx*8zZY_0KSeea`EW3I==a~92K{&E!92+b60ol#XR1@Vz$M0wt%fA{Ey)6hy_hzoaCL!yHlm1&B#Vuk&a}Mcr#gc z@!mzJP<;&Ii4Yk=Y-Gn2sq4)Dq%ZL|NVMHK1(IU-6KkMIZX%(oei8AOh(=7`)X8jYN+nQvI)>GAg_W$13?tZW8 z*5x_3x@Cj9b^kg?NxK?t>+~HmI!7oWgyOi;g^;P|31K9Z^zJdbhaKOUCTfc=qPb{~ zTU*f@kL_@@5gkNFJa)m+@&C`e;L}ccrz76!D!Su&SJ4^IJOBSxSJCtTM_Z*6-W!a^ zA^(5%-#12z(Kv>Szr|QFPE5ctMNAgc#Z=s8iFsl!?&phzVzF2vmg2TVEECJcO0h!x zi~E>yquSI^0&{SSQwqZDNDiDmIJlVw2b>c8G&wr#K|`iUW9c5cigKoEIc_L>^*@ z>%vV^#6~ek)Djg%QQ;Pfc+RVc8r)%7=6>_HSy+{|+7IYZhNcYoY^eeqipW?ARmg)gnO#UVt$Q&{c_Z!GgvWb)@ zZjz1s5nsdyaaNoYcf&YoHiVPr;t^hx>ct zu6Tuc{`Y>Y2*xjS;jFnyc2bH|Ak{JIp=1VGM~;$hb{WX7C}klLgp8BV4`7CXry$mb#XNPdt2ankHG zFU>{^(tLO2_DV~o;ZkYoIUCMG*mhc< ze#D$)GEW4H)jWvLvJA7c`P1lY==u?Tpk7(eqlf6a=F#Fczvj}5=tK15dXzE8NHltz zFU^|P2`dMm!DD$nNGyshArc)h7K@#=2% zpn6t4rEXRytJCDWG>=$kz2b#vQMQy05M8WV)+KARRo0BtmIf{ds%eWfoAxWPQ2%VM zAduw9iwMRoS4A< zW;ye3^B+sIT38LtQ$|Kpu?ksb%)9y{Els;-+6o7qW{KVMXO%$}6>x zZH-+G?&rwmv^h^XG6w%+>n~?y16dj6yX}EJw~fiMbPuU49-Gs&P5ugjW7={3tyU~B zAkbB>Y-Tm>+9FVBgH8v}buMr%3N7y57Cc6&$DFcZALuON%pLRrYcWi%qee@Mt)YQ$-kRS1-hw{O z*G8*u?zMWDjRJ4eJES&Ct(3mkn>p}MpJ1I8ji{eyWObMU9STBy8yv7mZ z*b{WmwpsbkYLT=2C3llx`kdSsmw5)>+FEa%3EcKdo^R>g)77`b|2fb=?`%vow&`oN zm0FDPuT_aGVXBgD(}RbE+;=s1Yi?g?T9`NVg|mcxm{Qbs##uA0M_ApEm+CRvS1>-# zJgryNe1W%tasH8BJ$;eqkT2bT+t(z0cXIQjqsiX1pT0Q#8NWkEGK0M1`ME+eNsE=b zs$1Q!-mrIbBsoVr*9PyjC#x~aMwW?0!8`it8CHV+ z-m%_PZ-Or>&?RuvAMK9_L>VQDX1p<+fvObW{tsJx*py(Y9Yf--DBbs@cEUItMwNLtKws?L(diKd>#9Pti^ATaOVN?(vgG z=D=+46wi21MbF!`Hz|dak0mWl{wvjyEO#gj(T$=fN45(6;;0+cDtJT4E!Qqbth~me`ceIXnQl(h8hBc!^h+w2 z)IX_J(#Yh6scX{ndordqOWvAPBK4(jr&)tjl z^yi+r-lD!N{-1$K#zuaQww3FvVfME{G0wTJm!bE=b7iO<*){5X#Ep?w7pfD&?ROy!($$_&gvaJC6d<12jaIUG)Z_HUm&Spdh@_|EwArn>YkJf z>CXedOd?v+Y|5LUn<3@h$6PkogOC>?O+$1?B}b&QscUfPu+Tdp5y3sw_R@aR$r`1% z4pj6j{;Ix*o|5SYQctElNsdk`mQXi7W5SxG9;qXs>x+F|1D3W;H}oW<0zXPs<$~>T zP@-d~D-b$4+!o=C=oxV)!{w-n(YG>v%d|PVeFl5TW91gvLL#JZ(o{Op^600Hsn%69 zS73MQ=Y+2DJK~e#DBjXb7Zp@l0!&0bGEk1|J_&EpT(cgH^Z|btwd^>l;24O5|6~M zh;NlxCZ$mNd~aj_rOm0n37s(&=z@>Q&g`pW(y__NdFIvQFhta{j+&?8|vBdSE! z$`~JgK2wfN%_6&nypZ$qxn?_2m?`Xp7-TiD+Vja)Qea$Kv&8WDi1<_S^Ap}B?N4u_ z-8L(m0|RF~PttpMM+Y7l8^mcjOK={S=04zF<609k%K0L=e$ZC?qo6F#&aUd86J2NA`9kB}1Kpq8qr>(`bc|XV-6iw0%<<6+!z%@?B#Vvt z<~h<%N@hRE1mr7e;+$E+w>o)r{M@)!@y!yPNzYUJ`#YHzt?xSZolBdYcG6QaklSoR z|F%7JK67*TRM#8lzm5XI8|+7H{p>%2zlW3#{SbP`y~SC>?vS`uTYu&c@t^c()avPm z#{Idxh0;DJze=PDQvCk-tV!EaeV#i0-nDjSGJg85Lb1^T3$bndfBu*L_VrB>tFP#Z{IpHI;mH4ZT9Q3#aZ*pPiTz z_c?BPd_uy3>(b-=-eST`qv?HDhfg9#?RwigqNDKEi*H`BNM+S_4t?i0! zLC^;0KkiOp8N$xH8aOVgJ(!crdZPcVzqppJmo;8$ef`rsXH&zHw*bAoi+9d(ep+jcJ4YQf--|N zM^@2EyQnQi9`ji5rTz4sOdpqWHSu)(&A3wWB=J=8)HIK0pzoV+ukWXKqqm~(gJ0LF z=pl`^9SHvE9OWwK?&@CPE*6>|b}+-ojD0dch-ns6D%0`||F}-t7szv^2zjJDLyBVi zXnuB=ZsQdLg;Mh;xZ<=w*WyYgd`-HPCi$-VR(V2GJxR@yd!)KO=Y8Gu>B6bB3(D=- z9Xvd!puM2YQ0gi9)O0%wsT^7;thc+M^OyaM@((K@Dw;R-LwcAIVsz8HYw`Y_-u7vu zlX}LNh>MA@op>PmPU^Ju-=5>%L*ATT$s6OF03UjRXJzrq+n@>|mE9qs4MJ~)lJL6` z!BJVG4`j-iS;@3AQVM(E{2o*~XsW%ZI$AnMB*JJ1+DTm1L%qdQn55Cc^*XRA=S(iE?abw(wI1yhmDK(|4r(NK&@!mXQ^wtOI_03Lv zwrEdY(c5wf`O=M!S0~8wn)VxO=AnBA2Lx^N`vX&;~lwJvDf!b7ROu*E)9xcVAcMkX9j8T!-E9 zp@YNnhH35>A-jU(RKXsLn-(IL*}?2&<}z~|4YeTuMo+c0b}7%2vL+2k>V|lpJw4Vl z**C@S4&)Eq@LzzB7@#ZGK9QM~kzXp8RIVOVYpO!Ip!8Fcxz zvz8c@^jX?=Ek<9eZ_zvGh4n`IEd7QaVjMQo%^N(Nj+S04MeM#H>S*Hl7Q8%oU+|0I zwvOkHk>q-_iMom^WD}9uy%6a9B z@>H3jJePk;TUl0mOI+jG`73Li)zKS5dD&T4hrGFxNY zQ}vvhs`j&`+fLbs1(`v%;B!HFf?C^)+xjcxrOh;$lofVS3RV6K9uv9?>TT!!;@$1rlTZKV40 zV>zReU-?U!rz9%-)qiY*>@h)eg2o4>*|*y7*bb=ulreIgR8NYL!lf^43cE)ilb+(c zwb9&YNJc;Xi*{7osy);~^eElZ9%u)&ecF93ul`l{VjX{2$9Op)9Ys*-pCk`R5-CIH z1C4k=pU}IA2&p7L87|WJXdaE)2KnXQ8SIWL5&{vvAY zXgZtbVt3eOwoh6jH&%+M!_+1!gNV&NyXUH~tu!VH%~(73L1}gsGWBEGKH)Gu$S^MPcAchk;p) z6XQ`SPZ2qU12ud#@rpmSp}^|)?h2vO2z$FYYA__zgV02c5xgS z(Q#6T_6It%nO zm|E0-s1tck4E~4gqz!TYJH*Bc18J*8*VD|vu4>T*v;h#6bovC9;t^7To)+O`6#Y!T zi0-5m9Rqu^k&kpf86{NGgI*`!MI0)Og=CKCOi+iCjbbX!(NySS8$Ag$^D=OUzNCeC zCQhLWA1}NjC;5W$AA&x8;uoz-Sj`|lfdAr^#7KUbcK}*BgE!|V`A=Sp$B4XQKCeV3 z@UGm)tC3WZiMJP@@!P||I2Q=YP61CcfEeZ@D@7-ghdvdn2&Kv5JShm&p$s`r?vS>0 z6*({ZWAxudc^Xd_h-bhf+L7s40|)&HyUPaoKcU0;dg5n2#6Z50-bS|5nwAs;Nk?`? zloP#x$7BccFrF*`>UEbK7ypv1zzUtz&)bq6WS7W64*@f*ON)r9K;hZ~tJ})Yk<-G% zHXCBf1YO3a^TQ;X?F9O_2kTaj?h`fWWK^G}X({oF=49Do&jQ0sfxZm|X4+ST)15#}O5i-N=ofyB zbf<;M0dh?g$HlAc;F)#N4Y=w` zplq=~J#F+iJl{(eC2j~m%}Kk9Y-BB42=5#Mi+T5 znunFWVYMV%=q1sUOy$?vcJYkA6O>M)BdxmP9lb`{SXXEq*6^g*0bI4CsEpe}U{$lk z5b_juY7?h{#vOvr4W!M*Eh`_(Ni1NPOGs04nMacs*OCG=%vJNT1CW(qNE22 zp%eHUz7C)N13zUaO<`s0c&dmcow=76CEKAHZ+I8BPqgOWaK$I09?mrLx^;_8hBof!7s(l#MhJf@da{;c zJCN~pv^0Oq=hI`*x+Jj%vM(&Qh+XsnH~B8e>Lk6wGf_9q3e0qx*i7JANh{G2D_UFZ z;!$)1dnKZIOL~tKmM({H18gy$c2^Ecid7rTsh%EFV8OJ_AQ$pzl(UQPZ@h@b*m`^Uz z93(UFUmN?EIe3uB$a;{i;4Ze%!gMK+W*ePC4~UaIn>a_u(r9Qv2AWEXu)lai(x3Un z3LZmBLu$`~yl|q!)N;(HN0%9lggx$V|FYjHS)2^5ig!c8(! zBKpw+;4&%_r}&$8;>Ag8SXFCrjeKPls6%9gzc@|vB9F?zX42l`gs9CPiGRc~$fX3U zNQ9M3jHL5e4H2-WlX0Xv+rx7TLQ9jSU|q`7PNX|5?O$N-N${tCi`HZj`ObqyS2|U^ zge5m4T}3K+Mn6EWPLql}1DQi3;G2TT2Qr1c=Vi%M776VeD=w0XbQTc!2ka@LTnkZ$ zcO?19Wzkf$qLf_aiSP`B3UXL12E#R&6cVjOS@KTYAb#NS6=_@e#j2#92!X|)x18iN z`ypOiTj^f5oNVN^#ZUU3)Z%HDO7qYvTo=<=x_ArkLE(jx_Qd?EP;wq`i7_yuu=)x!K5i{fG#qQPmfT`du#C-dQA z2-`rafosS|*HMPN<{l(}RD9zd;5!>qjr+tt(wi?6e_(@0@v1#t3AQMf&lN48kI#e< zTflfsCR2DOVAYA@1R`;Nl8>hGVpgiyKp)Zx)&M??HizcU!74l^|B_z(A&Eak4&UcV8(635}u&>E`@zWXwue_b) z61RX^H>4RvAzpxl!NY63r8N%my(W0Pm%NQA!fuld{IXSx6sM1QRz6g`Ap796vWQxw zKN%}R_!cpRX2;kDKu%T25q^lP!Y?w?CgOrMkZc5U`&RPf z@IRs=X^U&eK*P?0<*C8{@Hp|1Ultr%cS$7i#k@1TY$UJA4}goT3#|1UD!Ql0OLeYU zpJ5vZ`9XY|0sPQS@rK{$?~%8a7WeQh1+k?&?}A*U7xd~RUyVrR6$dy07k8f*0n=Cl zeB}*M2T|mL$RHYmQT)Qk^S*o*{PJ?lBOlIxNraL|ycHkH2Vy*DcoPu=4^vx|6fQB2 z)EDJ>58ezT&Lhh3OyCR$5F7OG5MF=f%aG;Fg|>NkTV4opr8)S{NBoR<5BvXxNN^HY zF)+G%~SNtlcrhzZsat zGH!>R4-y8nvLIr23f3V%&VLD-{sd!d&v!$f|79ON!AweE59^Cdd?r@uk!T4Sbiw*< z1>-jqIKxLW4kJnCi^V!{oc+aL(CrnlBG^0fU>8|09`lj7dNk>VoHh@#WWTru{`enA zt^#~@7o0IfOcJ-CxrJfZS-^irV%Fy{?nz<}V%9G48J`Wr)l$V;Qj!$JTAmSiNpE6D z9R36^F_iqkDD#2gOF>j04G9be8=4WB&jQkuY!DA1shZ%hD1y|aI&l8Y`6vTUd$V=SHk(>YV#>iY6L&CL? z_b&l&JDfO>^|!`s%8;&LW<%jgOM``0$!6rtHNnqjDY;tBBQtoW^ot%;2?bRP82|$(GV+A3g27}7BqxZ0)w0ddQ=Tr3j^~Sjw?oi zXtv8bJ3~Lff0e6F&LBemSusZ7{=O%-Frc|427)pj9+TZwD^jXsBU zu#pKOANthwhbvBs1an1xYbfSi$55vdy~~#Y5auu=cT-c?_;= z!9tS3y5E9q-s0W&xWWlMyN&N(L{Gv_jAbi&CU!&Dx1sN12j05}8NC-8tgtxP`TF3U z^N?uBXDYf7#=tTskb8)Jo1iOIarGh4pb4mD`of#PCMMBP6JAHuy#P#M7T&Kz8jxxD zY$dd{48~jz`cVw1!5;K2oWv^nG0(>s;ak|*XN;l?d`4|x3R57*{;>Cfn3o~KP?cSU zbQhtD{RHV;!Wf;f*Hy?@kKncl=iiF!J^*9C1&By4I+1Rrvk)1Z(m`|?U5@HG26<^w zyq<`fav|>L&_i?!9ZCP9FJPqy@#`b7ySmWOCa|Wuu*$n)zgUHptcRWvKNs-ojYS3c z=A5W7vWv2Ko=K!2S|!4l55hOkAapQE-(($t)0~mwrE>CC*&}CFDgrG$t5gL7rP#u4 zaq1Fap7oSj@_ngtU1fYlF^Iv7afOa)t9%m=9!UZvSAqEW?5izbu#1Q~w4wvl9<0so(3_2I7(RTe?-|3z=m9@t5JbeQ}F{H!N?%c@8{u_Cpl zP$`2{R_ZFvkd8?{shgZEk5+yGv9F=#Qm-o|l_By5$;&pee^_@`k!5FDSv58QJp(0y z;h0#pEQs7wEZy8|PB4d>)65O#E^`vb^vO7H+y)l+*|=+bGRm8O%u`lB{sj305vk~3 z`HK3eAnafpEO-*~t5>ul>xNm+WV2WgR)S?<<Q{#_57ygvX+@$fJP6($m*EE=2PQ}am%=B zY&8ZN^^H!(A)t46%?8#HE6vKxi{dEG%cGZNDKx$_JXJ$f`=#gx)JefiLnqWStmYK@ zk#=GqSRH9CxrA0Go)U;fTx@QD`IdZq;B{pzE6@o7_|0Cx4W~l}1V@B}6GHhe?zT z1mf`tQmzQw8%=A{q4Wl%-h(zFKlx&7wdpdip(Cam0HTljEB&qhNIwtee5bw^Nc&uU zwSHAsjgDB;islycvzcnfn<-{A`h4D4)%a0hjI*F&Z4hT)p`+;w`A%YSOD5UqOd7@Z zvhtAEPpP0hMgAt&R#qz0m9O$isUvGblSplv0Bg<67Qly$Bf%tdu0Rm#@k(%HPnh8$j;Q$&;mAEDy%@ zH+ot2urcg2okS1N#_T?u&zb`DS%kiuJH|cZk+I)sZ7}1t{!)+CQ*eCMZ|f`d_IgRZ zwSHfpYqT^!nxU2kd1+=N__Un79sJt?RMZygx~cR5eFQ%~nGU7{p;_CpK4sw{%S-R1 zDRKzL@>9vK_E9&fTh&TxK4q&^fc1tqSkHz^qoqUaIXw*YbR6*DH|z=7%tNf6W*f7) z8ES4YszPQ}jV4A3tY@t5(Org5KcM&0r|LC~(dI&nTj#+-{4{G>;ruR-K>quZ=SAjN zf|h2N*jd(2JA>yoh}l8?13=H=yK0&Cf;+Lxz_3H%1x7jStYMH+rIeR8QW2udr(w%I89I9NGbdsG zuK<}p55F@Xeyb(C#A&Q)NkqO*h&Z+7Px4l!m-V%7!SaVNoebY^>CRSoDbx{c+R`XdT; zlG`Zt)Vt~hb&NVkJ%iZOOo;_zvmKQ{7uHbvm%T<6{f?xvVe%!Z04>Q|VeQ@+eT>ff z2yLuZK@VWvgl5-H1S)Ah%>>tE3taLi1SI2^Io*mkoo2l8-27wJ;RmhP<}=d-Ulv7o z(UbHoZN=_F$A18)7>wM!9K8b#tIyuEcap3;QaILewem?B2w5FhW=p|z4RC_1G(vj7 z%F~~sDJj5yNWUZx?IBiKiRNnatT9TTqV3V9=?-JH-csuum=+kNmC@t1Q-Oc|GyP8j zNA-+mf73J;8V8{JrL2BdjCI=#2Io=$l{}*>zzpqVkDyy8fV((hjSs1l5f%!I{~%=s zH*`Yjr4&~-DwEXjY8kbq%-AHLdh_Vt$a(V9%E-?Aw49Vvx)03q124dfhvKa0e@hn)?P1$ zab+_zAR;t3ADel=dlk24T3z^5F$IzO0L{xbK(9uy$Fw2h$VQB454>1jR$9s_$0E0h zRB}LG@k%N5Nq$t`$_u68h>6wN3O0u&qR)63eT^Q;?Ccb}Oh=Ffq9VV5RUC|tx>m+P z{hn4?>!2Oeo@q<9W?C0*FS;5t=`)bo#raw*luGP?M=wG!XS_W9aBxAmD&}fXV zxEjF3tXfJBBw2buPtmsU5n=FNZ`ow2KD_)#IhS%m zt}FZGbILSjsIo%ot2VIBP<7cQPn9jXkh}xAO?Twdt?;W&>?=FP0yHbN1L3cVzNF`- zVrDj`>x1=sdLg3%x+=#Q&y6UooNSghr<#Y&JEq^9k6d{rzlB^Gd?0d~uOtdpLohr< z1y&Gj+;Vi+Cb2{C`8`+&q|r!Lk=+t7~K#h^B6WG+-Qpo$W)`v9LVzrfX#hkem8|V8nLlGB=-oB=%C1ox_kh+ zijL`3sHX-3`$#1HDLfP6@M83prXfZ)htKUIwUY))J)l!Pwk^8*GoW`@`^xJ<(3(%mlViCB>4bY?uSi7EPNsQ;Cf$me|Df0WbMj>Rg z?UCo)Gn-notR=94)<7IbBSPi_6LJrzEK3De zzmQFb_npWpNe88e>?(X|ZTSSE!c566$4ax%VO>OOAiYIqG=Wjr#BOl23OKQAW z0L?prtfzwLK%OAa`+(ly5wrz!vJA-ox}xK5AKOCHQ8Tt6+b-hyP2Pl?mW?M{jQ>DS`YDX&I5Ozh zRx}SqM0#h<2U^`4Sm7CPTXlIBo)IWOfR_?8(A&NgNKq8B>fh*R&Pgf#hHS>5Y0$`V z%*7JG7Or4@kcpka<5N~!s?ADD6WJTKA0vr`d~c&NYQUO;>sHt`%h0hYRshYZ z39R8IJ|A82t-+*U0wOyPX!B`)ho|#@c^=?8l`yvDV9ADn zwa5WnWCd!uN5Bj!A25NayJ=a}|2Kii*QI5pXz+YFQDyyNCB<>BA%FZXfO0}^|6#uMX-hT>V>&?^vHpm( z2Qk~>U~-y)WmyDNV=wTuXFx>==_Rs*oAvUtKwAhfodM8S8*u@h4knM0x(n@$a}=Yyfb%^8)1HIw2dX~_8Fmvq zy3mo5g8An|XF*PK6nV&OpnSK0B~~U8qyl!kk8<6;KwO6bPKNc zHyE)0@bGES=23JWG$A*n7AKxtKjBB7i&@CX9neTws$cc`Q0F3X>R794tamGs~Vcp0fD-3wz12X|AAqNW(WT7F;w=a_!DzFT`ziK55NsvucBCJJp-y< z2>P6pAA}apz_^z3ez4dssFmizNB^e_f6%>C9vsmDUIw-KWYQPx%t3NQw8v_!0LFR_ zb?jL%u7japPEiPUT}6BXo}FaPg0;qqFrb5_i43dALALOYSk0y*B*{4-7Q?~YzT)kHw!aa%u`Y|i?tTQ% z!@uJifHJHi9$r}-hW1v6 z&b@**tfNJN_!gvJ`7U~yA3>ZBgB}k-%r6S8{~HkC5x~D&VXo(a2uwnE!!gnlSnF6Y z5`Jh~UcC1lmQzN|1=^g6W`n;y1!n6jaN3I`4ZnX(!ccvD0^+5EDe6vEA!DftmU9hy zSeBzVfMYuwJi$g&o8E z_n}9j74ouE7;`!}nO-D{|D+M*idBp@26KH9mNploxs3jecwh(1fVV#Y``-zEZ#3pJ zShNDhF^S&=?syT*g=y`DFCxgsHlgR|lGsfeLOM^$8esSzMHa9mO`uOR=_-77A2!?` ze0(MJGfak88z<65b9zc-6_x3H`0i}z7Whs+fngjCMq(4N^#odv=Y_}Xj(HUpVYCF0 z@+Z*CtE3m50?$dn)Z~X|JiuxW2IJ9^bO*Qd2R$vL37JfC)n6;_-%KR6Kk9Y9(5sfzmpgPZ@iulfWB^qW-Jipf#2@{{=OK@ z!xYjRSbaCLRx}}(gaf>OJbDkfmy%v1s31RZn)&;z`QhdNJ$nucvAZS7m z#_=BCYDf-3Chd?h{YF2PivFrsU}H|fU--Z-hLdQp3PsVy6^qp!28&ySHQfNezKuLa zw5owfk{#YW7;IWr^lOcS-#jDw!Z+psyEG3m`2{kE)5szIhFlMm1Gvrta9P(Ofv?a9 z3#>&zbRoZh2UWtgdx7gp#eAxPOR9&}>I*a26|f@l?s zbM(W0!PB<)DqDpih;-eN_WX zQ5t+=dDuiT@Ql?kqDIJ$he8&+;B_CtPy9sYxD0seRoLkpbSiv64@Dj1M;+)mpmJS- z8r1|((UJDW^T9w(yP`L41M;fDs7$(}0&b4SmcYN7BVP$ao|g?(Z4p`k==fdun8RQT zXW%v)`GEk|;6i430gTR}|MM}Y!3oZUH(Q2%pKI{mH}Ikn*aH|%OVbiybBfX+AiSAy z{;BA|8iH##z^85T{3P(p+1Mx96uTwsVZ1GYXy(A4%{;(*B9P0S!P)NOd_JI_58;bP zp<|&7jtqdNn45ZW!-|2^Lyav#qvRL`Xc&8_Ft&ylHdV_Ud zfRUaAc3KV9;~3f-S!OHjliUeRCx%r<9XX240FH7ENJ=bAVTu$YQS3pvz^08oi06nJ=s&%Ryth6#tUJ~^biW3<`Q$2 zxfQtcI`e>e3DsGm>9&esf9WZ7pbp{JkQFxq%kcqzc@b7`1^o+{Tpj5a`ed)lC6%41 z`iH33R81{pYi1i@>t(BAbK9<~gVi8)xe~5Skv~ffr2~wy#>j-P!k((}usivzMY6KK;5?S!^V zyQeX|iM~$P^(jVS^SL?Fa_|w@r+Wr?_%B`qmQ|SSCI#ps*Q9oGKwhmBR`08= zZQE?ev3K*2I!_&q{jJ~CB=xA;4>-Vyy z5H9I9{TRBqO>}kZ+D&b_$N+BV=zpEOHL z)!u2}v~cVrIE)NlRD0P67G*nwbf@jp}>J#OTd|rC=f8AOGKRgUwPAOJx zivfcy2Dvpb`x}RF{mOb4{i!xms}Ib1B_wu1+kpP~S6XR(m7WI8It$J(2;J>zMsahc zX#lrhfa>-L9}FhD18Sno-~w8(*Q}m&L+T1P!>jyIU)o04``M4#mZ+}YuCF~+9z{aQ!ii-v4Ff&oj_b@l&e*>DB%fJNvt#{GO>mRh9Kzs9Q zt$}=<*4}83f!7bz_Z#hvJi26M!akT@(952tFg}_$ET>f)x$zsm1^G}aYNNa8%^SkJ z?4&e6&ZML&&(+elD)vtH=eA0=P0Bs_iCq5w_)>kfK2YBkSlRJP5!pbU-vu>oF#1|O zyb_q$EoLKgI~bKjb2L!zOu+4l@kB4D@7DT5R#6ydc0F9LrJv9%1KaT%ZJ=M}tqPFx zIsEP_kg(#|iC&K9LMPKgWLx#3hztqaW%84z+)?Uq_oRk&5TkDIiU8o+3ij2%+72ZvA@CrbZ#PSp#d!Q3z(0$>S-mXe24u; zG`L7O>VvX;jCIV+Zw6s(`M?fjvPzoWA-VAXqnTN>r-80oef^=nLoWc^9S&|tF~^xV zpkMi|(pGk~S9r`H=1t&;#QJ6B5H%1Tkxt7)u^%Q`X|5(>bkkH#{!4m|D0Lsvd4ZVDhgrkSC&p!?40f<@wnkg) zpmA;^(ugsZ>Z7!&K&Sg-e_ICQysqe{uv5N`F$A+{3%iNOik`qYpP9#ihfRj;D)6<) zG4oTIJ|pR55xCGZ@VO6}hi#Vj$q7nkn_(Mh53^HSb!81a#cQ?^_}mfsm)uY3gOxp| z3i&&BPj^AA_94&qTAR%|U|tRw-Obn5M&L$~)(fMwF$E}fnDIkffYJ5VKImug&N|(z zb=HR%vE~WuCdRiKC|!Bj(MofoSqw2`1R_EVtTYa}c}HYV68>3Z5!QAxc5M`t2FuTs zR<^(GeS$^@oedfl+&O5J?HzV$Eg^Bh^_r1%)zv7H|%S0TW|EwfscXL`Z7yJKjuYin|a&(YRUM=g}ywC^%b1u zO@pi#hp?y9#9p9E`eb8+S;K0L|B(m= zqH`2g(_xyI-a^fnpEbbPqUC{dRb{%GVk;698r;nB*7?vC9=gT7**Qh6Anpf-V%Os- ze|aq~kT=j#J8I39M%h-Wd*~x$L||Q@2YhNxy;GoT;Jk5~ez2`}oOfvU3Cb~Mq7wXV zw$)VM8qX{5*+63>i&@6Vp#Ro05>dvv-e(YF5f3PF>DwglA;KIkB>?(Uba!j9+CC?mVKTY6*fp+HSt3mo=u z*Yc6;w&jiiK|NTIVfqJXJ^zReNWxQ?5EUx^?GNTU+$xd-!MTsP6HONz0n% z_hi#T!PC4p?&u%&%|Ytf}a z`$H}UZ)a35?(5~>ptmzq_1}R^x+Uh=SGc!^Rdju`MX{Ycv(?6^6PW2)n%XE8P^Q1N z@fr-yBV)ReU@S$1d2FoJ-@zxYG79k@q`b5doiR(~Nm5;Qix5%A`VRh}jZwhdYBd*C zXgn*9{AsZ=T0LnSh#jU+oO4_&LVJZ}3-t#3=xZcb)!LdhPVM z&jv@4p1xCQ`Mg#1LRL;QLVvG^(>=l8!s|qC3jJ(bAfk;3UDAsNvUpx3S4z5;QrdUI zxWcdSbaRO@%h+#jw7OVsbD>^FpK0{v`)M(`t)eR1rHA19R`B-lsL@8aK2^J=)inNC zocx1bmSvQx*cC|ZvxAp8{UMuO6+-ufZ3^q@+N6XSUsE3^zE6(!H1$pLHuot0Jz{ro z_pk`}0~!B?k;+rc1#l^83cAsZ596Z)mJ%bW6FylMNCjQ!2#}}lH);ykbiQ4ZO zaVJyv=^m0F`$LCYdrggppd7H&i&n;Ej%L99rZPQk^QNDE~(j{_;@q5t9My? z{uF!a6n$P$v&acicO0#aY$<<|uLf#JnQWU`KOLJU81VPgO1{d)0aNLXO!XiLNwvov@1$RWn|Rj)^+v?8P6aEcw0Y z_qD{zY2MUR*cW@%`&3>M(IV5su+g-Bda;zZfn?=?^MHLdZyOk7{dV-u{4v|as8Y6* z`eM#vrY-f1C25OG)}4v3odFKr52Fq(lnq-XT=$Q z+-wPEz75gjGPYTYjSe##=t4h^EToOmJ+Q@h*T2yiKw8QJRa4DjyKfsAbi$F>H8pfx zc-0K`B40;6&S;BT>24`Sc<28~{1J$aNs3F|l3F1pujiKdGs+uOKwpry*56Go z=$aGKQtk{kS+W<(m?b;O+{W?PeD96)&qF2|ntn3w*RQ>C$?0dUkJ5hSUvQs$&?PZa zE(DBt8+rx`^YWGh+}93KO)hKerG~K-GZEFzUE`|R&`<;Wz0G|k^e$qtv_QE7MmMMW z$(AFyo3pel*}W>PPsFnfx1(A|zl!P=IzZy-w`1>od-ZckLPE-#6gjzGT2U*>Q6cJn zsQcc7EY?lC7jR0ZiZTRn^@;xM5W8 zBV{Z0+u!g;n{L)ftzrLaYo(@0ooELj@3+j0#!CGrdP=Svd-x980$m@=$x&;%F+*>Q z|M4h-Y$KO8Nb~FctO)$CQ9-GI^a{LiO?jXCCg_PXn|o>Ks<3*OEV;};}NO?)4pEjg23$(|lIIy}@-o;~ARSg)XkA+sFAl&2(|mQaNwC1ii_ zZfU9cz`wzJ-E%p;Qd-5-eyPpURsS~Q11Y9-vlX?)$&oA(+}$$%(E4Sx*T(y|`?Koz ztuKTlyDiNh84IyHy^PTdF?qf=8~^^aNS|c2AmA#|SvCTFd$Xko+qz&Wq@wGmD@$nC z@UxNKqFY2A3t7vDC*}Q7=v$uHc8Tqi+a!i2d`-D#il72vx~rM$;62QVbf}_qPFkWXvl*=n)T|;a zr3|qf!AqR6A*0=o!Y*a#m61mtcGaR|Qvdzc_Ioyr_+4_lq}vHRxskq4J?z@;im{vI zByY*`*#9`Z!C7q8_-%ZMHU==@rm>w;a8A|0C)wprgpT zc3sw*grLFQgS!O??(XjH?l3@b4}-hAyX)ZY&fsp*ma0?#Q$62Zch#y+r=|MTk$twj z``ruutNiQrBO;}|9mLCFc@mzhMEQ<(+L6LNF=$tCKA1*Ir$`d^I&5U}ZO-DpGC!Vv zZ1!o{_Y=Q|{%-QK^UrN@d(GjlnIXsB@3dTU3+17`SkTVkzuiY1`RyC*Yn^LdTVZnQ zE|vFRONfcJ#g>gt9al5{K|%&^E`6!FMtP{61yy`o>!v=H>k!e3hWD!|`pqGHFq?D% zUtcSs7_DG0Na_3HE2Wn(Rv7z@#^RhgOa29Nrk_%Q_+TUU!sPaq&YkYI!C8}xN{TBl ztY+#aDTM2)-}`gN=l7p?{iyrr#P1tFpZ`)5BNVUub8t83D|sjpk6pIeu4?Xgj-2*e z_TH|=!J9)q20eAOSM0_>Pm=h&vE5=O#ng_y7&kVtoqwM>SiNFf4S&{HykE`L_i}z| zfViSJ^Y`&}_to^@)vJnkVBBu%h5coHRekS#%l#o>Xiw^|jcw*hIZ{2Trcf`*oA7UK zlGmz-Z37+WoVnfkgP$eYlk8xMhACSlo8l%O72ow|rSDaK*8Nj8X6_%`pPjKUy(#5s zXhx^(_cR-M_=g<%-3@~-yUV)V$U*pA$!UPy0WU zn>7gAE(6hbT|J~WQT7ojN&!dLA7iJ!8SKj@e@c*NE&QE*C-71X@U`~0(CZmxMPs5y zY2=OaW$^G-$aF3#D@ugAK}%wv?&$8?8&oDq`=pnXRZYG!beXHLlq13SEBVj+zcR(- zf>)@=pAzvQdJZM6t&9c|fK~7XcC+fv0?v%inNH0$(Nz$hlcx4?wXs>tAD5UnVNCoy z6w>DjO+52`S&U%fxwE9}(s<(Ag_I9+O1TqxK;MXbjxwD14Rn~#Hu&rK%6cz)zIq0G zv-qy!A=s-Y6VuIgQZMiW)A`OKeCzGWV}7H$ZRzdp9GP6@gNBCOO42#>XOf>m^X(@^ z-oz6zUH&YIIUL(0Hg`-CH1>(!>EeQXRe6R-cD$O}=C_}AM!8zKsyng9?LJ#Jdvn_# zWuci-zXp!4ooBRXlSlWg#o}%ThJ1m(z;NU9`iiVl@onD$jh0)ULDXouk;(WB!pH-M zLkh3rt>*oVEtC~2J{XimOY*6{k_kKuFLO`6n;vvTYb~=a$llQL);Y^92fqt$AM!S& zLvRo0H+h>ceL}6c^>IbwbHvq*S@Ne_?3%<1dN(tUeAd=Vq`F$u?9&{>oZFmE=RW%z zTbykze0!;E^~gNWY=mH;C&#a6;KfUV1v%cAml@CqJo0Jq2{*}*ng`NsxV(ax;ZpMH z^O^lbMZ@ENjn60-IO3bWJ}@}c#s3lkcf%)G5dJ3Wy&T+kO?jX)SS_fzY$5nWw>sar zuDfRhslnZYcLim1>DoqfoV-U=&z(GYJ|4mmdsJr zanOF!Hec(j<+Fu><>MALeW5J?x$1TjC#vExrVq03#_r zpjyO{niKozMP$w(mK$%(H#&kbYhesWb{r!!qBC3_Ym8S$0vrZ2K-QliX4Fa6<$cNy z^?~-;cGv!oW0P~5YqGmp(6gX>?t0EG+DWpci|e`JJerx}$ z=&9B){5=LuHN|L#C$1=H;Oc(lwqN+CgUS4=Hw1T|OKilO-Jnp$1tj<)|;sf80w__hjyZXvB`6IvDoxHX~ ztgtCW;l`RR$dx=re4wGRlrebZzwMXwYKug^=pqH5F|BALeT_eCyNfUfWTvg}P6E3?d<&IRJEaZlW_I z$>&S}9Z;6+tt-TC_JX1M$JiBMVL1jL$X0WQG)cY<{;`TOlspdN49aP+t99fG_)RMk zJDY+$ZVXS!8RB-CiP`=H7ylKG(Cx5?jy48>44e-~$7*Ahan!g-j=_0jH@>x>tnIPl zxQG+!$TwXDN+Tutf$Kp*6S!ouM&T3M=Um{nV!7N zyu?0Au`Y|_H7`iK*+V|+38cc=Rsj^zS2o<0335FXXRa&Hl)79B;rO?+;|1R3)mYuycJRd-5F7!)e6v zEWX^1#5_w9Z_UU!xWPBriFgY}<|}d4Hyp3P(A*7V+dU^z{E_oLhn0!;p0>T`b3Z{> zTfDb<$?B@jo%R6*wwH+ECt{VM_-acLacoW;bU0ZGQ$X3ylNXZhy^Ni7Jw3M*9+yop zyDZ_=8S-diq;5mWO8bvrfKC2 zv^le!4NqZCSU$r!a`Ar{GtI^4Y<${H+y46-#PeVdJMTF-U2+KboD{@k7?D)#N*Vcf z_Q18W{O`J1zyxOG2n+mPYT~&rzAx~c#^FExM!Vi}yaoaG0z}3$vRfY$RenNN%oAR* zo>{NH=6FvZe*{DNjhOEr;?9X=7ZP-&-(|emP7W7s3;N%Y3@so9znPX_&d8mn=P@(0 zA|35YPn**6S|-|Ty<#2K=TdMap>INIp~QH^gO2;oj{cq&z5&VdoK`(1)^L^MJUwjX zG#*2;>}Ra@GYj_dd=F8t-Q-2@22ZkwS1rDfBaFvM#`HYr1>*kJUn~B9CUA{2c)3sV ziQ{|{#tg2qD{yQ9<+6jQ`&N$4#M?JBkJqvi*O0Tfh+`o$Zx*>a(~)aY#LWL9LOlxX z!cb=VaH7xyh+Ow$mG%f6U70!7ylKUnY8=RQt;gDKzznL%QH?l%Wsr;&h;5f8`d@-o zQX-IBxaiPX25VhZ_m&WL?pHpjsf%CRY>3 z1PFvUj#yUr4=^I%!C`!0ZM|h>y+Cd}1F!NFFYtZV-fb`**TKwO1_5yvxohQ>9|kFM zkYgtjVjIY!EuhghBF$DJWBv~CgHHuPHx&*85ceSN#&8S=2Vt>{4*?f908Em_Ki(H4 zX&=s>0Y2}pM2ao8^RE17v6y!Tmt{Tg#9=+}2vdW_kKR7OpKfugTYT!x17}lEdyNCg zz54vEM}(mPXZ^rqL!LDXJZnOxWjzp}wascAH92a)IbNCLzsE}ajST!(jb}B;bG068 zan$8|^}q!-4jj#R)`VYf#@Px?PAl5g7EPyX;Os@4yK_%HIJ=|$^bVZ8x!dmC^&tLQ zEbIe#HZ;&HR?m!}UxxAD5VX8uoFjQ<1pklXailqxJ{-w0j%Q=&(=q)0D{xF9Mn4tw z%gn$r3%MT+l6o%ZV%QB9b1Y>$zi>-luuiqS;F_ME6nF=(VX)Fzij>ff9;6=-|t&J zU@_HOCuku0WeU2E#a};>IcWWz%(KZLJ*RR`;|k83243XXt;5lIQRc|{QBSD81YN|M)O`o#`@&JpOf5D>zl94P`vO8Pd8d~>UxGjV1N^m|q_3!Fw-83iz- zjD|HP;fzmF#>zTNfZZ;|=#~B7cv|CInX#^k%&CElvSd|FjtDyePmf9W=Ufn z8}itKcbhVw8Z(z#F{j!yzdCYsMcQ-?$R0}qSu&^(bJ3~+(IYTB|EvBmm^o`ze;9`3 zu?~wJVF)vLFmlI|)$ZAy-ZZx}Ft#jH%YiPM4P;>+wA%b&1OIz00NSt!c)H@~>cz>cC`G-FvT_-YavTwI zdCmxIt_ZmjXCxl5NVzJnR`}oZ@`1lqc-?xZ3ZIJPD91Ofzmfc3G4R<+JVt(?p-uKwTemZL4D!B3XtiY4V@w6Fv%EWu&5xFnWWQBDi#Sb%%V z8~7W}-+XA^1@JiK;>^nbc{p=(SXzBvdct}o2mf1rk^`+b3uk(IDosE;wlv^W=)nK= zqNM>R;jnalcRgnzp;Grc1kaBJp#W9EDl zRz$sk1yzH!QUgS1Bu6DQ;R>vv2+q>jpcW$5V!kOB(1Z)I+N^3Uc{syaeL2yHv#}1d zu`V-kr02}Uk)9Qrk=L>XUddy(XrT*9l$$%xVBy2*nH=;)elW@w zgHKsrv8;xY{9g?2oP7LOiq}iha|JmI{qOZ6{8yOMvLy=e%;Kym0G`%53kBXQ%(H@l z>lWbqCHPEFYO<5`1qVFqZTEK$b5qp(bV%IvOBkDp*? zDb2i{ix;B_Ha?YLL<+M3I!G*t@=;iyQ<25X$V2ag);a*(dslYKy09(v#YVO25#0b! z{Ryc&6_3J1*1=d-(Q<6a;j9~(zS)HLV-x!GM)p00K1_qx@+jJ3S^OYL$enA2ZT^9E zyB>*u4~gHDzSWrF5$rD`*)7I#{}=JIbOEttdFX)dFqfOM&RVmgyyU&?N9(pKIyFOg zKgE8Tma(bF{dDIF5#Xwu^U1}ur#idL40fMpjCfYYx;z;tab(pLWAzB;=Mnbt8SMBC z@Fvwn=Z#`N-wxV60c&zIHsV!&=N@xeX13PA0v;W(m1naP?c~aPey2EfM z9YUhb!{;@cxzvDm{YxDp9naHYtdgZ@TXoR@W5joH0MFzsxZ4KuI9l`;CGaTbCIVGd zlp?pV30bbQ$TvJeuImT!BA^5NVbN$n%c9Y7E}@e+7~yc_R|K-K3o9UsUOB*yoJ4*r z6=c6k$!s6PjNAr8axx{m(qHLDWMr$dNZA1IPYZI5lPXW-IqXM?$lD0%65}eF&0u5f zX2j`pz;`;Sw$lY3jUBL$3?drc3-0luzL`X9a`;>MFZ#a`eW<06fElF$3`NDn1o)?m zGD|Wri;hZen4BNWtrWkqTJ@>RVA|;m>a~simwle2lykf@t!s*FwR?=~qOFts)%eS- zqoh-N$Z16c+{6v>zAiF;`f_@s6EDY)iJK8W+;f2_(OzFF?*wl?y|8$oclKTN1o?6s zQD%2}t8!k=q&-s3NVU*s>(Upk%pO!ldLeR>slV2|riZ~8KhQhMQ{1C?EBUtjiupcz zYk|%#uAlT*_2mGyec7L19EB&(lxwKnw8dIFTQhrEXG-@`ccGvkL7kF(Op+#~igP0w z%@4IE_UGDBc{1x?ki&b|JfoZ5e2Md7i~nx(du42W&sN__ujYwOT;*G(-}4)u6A9ZB zPxzvYn`Wf)Tw88;+IOl$*uOIo%eR3O`XVYx1yw^!s*RM_ihlZSUr+BU&wI}SZxP=y zVor6uyM32nJeuS^n|L^}i?T88VnmIcx@%F;4kF`KlWYnp6Wj?@deYiyJF9tryr)+MA#45#MN_QcHzOA}Ii z*7&yT8^uhirjk-yt<}`RK<4itKlz;6jk-+hwDIu&6x9wZJ>^&M?XJ`N`)h%5ck6%q z$M|M=t9!e6L%j7o3ljGyUi8%QUG)C|HCUNQW+B$IygJKiv@>)0#K8ALMKsS zCXLHa+@pkRg|o1;fzxn=IX-AF<#?l&w|V@dn4K}X<7y|IN_>^rGBGGIXCgHQ;&;Ur zk4q8%E1{?NxSn1LQnzTsw36yl`5l>_HI>T54`yh4Z5QmV9gpolw7-;;(r?4-xB1Ka z8xe`#tPg^%qB=N*d5HrPdc`Nk-HG3vxW`+^-%YPh-MFj9Q)379Su(1RY|ETkf8x(d3dyFa*FxF z&}I+`Gsw-!pjskG&5(Cp>38 zQ#>(=|0d4&-1fXl9Fm~MFOC}>pE+@ycecL=oF~nUY?zHY+2VrC;QCS4PH=B@F&w3>XAmAVT0c_ zT3M{sus?OAars<+*K}7S*Dlvbch+EkaQ2YT!9#*$f|MK;q63=IEsIROq3eK*hKHb;Yf1K#-O(KA;i5NTZ*z_Tnup2w^dA$E|k_#U1 zw`dPh=ql;S#cRx7zd+;#PtlvY1P|a@8A7##?&LPiA`>eOF;b5aEsA0J-;<6KWu8Vx zQy*Iidr=U1>70k0hI688uj?XGAllX673=Ko9O_tQ{|Fn*ENzdPPHnA>lTVP#T87-| ztl~I0f~#D2HQ41dVENCI*^$Bc35P_uG1DjkGwcVE3NOq=JZ=V2WEFo-Df~Hh@}V-C z@bZf;)Eqoulr>Tr&&byCFqa;Z3($#ZbRB&dTwn=$9%D7EX{SVcycSczfNX%h;es+l zZAFI6K9GGSsNA>B?xE_437gd~`$zjf_NC-wJ+yVTh1<5l#+MFmtq(A-JSOw6K1}m- z$#@)!@2DT1)z)~7B6u7d@F}H(7xr&)434`O=*KZ42`KH+NP>>!e|HCOJ=2&@_Q_-8 z63koMnGJ=Fcd*YL*DrA2v3gP?j6A3@Wc6I(Gbzx3qVTMZBvZaQ-h@uH{2;kU4d64G zqbApOsmtN)$f(Xz-l;|HL+nC}A+PS8Qd~Q&70~*sHnkOe8jIoaa&QMJm@j3Ojxd6J zkYeDPZl$~>UcH^`E+%KFoN|l2&gDqoPROQjmA?wQvdBA$c!%s z7C(k8pZ(V^aneeGy z2R%EOUk)LnSQ&fZoO~V)Ha`*QX2cZl%OAMQf^e1G!D5hz)31^@6TR)Kq*X$B%nFO+ zVzsZj1#Y7y)Wu7s7Ue$2DW70Cc}vuEi@cm!JjS%@Vfw@pG~c#xO;@D8%W302@#7xG z3q70|ayu~|p1)V@YUhYBkL3<}8#%;u=F>g;ygNGQ4%SIevx8Vc%sfK6ftR9;bX_>n z*k2P<{eZu)8d-vU%$?X~8ZEwrhTIwEf)rTrt!T{{^x$XG5xET>jEQ7OCR4W2x?C_P z9+dMam6a=Sg1BIc`6v%nsw>U#xg^C_x(YV%w)`0DK*HmZ4UBYmnrFo|?9sF`QAKk%Y}{kbT~a>92bVxkBLmzMJK)aRb z29V9z#V@g(3s8|@lUYk1P9{67;;=G&WA3ChI~qyNX=JjVl%A1e|5c=rI?DsGD`K&R zmz%dlXR{+V#2ho9JX308-XlA}CD&Ag6qsGuF}$GoqU4_POXkp3GW;*f3cXlT3K1{K zO}`*QEZ?d$L78lJ1I;kYOsPgIPsmODg^UY@h3T$%Z@f0XiS1b9Vdf7*Gv~mM<`b3jRT_P6@ zBOPIjSt4~dlIV}5=gK3oAHL4>W=ACm3BDc7gI^JFj{IQvZp^HU#+UZM$bmn3ApWs_ zMrJXTb-fz2TNR_0xeQj97SbzZn*=9C7Nmj?+w`lkhIy7wl)%eM9bopuZZN3Xh+Y)} z9dlL617h_W%nYfR1GmA36lPwvHQ!TZri+2PwoO_qCdEdfvIRgn-mpeEto*K_Y9jTT)RC++B@pn-UkIfX2KCmXhHh)mB z34i-~*6tzmJ-K={jJwi3rG(sEgdsyd8k*`=WaB89tS82CygOOg>6_x^Zvfl+es<*H zW=S}1x0;jn8^&=F14Bh7bA<7d*wO;3On#@zz*Z_vB(et6h|kF30<6KhdKlit??xTD zA6EJ;(aQf7Fu+vmA>@Evuso);3bYIMG!K zlM9Nj!X=eKn@X$vW0H-Cr^PL=Fvl55jVtEgNp>Dx9EuH z^DW#a@$6UE%!>-e%kO{!pktja7x2L zp#*`c>xAbb25-(`&}3c3En^m5%cbmur^z=uW}KHcYK!EVutWVLsxabKh1}#)Y5ANu ztLKwO!rkbn7Q|@0TKnOB$toXJ@wFQzjj7Cw%Stp=4_=C;N?T-8b|ukBqAZfW8Ew^M z%5A-l_-NKv@~hR%90C8>T6u)l(fp-vfDI{}EW?H7G5;Dp-khcE)B4J9_2#Te3Et+` zNXyyCO+ne%%~@tZw!YrUjB<5nIDErNh`ZVQ~0wY zAkLX_iU7eSnF;uMo|kl*6ft%hem!@7JU0HGRQtXq-z_PZaV#`CRi_zK4 zjK|f7ckiy$2*lZ2^O(rR{yCgG+-;0SE^o(684Qm}ewdH$l5IH&xsX*UE3Gmzif8&% zaoEg^MpeixL?&psSwmTm%n#wyui=v$h`%^a+9>D5hjZ5$E#64SlvHM4Jp=aa9XxCg zLfL@}u@_~&`(7wg4v*1!PvjHgJ7 z(_~;ZfYEJ-Ssgtp0shYnh8>%0i&VkrD}oi5ltJv!WAu(v4<)UcThu2uy4Y+e(o3u4 zBkV0lM700Cc!-2LtvxZLsFWRTo9l-BZGwWjC zt{3B_wyL1Ac3R@F+liDPLHdoAE+KDvOD#oz-O#H`qtx?CW2wA;*2K2l-5cnIoK$uA0nsgp&UjvsVF0Xtlibb)w&SNETbPcRl*(_swte!S^8~go{ zFb}06qsJpwil3lN=TTASj5?m$KZT7;;-(Z1Ct(BTL~U~z>+3dE$g)bR+(KS2oiiTN z#vu8M>Xe^|Hq6}0N*i>+9nuNW3xD<&X`DLT{0m*UkC-N(kmupSe~wIfFJ}d%HBc(W zCp!@_na6jIiGtwflS)&>da|MyO2P6itm9hHw{(`_S)+o9Zsx^jQH}Lj1_>s_CZi4! zkfB&Cv(4{fp7~gAfjlS&9x_3)GdtgiI?VZN=3Y6qT2C1vEfqHA zHBr-3?nJatigEjGqSNLg(xJwQwFMOm33wn<1ZuD{7NQ4u=>#)!!B{o+>h3r8cSws zpa*)1yZG8~O{~bhqG7+V#E5DMv!wgx<97sg&s8pAnWm$<7NA+7`s_a(B+R~_JV7QLg zkD3KFQyr@wR&JWb@CqC?D=6o+g4%A7GFS~_AN8^Rk$b@)zfoQ-+(sKQT;4){-g!A3 z+4N1MVOOiFgefs_;a#yrRFE-yyDp^LsL6{Q{ z(-Ly|yC}c#p;J*%T-4)@4X{t%f<-k0OkDLq?^ME9Sy(9p_eWlHxv@uVkqVLV(?I6&1(iyO17?$-!Cm$Tz(L1M?`P3bDZ*ZcE4l92oDmouK?rS8&QvlQ8qr74TtvnvUaQT2V8%^RZ9)S~51?>MU%}D-NNA#y+-WXBE1bGX3dOrF1m(VS8fJ;flDvl&}*AbLjU!sa7;l~~%gZ73yPoi#3 z4cMN-WL0{KZ>S!->j~Z^BMQ#Bd?0&5h``08B^@Dpn-wi;8anVx`sN{glV@3>ht1>C zU_7YhVc(PS!abz|%N$S)ZHaEj5s90LXKNG8VQH}NPaxl5fRrjBLvmWYXjqtS$c*{S z4*7q(Td-jQtJM{e3iznfQUu5t~hoUCo>$3V8-x z{S3Zk;TQ^Xv>a5ulFLMgnG^7JOe5-;gL|~8%{IJ5UbtD`57P9W2_aG_;>^Kw?15>6#ZEcEpHz@p;)CLysB`gE0AFe*(-~I z5?fEd4dfH~;2~Q8x~n-Tt%pdg8uVct*rx<^Q7e{P1$#9Ho4YNM*1KTO6S%L=^uZ?d z>~VO2-q72%u>@~}0;_^GxQXbg)h8BO^)#AxLogYs@rcc%#of^BR#B7VKHvJuZsKJu z?$9%HwukMqh~;;b7$6Nh1eTUbOdn%-}Y6!hZ=TDy{VmIwd%9iKoj z*n`S}I~`1~=LH3r2!=301mUM@$z0n+WYpqdZH`aZ^0bs<_C7=BJ;f^9ON{>vW4a0H zI)Qlp(m*8F0}kgm8s}=}Pfudc(~-K5@oAoAFKEg={-QTMpwW^sSGJ)A4dC2HKPHQ`oU~Q!{P0m`9Gu0y3(iI47gKofZo5 z@tU9lYjVxOT+>Huqu6=i$tSKG3=+S-Je69u_3#uwfeHF2m2d7S*OUij-z9;udXoA{ z&8RieS^{X9sk-40YOEB52|SsSK<-BqA_e8qJK{xou?O~N&4?v0(2vL6Wqg9+rZ%-p zE>NxH8qD7F^&~`ej>BR6lE-7lLh9ihrIz$cxIXWY=QtZlHJ_PZ1hjx3gnCK(=C{;U zo{Vm?g^ECz*>M950fAM2t6! z;CZ{x3@aKK%T~1iZ-x6{pMLlbMcZtOL?64S1c7 zNDCp2lQo#3f51sMOMR>sp|>;Hj@y>m7diGg7rH#IC|40@cKapuB!0s$0=71Iv}?g` z=MbsIY0w8=|8w65Z!K?eSVsQ!4bZEIKV~V$vjg#zWC4!lNDylG8BM`zu80- zBPCeTMbdJ4ypo3q{|0>THDU9+s(d6TaE%sYtLkX$40kV;GkDLNv1eT2*zF>_K+yMeF;f zyw>X5-Hy}tMYiVZIzE_@+iER_|CcIB*;~D5% zqVE?&V2fTZU!^K!bFtdkLtL#aJ9{lTLP@PYQqyZ0wZm!}A_r}#=HgQxsFiJr_Iu7~ z_syX7!Igqrx%1icNojlwKvrZ`0t$l%o zEE&A#RQ;mz$O!SlY^7wejRWd-*7ezCcPDp`bW83%j(bW*e^A`jU$cI@62gt{>P~xh z$4r~7dd%#4anHNB0&zLz@M0;xxduCeYE zt~<8I@;Ch$*oCt23m)+-raDA3-##qZ%Cys?pY-lcm>f4O_FC-U@i#md_0?2dUV)aD zPl^ypjg>?VbHfK$Qy*&#Hh0K#)RS5s+egi-)>O*jv0ZIM;afzUd4No}VWj+g$Vh#BW;CGD_(bslxh*ZRx9MRvWccXYzDxDoLa6Tf?N>idiz zW^47fV|`Gs;IXb{>Q`eT)puZTlH%3(_K(iXL?L_I&nP*}z4|`?UvP`%^JO91`M$Rh z{&W{sQyZxqoH-uPHlpbM__Cf&exImBd?vk|i3*O5K%P&4SF57F9nPY7eWY*`lgOqv z*LG+p)#vgSSOzVPUE1h;SBjd0kuFKin_eXI#47P)-@&ygpAw|9$g< zI?*;!Q>+LC=m?5!@XNWJWQATR2hT1396R$XeO(oKf>*1Tdw=myn1&iR7%x5gkenJJ>Cp2v6BGt86LTfhewkbd4+ z#9AJU24cV#ycfiBFJdVNg~v!VrlHyHLF*rZRkr~|#@vEm-vdUk8&~DWq825>R9yIDu zyXhm9na=nWZ%$8P?@j%-{MGi4RL&61Y**jZ(!S^{rH zRb{WaN3ZMCJ*z!Mka>;4T>a}S=I=!ngnGnzM;Tkl61}YFp-Rbhy*u{sEBO;O0enQ> zuYe3*#Lm$W&0zz3NCcW{1!CP5V1k>?T3V-N&>pFN^|bcW*48n~S>IK}^~o{E_7%TD zbNum>@Obr73n*D&Tknhq(M{Is3p1M$O>1^}#(3BGvg)r4Nvf^Hr~|c0%;)uFP2W-T z+qT&rviGczKS;%?hLT=ggya;8Pu>IPFMM0@m<|OU778Cr72i_tVs8UqfB$T1IZOp% zRv6!sp%=nlNic@9hn8ngIts?F6BPqd5(pF2U@y3mnijV`FLvo2+RJ9m}F8x@X*E_() zwN#HXG8#9*Je>9q5`VE$*Qu)%6x060NBy_fN{G?{E9vXu3H^ERYfSY|4%J=AUJTgT9$29w*n0~mF^%*9MrZ; zgJl#+O@-umPby-ycE)e_QeW@;+dB>>$mwvOp7b|II!sWSYU{P++E{8^)X;8f2es{L zVI_lnlo>&t0b;I&mC;mv8G$!;q?FQ3MEmNYkMXbYJ@pRs9)gcypRb<(hkqY(tUQt9 z=6Fe~7Kf)Gh)Ri_A}MQr84MMX_Y?oRBb9iB$1b4*-XSIo0b^b6S)H{j;8t) z8IjIj$FA8YfG-$tD{9{k)?lBSO&)@Ne?z*=h^Cbm5kvo1T*NC-}47t-0 z3Hp!-dw+a6W$@6yp_W!K?5FU2>AR^8bIm`F9paT#1^?17xtc1p9Pp(Cj&WU#HA?I4sJ~XrcaXVP)BDwH_)>s)sZ8a_q+m)* z24Zr*(Dhpp)oLg@z@(c3OskAvtq<&ryU7$+$*fPHYI-*174s;R4DyRGj1<#s-~=Do za#NwPmt&iwpFI^j&~dEJAJ|T};gKi}Ce&k;!z%j%Ms%YQ2Igx!n1Rar1dyPS=)Pgp z0>+zQE|RCHC*W1`X=k)G)aE;-uEt9?1}u6EecZx)C03(5cf$VL2;#J!*iA%om;M}` z&*xoE{UbZvN8^3RedYY=^fE>s>=6lG@^V;F?XXL3qxHiBv z)_CK+9*(w~7L3PnbmxX>?ZLkIv-;FGBYqjF*2Ep)pb-s-S+gn$EThUULOlmV;! z4r}%Z7GWY><|V-l$KrpQkHok|J*=t9SE}EARnUqQXLbR3u zC~=$V#CGx-ZKxboR1c!=YcJz2=%cQBdgk65>TlEq7yeA#A#PU^iGNXU3dciv^*8Lo z3j3^rSF)SD7(Y&5^cV-0nbZ1^J)tgdpbCR2kW2oIYh#;fT4O| zWCu^W1%CWRcog>#bNK_dWxi4YY2Jy-U%$wpnv2|fjPIwAIadUW&EUtX8jqO4>-~{% z;5hwnee-;kePOW+@LDiR`%VC@S_i*$6rW=!MT5A{nn;> z>RlzX+EyK=&Sv%nX+yN#?2!Ytl$xrI(5l()Q(vxw_LC%>+wvEfGHMf{&xH0E%IZ#P z455DpIGu@DAjg%v#xYF!jJn;`lG4Im>HaBDD@)SQElyv-=X(_( z;Ub41J^mr9&Q%{`3O=;FwydUk${tv{ZLky91v3_-HltG5D6N8)Q>&xh(0aquJ-`;N zZB`9sA2_Th@_W!*O_05N!CH(mG9WQ#;bFPYF47yz{MPs3JL1Z=vUe zsmg&am_g63=h912K{%RyD=ipHm6=`$A6}4m%^<1f;zDe)Vs)z+dTWKdZz9o@mjd8!isLWMZl(c^EAU$S*_Rcb zSu8-W8PP*%r}NQI+aNPDAT<(Tnu(*;e#6NM&CPY2;w@YZ6Z}iAn~GhxV8D0W5q)5N zAX6tCf7K@N6$Oa-tpZ2#36D=E^rW`1Jg*}zb%!zgrFe*C`B>fG@Dd(JN)BRNGZBBj zh}Jtyu8Gx&EJThrLe76-51fE+G=v>z7k}V+a35|FX55(lULCM>th2L=R`Rx`(s}!I!qy>YpF7gSYZt%7iec68r1{& z3_ImW=1U}bnl^I1PN30GARDJND7>%q(@G+-4b3cg`Y$0J=8376WkOw|5#-8PvDtaF zcr)*8w1G{$Ba(Sok{pId{W3CFg4jvt#^TX@|Y(Y8k9lQ#hsTo4Rb zDf+t>_gkOC($$(V0)@z5Pe+A`SS0>0A>+TV1Zu1=-p3h?_6kPh2zuZ-?ra;+uClit z$M1RpuXb+6rYFC@ihQ+Mcy;H|n$cMEZHRQVBbG85yvE=U95bKd71g zNd1|N=`xT41Bi3CB~nm~Ii`@MnGMve4_o6heR-MRI*d+wlJhQ6kWGx(WHi(!c)qPV z>s5&qlp-P#$r$ElH6?))L4aO)2JX=>vNAqpi9K{cpW4eg4ln;WkY2TUwKADzA$+<% zk&l&(-7Tcd1$?j98M~j1+FkU%qa53aacpD^Px1Ucn1Py%VO@5RtY~x@$k_~%ZV}@- zOiSPJsYk?3;s6U{iQ@H7Mqb@_Ub(?14w5Z6mdM6p^kR#BWj{T;oyXT?1)dlATNndSnL@~$gf{byrKg!jTyYR361drn2*rF z%-)ZUwGjWfm0`b;-n`9NMl%y1a6IPkaeVRl@k2zA18UvJPulU2yIg~JdkgpD<=x_B z%ccbPRg00%&a+=c73&gXYKi64vx;$@rA2A_h6 zo{A5AF_Pc_b8|Ae&)t~4+oYWGFY@y~Vk6IoL+BK8W-9l#4~cUHOm!mBpHEpa__LSw5k;%clLzK{jx`A(9Cwt%y zvfuzcRgfL5qUA5+_tMMfh_R*zeVSC>&A2rs+p{CSg%Gl93y_;mjS22pf?h?2*}46bd+(7%s1|CIg)DtZ63)ur6UrWl)i6I zkH5ucbkIwynH$-urFa9x`EWG)Y}}2N3p|uQ*vK{KgOvM&P46X2*E?Jqs;qg#Bj{)x5>AnV4e{=EG>RnY(a*)9Hl=V6m;7?00yfKCygMlPhUp2 zJ#G6<9H#**&B{0LOq-Iiii+{Qajeyn{H@0NaUxOjaJ@{R9a0cAeTuXR;vTCa*PAlN zOZcTwT3MVvdBA(yu=_1Osurxb%orFW5HCc7>lHv!wz!}++Y+}IYL{~A!RafZbheC&N%ER z6SO^Ewn^N33hv?&QR(IM%_F2!9OGG%wRaMBuvx4^D`KzFe*@UrveBNBRI;-Qw`c%y{GgWik@0hi`5F5~L@})^y)nb22A~hJF@H<2uLlKu zQW?k!w41wN`bj~*Eu!rov97Isss|DE{p>nn)R1gJKaXL|t08xKF>j)<5%yq3M|0mZ ziH`5*T{nB~Kj=8)nc)VEGe?-~i_s)5um?wDS$!7C&^r9=oST`qD~ahp;M;2hj97EX znCgwU?>6&iK6^wZ_T5dQI+e6@WY1Vpl7{ zye`cAtHaS6&EN~nq>+rocPi+8Vb?!J|22|&<6Wsnc0e|2X@(K^nMB_nM}LvAd7RW} zY|YI1LXWQ?P7=%vI7s$-S*q^!WG)s*mk1_)|4T$LCzhjs6@nvR5!%igj)`V>7)1M^ zM}&hH$O%gG18cUd8O=3nA&ql@Cc?fu8=9 z5&XxT1)6drGpHXm4yU3=WR>RQSIWjX<(2zU^%j;JBtb9GPP=HM#q&7|O!9JM$#u{f z^+2eA)J4w6(pxEL%XQXRPWIB7_<(M+GGgfa9?0-%W=lL?DexfoLPpo%S`Wcy?PSj% z#cmWurb$YAv;_K3G4_M6^g=#%qtu|;5AxkK$d2wvZ57$kiNEWlbnq^uV|Fz|UfoCf z7iVv?GAJ9$Hb$xvt9K?rt5_+v0 z8f6hQuqXImEWIX#-K-W!tHbPqUTav%d%#i^ zL>50VZ-6+P%}zO*K3v27$1=b7GLj>ZDck9T{Pgl5fFxpdXCO3C{VTmePTG z=!lMq&Ck7TM0T`CGkAkN;Q-4P#;*SrX}ts-Q#E9CDq8gz>GBawQ4sxQvCix>_p*nM zGdGh7I078R46ZkhwJOm&HQ4=}>~Obv=Le${g|<^%@`_gA!5Z=XORPi>pB#r?l^gpa z3OQ(_o?9eyGaJ~xh0N}`=o~BAl{L8`yXi(S;XZMadme^uF`1q7KG$t1kE8D`?vdQ= z2BGYG-`VF|Gg8CY<3}+I29T#SpVqZzmrjSC@)+ixgM98H7FKiS*Lv(fmG38KM~h@H zOo1n)E>}5#wBJUbmcovzkA?aR-;qWS9pw&Mu@_vzhWo%QEQsFR9=+=UYq=UaP!YNBRf1tphbk_J*p>ncai$GtH^T6hcy0Z4qEV6MlIsVDyHt2O!;bnIT;`a<@UrV&23DOLC6C+a@obg1iv;(>Dfc9B6 zVI$E)8X<9qGCNa&A-x6u-Wkv^79!0e$hKKd-o;AB!-IwQlV^5$C-qMo@$L(<++ooA31r;ue=m{X&uL+fJXPoq&~Ml-C9M)n#h{ho@iyEq52UMF)m2eIZ~ zvadM;+C&NDRu=kq74!ZBy`6@88-<FYs0u6gAGFKOjNnV=RAu(?j@;ROd?<@~X7NjX zX0@-QKi)I5-r*xyz}2Jp%w~FOA9J%GBWP(q|9N9_(R-b-IOZbfZ!_j)nO|?{M~i>F zF6Ss->%hDVW^MF_o2xQo)|C6`&O76|XKVlXM9+=HV#vrhx?>}hWYz{DPX^N4Tj-Nq z%-%}OrD&d8>~>9Q-D%|1KHl%aY~2NC?j|h41I&(XSPMHi&T|i=cyBIy>S0@ZQ4LHiZ$eUN#CMU@BrA9P&T7&QQ=eKW=!TAi^@F(9s z1paa@y*YuN-O1Rlyuq0RI;=H9 z9qIKs{QglqG8V(#W#rOjJT0F=Hii-JXakS>AkG$Wg-4Pl-He$r3XkzLF#jX)uNEZB z=@etvi5}06_Hv#4x^~!)21tWdVhk(;MaiU!BjR%0*ko)lwo)180@0VZU|~{;A|Q18 z;(NbJTrLY3<%zWOHTp|_TGk)E=@9eOhPS0BKJ;5efU+nhh^RIojz3n}i>E!GIz&CC zI*1Z4B1%$`Y9}sb7dq7k@V-J!V!xaZhWIP|jj|vMy#KDb8SZj7FciPL{|5;|md= z26``j4>9z1#$s@tr?}2M^1stkpD7Dh4I__l0D4n*`pG2bGZJR?Z&VFl5ANcbI$6tN z+ic5dpK0G?Z%fsV)9P=T>>=|GUU712S)~i)cw!D7X{0&bSm0NEb-jJ599z#nK<{Z} z661($WFrp#QtwG^z)VIJtcERQE4`Fck_Qu{yoXz2G%N(+a!EYlGpTMpoy_nb%<f#Lpn>E0ninv?{H-cO3AdbS8 zvc^b9l(`WzcPBpQ$zrYXSZ@Zm$9&?9HZW$0wPdLp_0|#FsO1Z{0zWJ@Bva5AgS-j*#C!Sx*jI z$4&4EpNwzNndT;nw~hRXztM_bNdu@F`N zkASZSDRDpWv;jHo6NPxnu(5Z}_56ssNP{c9%9%GhhjAhav&*Tqg^GtSz zTUcnxkX^aqy1381>?gh%D*w&izJQhEgJa|&*uVos@CFdKufhsSE?$5_8O99jN*r}E z(ev5HGV+R4@ESFV`#s>liM+PYIBVP`w<{YlSBupmBf9HHqH~|IdMjh&b;kp-8|$Ge zyUi>lOIzepLH1*dPcAufCXC+gMvWyGh@el(0T6fPK!$nb5T&8Aia6Iv;*>XG7+i)1 zeHfpN6(I=5!m2_{AsW|# zyBkWBYb5cni>$*M=vJe!O(vu7U1PoEK|)SI;s@d59ZOGbB?9$_>fZz5NqI#b7AUpU zSIQXWmi&!9svQyDSdkJ-voB+L8VUIZOE{Ogl*pkCMw-6FLW|%}3F5BrVPAOJ;}_5i z%h`KVu{KJvLXz|9J+!LwXe3yC$je*Ucq6&DSBym~@(B#qzzgHEp>f4hj7u(&hKPA# zB68ihtHt1HJ~5X{5mV`le`6I9gJGiT+5OieaSCcBkI9j(nZX{bFhTiXHG59vt)1tKO95dJp*RsF=6VMELvsYzcZ^+Cz zS{l(za+oq9u`nix+t~53z(|I5wgQB~BJSro^0kna$HO%)qaTFe$u3KLvK3giMa=tK z%*eAurY8_T%Ru%*G`neXq{V)ua}zvmiNuGulRMFb{t*91(|y3*T>k$9KNF!6GD?VK zk4n~8iHsJ~(jdx6NlC-ZXdz@IMN(D~Qc|I=Km6=e?OEmM>?9#E; zE6q|4{X$cC;6IN&s7Egk^)fHOmZSORqI@SC&3b_x!JGADxdrv!Wv;d5;T)C&bto4} zu9>}zHhU5c9M6{K!?+rjT|h?{hud#}?IyrAZ^0_oSpTS>}k zetTH9;wV`TZPf3j#AGwH-d6G1?q(WJrYK~-0W01L1y+XJ-?YMYMIDRl`xwdp8`sYh zY3)y{U5PeNq17XqsNJc6RxjgU4B48Wi;G7^`L4`)UT44$!KYurs2+o=KBh5?s^{!W zb7Z4e$s$=1%fyaz{fCNjM-5m5(S)}_*@^BX} zJ|VJM9SVOMO?I^_s?DIf$L+y?kq)@Jqg={gpwNW zj34mRH)gsAYHOc=)IP0NY1hzB_aha@^Ac?|;z?+{zH5y4=_S0V*NbpIg=gx(W24bt zVOZodwXR83_BCQ%Wy!6Q-}SD{g-7k$3vyoH^Vhqvk7M6?ERYvdF!o-wpFHDQ(SKmi zp5*@_m~sk9(8#Wx$Zp3g?~{wqkcB;DC3lkx&^3E28F3S8>ymu}N4;&&e(}jYy0-sA z;$E)qY%WZC3}2mxzS=`^!(nXMdE{jsHFi&^0y<&$CdUrOu9x-uZv1XJe8-|s(Vz#S zm&PuPtwn#Y%M>c<)nPhl9*_syC8rxe0|d%;CoIEQk8h z>L_t8>F^Pohlhad{0XXl18SFzn@ke=W08GH%T_M<5lJzK-V2!lt;No-^?zrvh3ClGS;jim z3`U9@hnl`QxOAr1r`fZQP+6Ln<92q@e2Dvq_*`YQ-%T&wZ%|cJ*;CVF+ng^wIR0|H zb9|>fu0ziKY8`tTeN~I?R*7~Y1YAQN@|Cc6%s$kC=N>aQu_$_Lla*_yw&kEs63@^& zyJ?LzG)XNpXiU?8iI=|RXD`GXn&BPXXia~or@l49zI6H>{8zWrc?ay!OxSp&o-_3! zkhmoFg4wg>%+#X$T9AN4<#BxA1h5&hRKLhZ=$QxUh?nu+Q9hG)r1AZv?hEAJBD6V> z%{R$zTt~~iuTt%OJmRov-!W zjkNeiG^UbRLO*j}iB2Q1S4mnlcqf9tF`?@3Y8i2JqQ9!%xe1paf;bkTn-)fKQs;{| z;LirACsJ!W)WaRxqm$tRn;oOlIan@HX^22;W7s5qP#UFC=Pa-G;_~ZhxboINkESUmm+u2yvtNvI0=;mRs9DH)y?|FNMhEpm zabwBXbKrw>*z&h|G|TzFOOU)};f{@V=rQv65%a81wtY+!&7oygf#>qgPTP%RxyNz8 zWi!P|yc_7}`|QwclCT6iz8lJXg>3iP={}tD%W2 z-TA2K&~|I_w>5d5UJSWMCB+v*zs*AM#|<>w!+yGoQ4U0XZ?d#z!q)Hj{8rKRe?%jS zk<-`EwRgjq9qrl!Id99;B;I3Y-otCH>42IfUxge-I&GbPp~v6mU+86D>Y=0ryXza+ z=tX&6571ckomW>-9^wf#6elvL&G?LLscdDlZf4${_2?LiE*GjE`9@vtKWc`nh?}&x z?jiH$3-V|m33(QuViS9OKd;t!s4>)@{}Uiw=kgT2VU_i$eDb-hR0 z+zuOslc##{wXV-$hmtz3$){I~3;T+QinI=Kto)r2f8mffYV#7q|0Xtvp%nQPROUh5>s z?)ndo(!uUMna6)xh4a*pcJaLx>Zi}oVXq-H!?*VDKUn1;^jY2x%!a7)BPCI1TXTCH z2X=6x!}V}Vv1n-nF zSd-K~4<0P27v0zFv+0?a$j52MJeNo2v&@gW0RJs7GKOLs!5;UM8q;OxZHAD}QQzH+ zZhHo<{>1M8!Ha$o$u@wb+#oVj7ZzL&spgXpt@!@t@*?GvB0rG>-JyYXdJ>MoN&C@U z$X%)`+O^PX-{zXV@k|4{Y9nt)Ez~;7=9)?+hwXF8~c?VJhy;x5fxE>F8trk||enKGDvV6jA@ z;pQlDDE|9FJxm2zuMPPR?uJq(lD1Fbm@v8lq}NK4cug*kBUJ6L!$JK-{5#Mcwei|+ zV_1at-&0@vS@x@(KdBlFdO7`B15I`2DGHX?8z_4*&HH`M9xQ;PZa|^IhkUga3D#RU znd}TJv)lca7{w!y#r5d%8q%Sr`160Ry$iNBX*aKh*oKgEukrzZ&IUdV=T;{x zABOTjR7q8wJu*;M=y08xjylh9s@I>3c7;67;)5v@JtO)ny>>msSk>b^*zXJbb)$Qh z!cW`n(Q8n`XFMC>jG;Ypb1TCc;q06qbmLu+VK`r>A8j|zTFv)(DaVbM8+Rq+FRI#R zrE5IPe(7ZIYSZ_1a9=(9UXKRp!+w4jwdT?4H@emgGVT=2-kUbs4H>pTqhojwesKO` zG2MnsLm6Ml*sU%z`Q~_^_zd~TKk@T?2i3m=hgFVshqY$Q5`Bf-T}NZSVx4=^za5Ra zzRp{t;G&S9FcW>pbJ2uT^hXhTrEUH{^zKdU-?_dsmyWB5yY51*cd#BW z;j1bHwPaKX^kTPFL33?#9kQCCfk#;}^P}fQ9~3R`0mnNN!nxD~ zVmC!sx_;;@b4X3X<+|i=p(#HnbN5ExSF3lLjCnDC4qX<(g%!|eqx|;S`D%z(WG*H} zCzAS4z=8+(d%nrsWc5Fwv-*>nHSJ&(9{L}}jD984PDDz<*Y~jbhLLLBRd}?Avd4-W zJ&Zcbpn>n~*KPFdQ*g=;yur(4G%fS3+erMTti_?dW*+`&8EUBk1*ED_<+RjwJcc zt{2kj_F_?kK6c{|dSHOqOP0O(JM`9)hHq!A;hfp$@&7aK{RFA`A79HyPJ|f8i`j>y zn*dp~VS!{-@N|?_J{5kLtdqgl^m)GgswOP4S8)8_Vz@O()N5&{8zF}oGA+Z&n+L_n z{)Qd8@@gI8)xC_Bb%*Ty@6knVnEb0~`FP#<$Y}HY1S!=tG8kVq(q(U;y!Ex&^YHe2 zB=^fa1FiTcH?l|%v6zmDIKHPUV52zA37ob@_RR+N*diY5-f%-zAt=(X2gtQdDH6c)`|8c`{+Ldp}L4xAyx|RU)su$G4>5xv+W#I-~-Myh3z; zGCepO?yHxh#Ud=y88l)i9*ZKZtX+7f84p7dnzsQPWh7qfs3NSZsNLOmGM~@>Mm~vm z;rpdL1Q+u^)ri%OUCIvonUpN8BX9{jc4l-1kM@zQM}B3m%@B(|GH7VnZbCLK5*PFVn?l@r;<)lRR;c@L5cR z7gnQ)mazI;=xj^0Do#GGzNbyBRdkTun;iR(Y(5!Xmj88jSw8AA;ym&FYn&4Ih&_Ll zSA7GTA4OYEWm!Lj4$pvwt`OP&9yTh?E?&jE|Er#2)nxvZlGpW+dFeBXKHg#j*UO*H z(m0vD-Rl?SuO#O$gJFxJxLW3W7q3k4$6V|8rZ}e@D`h{4`V{T908*Nny^=(^mKArN zQ7xrY8}K?bu$Jxh(fXb|2xpYdi!`EF_T!Xa(PoIBj4;<9*(NtYa-Xq`=knaVO4lxl zR*H^*@O$uH6mas@8La%het(>lIg=N4q&U(isJ5a^p4Z5%;r4$$YTOOupP&CJi~W75 ztuAZ44s82%b{efYExV1UYlt!5%Kwr^(;Z2a1*rNjlv0=!{)KEQ$ItN&kNZ0i_ds}H zGXFR$+=j9R!bpR+wkaDcMn+Y#W0QGD?<8{;;jgEx;Th!VMeOF9`KuwG z1KCUUPpzUFwjZ5e9iF|D?erp@|AgNyG|FwbZ@XEJS z&SH~J=j(rrB4y$cQ~gGIV3>J`7V&_UX?f?L~l&@qzcU6H!{K8RSqQHiG}9 zSZu85M?k#6dod4ZonULUKtt=%*apv&V1+MOch&KDIT+OJ+I8s zQaurb>ZIB;_^N`sgT8pC5!~1fQao-=F2(D5L!#^Y`0zP<{$Yvi{jr0*xWin}S$U}2rkYNC{K5LwoEeuCs~SA|rboI1p6n+3}(;brN}>iJ!~ zsUCUu5qswaz2y`5r=vA!0pXN~O&9z8A$IXtc)O69&LSJ`1BKb8#r zNNj3|ocJ{96_IUoQXlrFybDdE+vGb;BGH%8Dc7lwf7_m&MyX%&H;*MZR`E~hr;K0z zU={5Z8MsWX=HoQ=E40JiWOrk_eK+oCAVN6LV>Qpv4za?Q#ntZ3^fr$b?A02mtA*K= zw`+G~Kg<><^Z$pQ_p2srKx+KT`ibRyn}=oK)*;P?@<_eJcM>uj&O${Cb!~VDx76Uh zx+L0_To@nS7TXi+678KoEZa=3;gHhm$}Ek zcsj<1@+sNB1ZrtSUWUB#FL@oS$d~RU?=MuD_s?I)i_(yu+<@;tXJh{YpWTw_$+r75 zQ$SCHDgL@q)bKmCf#ukO&2da$x@`ilU7Q?!m{s*O?dx;^nR6$M{xo4n< zmw96j@@4K2EjVg7yYY_Q%re|WGL@5$eJ>kM-zE{IjV#pex*{}UOZRX>$-D9e@_A0m zlgMrPhdyPoUt^?wX`4>QKa-ZJV-z8CDb&+kDkd~0C(D+>8&k-$rI0`eoWCZzQ}y22 z{Ed&t(r73z(l@#?_FQ~VtV(n;`)FWfO*EV+Unc6*GsyHY*|Lw2e4V0KI6>qZ`!JJj zw2LSI8EExN@~kjE>h5lfd7~zwhhj1lL$y&&*3@dU=MR!N*gK=iJ=ur+Gehvs>CELK z7YF%jCYtkbmAo6(tIwii=dp2`p}wy0&6QB}Qc=rr-thwT@ITW3JNK_lL!U)9mE@@l ze!AOOD*vg8?cV8+AhaHFK~z0k@hJO%%WnAZ!ZK~~~`e&c>5b&S=!AxR--|`gmmNPoTVKX&7jcWPBg67P&z9#EI|2KiwDtq# z%-qIb^*CSiE|JB8;!#2SH{o~qjyK^>9;7+EW?l7ToR3dBvfA#<-w2DXRJHv{rXua} zI!*t#jx7UmN?Z8yW48S?Tr$VZVl>dpIPP2fF`t~=A-k~_zhFE5vnFzsL;SxfFWG%DdREBLma_&4Ru{KQgHqCzrUfBlkyrFZ*HnJ^Ewr`BI;t}5hk=@Jl9NLN& zXS=kJ;cz>T&ps;_eCW%efQ9tUU5=e^I^>tD-}n?zd9=CzH$umzx~3(e|j$Ydd( z>50f?sA-Yv*9K~p-_`-|pmREY6tlcUC2S;q73+C<^a~clzDQSlbeZh2tt_e&*@8H9 zyWIJgR4P2i@3x-Z+y~#bq4DbT|1_qrpX8(al&!Uu7X8{92G77C^5``)8cJ8B(AG3N z^ocmpE41n!((QHhxK*~p3AFg3zh-BC(+lDOc?f|De__kq=UTwWf7e7(*Y>*N#M9y_S}=!fyM6KBP%M8m0iuSVXX`De@Xcv9EPy7ZU& zNz~gTQZBzCEOIJ)h{Sl*9ZJYj`G&XXA6CHIbo4h6-=i#%Vx-$rGutmywJM)L&HR_) z`*D2rS$>eG*)(1CJ^kI81iR(#T#|iW<^LGnG^S^|${YF~Rn~+#JCS#rA*{vZOnVm2 zq)2J}y$@ygsI_V~SiCpDc2^H1!wQxew2uSh%_ZyI}qyj>hWl4zH>ITos_ ze~e_K3u3d?vzLw*(ZO8&Dl*iSRQKZgL8262sNgOMb;ec2y$lgON`@Zw*aky?K~vp{ zr$VR7HRMq*k@$A{7F@}ny)#>t<^Pl4KOnXHkmCc$utj9{3i>u_UrxXiWg&@Lxam2T zQ57-yw{YfK8ow0{@hXY6l3eH|(`1J#|4p$pY4ApTflencsv2*c_zIPkYhh|N64|a@czqaw_x;BnU3$FR9?giYjunaD6u(~2x?=Hes^FiB zzYtp`4%Swmr@Q0tp{)-3@|-|-Uy$=Jz#1Ro^zr3qe+vyNuV}l zPzSQ_P8Rjm{%*y;_?w-)nC*Bo^zfzMcgkBRCuC zTtP>Pk7@iV;+fBhjvvr9^LHMaDpyQ4+LpE^_ca{y!bPLYImivEcg~&);`5&DbqIC;nh;mTdB`oaphsyo@i! z=gy3j(MxMm{^7`ewpt#0_#VEaEAjUk{yG)eYM(=f_Xj!eRyY;8wakg)>Ji#P+Lxn) zEgoIvN?n-0jfL5i#2qQxu~hHJPuS26;I*viTwm+IH&X(o{l}t|EFZk9tmh4g49w8on$S#G` zkoWL$RPd<5uXe8(X(wI6>xxah%`fyR%c?B=FhgF|Vu)}CTKtw2uE&RUYtFY{5vmB0 zqKP~_Kk?Z9YUk#X?DPF~O6>nEnM)yK=|Ouq2(6DalTCOyK^uik@qE6iP$}7!)$q7| zeZpf1eqLo%)vSFZDB=qE@)KR6`^FZ^#|Saob&)3U_{$>A`(!fBbhTk(fca>zePk*h z>E$e87L%y$5488=B>!v{(#v$ud-gk=usWS4dWJq)Op=#{F#lj%HZjtA*6}l_V7wjv z2&Nv!`rZvSt)=G*XV>ZvbQV-G3`Z5o{z!hD$W+P2pgZ#>v|t+zLjfTwb-;6mof|sP zekb0yi5D)Tj_7`t@f29|4!SraMpT2nG1KTTlT{JN>$9MpPyzg|z3MHOdaQNd;kD2= zSFH{CFn|W1hEC>?cT@SQI*}Hk^1Lg5;pMEXpW*$nWZyJ%7{M=aJ}v!|@&68;?xjVS z#RGOK>;iY0NonuyA@0k(M=SMOzen&kUVa4lEYk!}g&f=+Nd~d>AQ8sGm$&hmO z&CtYFmYB1C$caVX4H?kmtlh(?W|ZA}!*d5SIYQ%9;ui|B%EEjhugW=jUmWFUx^)ie z-5u7R1m|s%kFtWFd?|WN!7F>f|;E(oX7|-<8ka7Xn z+=a^ndc2`a=q=E1p$)moIV`d?^aFB>#p1V4#KPT7YOBt#MKH!{26o4m@Ld%|#Y z?dUunnYZ!InM(N2HSRjKVKFSp;=sSM%+(mUGgQ zJ}nXZhcus$$FE>t{A>5V_l;4g?0V5zedAf}VKqV*_>E-LY8Wn1(KFE7edKV+dZ@&k z`@8(p1Gr)xE)TJ*kPSLS9P1P7zMEA4&Tjl_K4l@oUeMknzT3+>EH>iM8GJ7Z7-RXi zM-PK=!%8v#CTM0cELt|!N-Srl7(i(qWD3RC$0m{OV`Qzhj2+P3sjX~=s0`7KIO1(P z6nabFWd7YfZh+h`hMx=4v72(Jb1a#2C+&X{{eL1}HIr|=E1!7-o}60x2UXE8rjGy5 zBfqQ2f4ndkbv)B64cOrK@)h2}yVH$yTd5OGMf$8BFJ)VN^@Z4HLv;O6PNts6Ql8Eq zxF^R2FUc1BH1yerOD~oSQ!w@kN;!p=#*%Hl@Nf$#D^%xi6PYN)<}Ao>^BcNfg%9>y zpOB^6+TCipTP0Zl`%w8Pn(|I#y4lEgWfs6IAMGK&Q0LlT@%M`d}?BlLZO4f7NJ9cg{0pqqg-<8jf#EizLM zh^#)%KUTy!T>Evo`c(9Gr1*MAJ+v>${Et850SL1T&&Ks~?e90Aw@BA*K6@%t%=d=l z;Gb~ym+;#Wo|_@;qOIm|*!oqMjX4WNWMzT0U^{n1OHb-G_@#bgeRbhoIM?Wpk`fbrI`UAeCWqJn>hpDa< zgT6RZ*>m&E4Kf3+^nW{fz9aO58pXG<$f)x(h2>ZMEn8`;ET&?psy3Rsz;k^*_B-t4 zQ1WUG$<&;`ca<1W6D}z8fb6~nhCibt?*_UnHgCbU|*o`86sU9bp4x=zd4`-@l@AMiJY2yMH1aYhusV@jpkSW zhS#ewk6KmHuRL@4hLt!QzYI0*$7q}vSdKHz?6n-O4f(B)*t6zVt&-Sp0Y2Kl_=C1X zCgCUi_BVZ1&Wx|6Yi_p+cfoJh^W>K$`wr5mHAqZ(;4XE zY(AiWtm$V)`#hBU0K`9pEi>MzW|`w^h%Z(709g}qye+X5%%@s*yI|C;i-jqWzt2<|b1 zYv`edERFV%)p3``!zcoc&D!FWD3hhHG|Gzu*4Gv|;CAqw&pnY_v;w#q#Y zG3L^+e|>gHO;;)_M}EKW&Ll0Kv1XHVG(FW{Bdp@x@bA66g)?(#G|MlLN49KWNrl|f zS76SUjc$y6?_~E{+vRKR+%0IipVgnwR$4_4twbZ=`_?+^HqZB7a-aULd!rS)###+= zrFX2rSNP`#pZXG&O|W7QqnsyP{Wb4??$uRzY@z#&@Om%QIM{nHTdk+;_^s|9a_=hB zSIzNexcd?q=5_OVfi@j()k8O!H?8|ZRPl;YOtH3OP`cW1^PS+e@t!C6&b#jRo%=1v z-EY7ZPa1E(TyEW~<`l*^(G`cgbHGUVpvKF#Fvh}U|etvlVZultSg z80xNLd~1UL`{15#=&X-X-NC93odUYE5kkk0rtG!Q(QY7)9_3p@P-GvwaI^QW_kTa@ zFx)&sH@E9}NV>WEXzM%O3QRMvpZBkGOJY-)l}nHngiZ z`TKFJ_J5VXjo40&*rCoeH~vRlD|B`mZUzs#W3b3O+QB=$7iKlej*Z93Pn&tTZWrTk zO9R$+$C|Daaxm}3RgW5Bzuc%A8A%hI*3I|(=62*ZvuG|0N>7*jn``%Ve#B+#Y9a?`}-RAde9g`7lZ0>Z3aeL$^Y{?T)54MkD5s#SFV=RQ?1Ok zpLKo_osYDRw~(;sitzo$`q;w03EuF_+_$mSsAb+Uc@TTqCMRINpa(*wZv_Zc)WGMz z%jNd%pgI1d&(AWRl9179_dE!no?|wbdL zgN~tNNAqh=_xgC>eT3fBW8H7#a_DD#~*SHT>GSNO@{Y4ENlOH*M7kx*b>1X(ZhW@`U(DtAyy%*P}rNvX8x4f^mNrL%{%lEZRn~U zjV7FwIwNN_FSZMFa=SUvwFkOh=)5!_7cY4N{&>vlPh)>=Ms-C|U1fCrFRGa6Q@45a zFs`@Y^dC^!a=*P`rbCV54g32&{Idzt`+%K3gU94^^syHerr37>@$hW(t>tLpZS*wT zH^Qjr`1A&}lR`atJ{3N>+bi$b`={*Ai>@@!XI7)I-RSqYPsCAX=y+3rt#SYzZgHPA zJPhA^{v}77zd7F-&F z*@CW@ndh#Yf8nJ297o&1>l5nf@{HkJ7%5aH#_-%p-#JYZg$%skt-^laI_Z^^?-cZz z(zq#j`9ft%VXtK1sO>%%Fx57X-;DE6j(>npvMzKFDq?o~%xAr^{s=3s%B}AqHI8L- z@~bY{b3IvnGf94RPG+?t+p0m^N6meYmH#V;#Ukbw^5#N+#)hP9kRy#e*CcH(5K|6) zJ|cXiA?N-G&s>O*Mg5fYPA#8rNcXiOuY+f$C;1*8J;}E2?%l?fYLTbaJ!;Y$Ey%mB zq~5K*(cDk)(sdz++vcQZ$e#^soM*KP7-x{75&qhU$VQN3P7XzJ7t$P+jI69RD~b9- zcga%zzStdt)V|Po&$XHbeZG|O*CK^4A#uYfYKlZ%=q@FFD|Cjvz1#fl@7gDPI&j}_u3`qV9K2PUDo#>EA%5 z;gdzIcTp=4cBYj7&vBPCy>h1YFJ~7j`E)tla-qLV+vV`Nl0H-1YhkC$dq2D$cq3^% z1^plPtfcpy6X-J+`oAj5INzN@@1#IIC5$OhObMSY<+-HqpXqUyM-gKVyHX}cDPcWQ zcs60>^3C+5NAS2Oac==X3EvDBOWY%L`zmG+3;Ml~*F(qA0!9^fxOi@Sg}oD2uc&V( ztxvdGct5N|SpTqk=NgaDvCp63f45>c1z8aC_fF(EJy39{MRa003erfP%m^!<@_zVc z=s}kCTZj;pHjfK@KFm7oS=gh~?pfF!i+HV=5f(R=F#FJ9Fi59Bi-n9infpDgQyJe6 zyHhZ?hXJV+_UX`_HfZ}Y9^sX;=3mB2m(M*ye&Yom<#Ib&!E53DbB(CH=Rkq~CowZP zI!LZlMisC|A#*N)XDj=B>D-! z3%4i5TqAx8qQR)|1WA0t&%Zu%#1;PW%>!glsBbw&0{ugx93df(lSqeUKnC3x=-`yQ z?2}{Z6wDmWZlOsE+nb#^URaAVLSL8lH29y8#qT+^xGon9`iV~43{n0-wr+LxP$jm9 z3_R#wr;RWm*1fdk-+l{uV_|0h<@PPew||UopU(#@8f5u4GJ3OX1)Uilx)b{ASATEu zUXY_f&hE>7>;EL~VZZM;r|^6*Cx;JvKS<_3T{EDA?Y_Iib$9tcypzqXGe~+#FPoa}vsmMFlppf}5fhYdu)j8>I?^%Vn&3886`Mc&k z3%&~evDscd0PPjhA?>7be{GGon&r1Lz#^`3T0hm|R`p_Z5PVIi>_pIDYt68m4tP7{ z9PWjZ?s4Cj>`BOIt7L!2*v~pTiq=*;alYEIRlf714rkB1;(o6@m!p6W<#lzEO+3wr z&Wk*xYuVLuOqPbz1C~EK4hUXaEESMk6M8mK@R)tyU6u$<)udZuhlqg z1v^=fwJ>2+2e&!a=n*({xQOn3yn=svB_5ee6aQr{e;C`d_$t&@--+S}x$03gbca4@ zd&G?o(c2gEZZ3nz*F!IN!OgqKo5DWf948$eXXC%G*#Qlr%f(CfilR19VX-7~i}=!3 zS(}}#$p%?7x7gch*6En8W<~8z@V-xw&GoST9^h1@4swE{`nS9X2UqdhOICd<8*@=M zPaJ+Mzxp}y3P+1WREVnG$o`T)hM#e5=2f1IBiUGFFdA!w+Mfz>)5rzZu{Y^Ms>;jR z3*q-MYG+F8`gVk$aEP@yC1d<8`T3p1?~i8h<0E;Uz0ylwnd;z(layiK5A*fLXIbr( zbsu`4^~;|^zNE4r%PPM~#G<`3#al+MLC+ zpoAqk%sn}OH#Ar^e+|hr#s1ajW2*0rHZ`2t9oD9hJoP{I?uny`BIH`W`mY5}Ne(h= zK6-sWf1ER{J~ZyjWT^bd;+aA6{hKWlT@_h@QfEQ9XFARIUB10v@!wf`P)|m8AHtQB zT`kntOtd5SI3a0>)3NjN-{-ZOZ*5M&o$9>v2Sf`xy|0!$uV%6o2ixH%aBWX})JK2h z?zs1ead#p;^vyz}zo~$D-|VWPlV9@ZK(Qs|uGY;-w9v17gQ}x~@`_iepn65#(LZ*% z9-rPyquq~+`s!wS9*@I^I_}owZ!Q{Xrr+`+5_$<9dL4a+4t9rdVtFgl(>@in_sdw5 z6{30wlaxQh&M7A;&T*f6&`XMK`Muxgx>@rKA4xuIXs zQ)_YZ?U9~n=vJuWh)l16ev6>=yYTu(-65Cy=FZRq6{T-6h7vqtS$udxPS*)LQvweZ z!thVE$^Xc!cb1xjfgZ${P!^5lQT1D;ACY?A-7-Mzw ze^tk&LdL(``Zuu-zv$)hEl$~&-(Roy=(AH-D-vFKnx+; z6RKN(xBH>8aGpD_CguJicPGl1pMtMHBSFX6)!=`5gbduu-X6!E51xQ2Mlqdd<2qi1 zF>I30cxRsX{So#wp!O=RSrQhh1w+i`=^W0>x{$9k8{upgrH`wCxJmTmUXSZ|=BuiY zImP$8h3|bTPh@@cdYF|HqJhuz!8OHIN8QKbH|3@#8+jfyE15M8}ZT|j4X zNnFGv1@*aY1sjtQhL&>PY&?BU`T@&ULZfa#}VzRb{YK zn0CbObOuf|e!!__f6KEvEWfO-I-Zy1px?|Vp5@4ymbp%q za&4#j#GUK2CtXN=>|?TEm#E@9rdsU@jPpT=} z=kksZ=Bi@yZTfcCZ<#+<^mnF8DcQo;(lgEKpKzK~ra~owU+7>rF9S&gDdTY~y5J zU&4+rjLYWpCr!xyoVm>k*UQ8*zc}HlO#0VUEIlkeBK=uTLZ~>3#Br%Bc zvg}LC+4wa#_KLEdGO_$vK_}p~a{|=8@yp}q#=mfO($d(MvCq}qz|O#rXEZ6OgTF;RW^Mz^=qnr`h#=_c>)JS*!QagnU%eqY&*w} z#c_BU9^!c(6LEOxm(re=`wcGXi~6oW4@X3{YLg!eac%!d0a12UHF*3AQK5b^Po9u( zvJ#D-icV!*l#j=qh1w`SF}_`P?pyIo;;ZyU8l)@88=k)t13D`*o?mU2h~P)r?M|zD zBK@e7mzp@Wd0A?3szvH79qpb^{pC!>m(ri4SEWmdWdEo_b%89a>&Ub-Nt`m|WnF&c z??t+rS&grZryS`Md~k~&qsOqL@Z)2|ok-a3o{wiMV;e@MQsWGXW zQrW!HsS8rgQ#Df!Q~jK{*1&mKJB@w0{tta*{rsD~jpT@lq&!OxeNQ zd(#LDc&*#$HC~r*`wM6>Mt?3ck7vol=4iZZ^k31Z^=5mEoRhP3iaF)vyNjK-epCGU z`0;p!L>niaFZCFxOW)G?y!gXp<9^i+#btQ5hz5jks~pVRbVuo#i8w9%I_IvpN=2OS zm(4qwcS)+g^T-A|+izd0cY1gFE;$Z=XPSzz|CzlSfBufr%Ry4Q%+u=q#M>UHwTi%) zpL@&_oo_}~UW;R+Jbb~z*nm5#`0Q4Cq!MXdIgBD&o#g&ies*=I8a^-Q^v`%aad~2d zv(EoZ>_|MDI6Lu!vt^&r`KNUJHT4lw8R0{DK>Fd(TeCZ5yzEau=w$NuQn#flIUBBk zabM?j!WPa@``pQTOVYLF$t=U6=c-NoN)6l?yB|Caf95=wLAREp*Z0xt??Uzc)x6Yz z@~e~cdvf|XoKCfkbiZC)@UKWX?XHcyh5Jaw7ET2{9=lxb@)Z4~HplbgbrRhZ?#VWcbdeZ`jXc!wOZD`XE` zjcOXn7}v%*oBp$!kqs{zw`fQ%kl0YQCTC^pP6APco%4FQqR+@6(;Q-aJ(?RU&mx zs&VS>)XS+a$;bxI|%=_t@>F-l->w)=H>h{!? zsRrn#6W(2&N|2hv(qE*D$yJ&!Lo7qKx0K7YAzMzYrJBr|!*Y18HR_>uqaE~J2fbV< z+pr%_jX-Q6IvVnl?}jN?%N7n*{$tcyuZ@TK03l9K16np@HQ z2dRzLz9Km^JiWr1pY`=9nJu5{xb?qAhSFoIHs6$WyIzjuNqpQ8y#{412X&e(r4_BC>(mS*ZhFTK*|r@u^%BNzp!?Nm=T>I{&XGU%BTN>P|J6%>jEQ)7G9s}(gEw#+(?@TpK)k#&=`?q=Oj?@dO zkLlI}sr*zmXl!cwtMq|%2|Z$Z%6nTb7w>F6I&PqK!hdk*X2X4L;NpUN4~ zR|6Dt4^6g2M&MPD%!m45ovoJc#n@l^JWO!PVKKd8N05xiocG!-IX*cv`ATv~^2TKO zRMB*@7tP)<|N%}DzD zoO1hJ>Qt&sx_ zNX$?4b~5jF=WQ2{&xF_(M*kQ6k`M1&T;=rd?6+jdIhi%iifo-eBmFau9i1A4QwOHT zlAG^1`SUL{Un6~U`e7$YzLnmRF6=zyjw(@}m7(`5>!Xz1yPk4{Ka?Asm#xm*@GP5c zJKL`sOY06a{|@O`o{V`ux?0b#i)H`LkL{vQhs9SqkG6E;+Qi7j_hd}5WLb~W$w+d0 z;$tV&_DWolNF)}}UVlS0WnzovVE+nj=ab3#Xtlol%f(P)smw>|LFv2FjY!PePIkQ< zLc0i`R!mn-x1ztsLGVlUL*9};;jGf?@;SS!9iAf_atCXq8cVOgPBrWGma2+h`$A)% z^6Q*N^&KGfFJShHDklc3^FN}SZGXI))+718_~iIEIgQ#k@mk`W#2Swki3y3}i5?{6 z6;Ro!_(EsXCgSh1pbEv7va^n}x$3bJI+4+fSQ1s>gPECoocMkq{R=DOM{;p$dOWLR z5e)pLGf00_X|^Z*r;!(vMSH!oOM8(z&yo8-%H52}+`K}*^C%S=-^*XVi1zAY{TIOl zCwanJv394DlK-d>>8X+~^aHrc`MhsJw`ZfCp7Ck==Ny9vdjy*Q)M`#tZFL_$zC|ymw(@4L zb0+RJDuQqH>R_Du0_*QN^8RJnonJy~1=ueYay>S#)idN}$mg(3dEILGyN0q`zr(xd zllBj~=esBmbYDd1AWAb2u!UeThunY4m4LojjW3iMlAOnym4v>it74 zUlCndPok7KM8wI!2N2g)v5a>wBX_TZ@*bBZ|2==e;p|!Jp=!y`xK|d!0_c7h=^1)Q z4Tk?W$yscu-h8rtOh2pV4SkJn(6RXiGg*qNma|#E<}3J`wKt#L^o*UlE7q2+QXuvt z4c$qe?Gd~AB5&)ptcT!nA1S-EHA!6#Vmk~0e4`>_nriEr*)Pb#i7GGegNQ@_qK>&v zxNUH2YxZ~3Ts6y$I)U8o6J6-v@ubf>S2#ldX5fd5Xs`R|u9+~c}O7(`+K>5 z#dJ)(N=0z!@w{1m)~V=udiXR&C$(bD%&M{HX8tN0%jn;-Ql;$EY7!oZUL@yl2U|Ym zAvP9gDXG?SGtbw{(A9t(x~dPSRO0I`hQm{=kbh*D?x9bARYkghZVKI(U(}QBc~u}& zWc!X*4>CxF-vICRS1C6`71lwI3*}1R2s3nN(Jtm2-vc{Tgnat3OxDOiEN3lmkb~V> z<zJ>eY7ragE*5RyuSa+kj>cs9V?~6MdDs zoo~_4zfO1A?$xtl{JPLj8%X&cKB}cG;6P3P@O7TYlI${)i^M}doly`Ty{NW+2)YtuXM5f&L?Ct&dE#zp9;xT!Y7HUlH zhVDG;tm7c#XkwIIROJl8t*@fBPh?x_+Av&&#u#?MKvWre9yVcj zRk6=~*zMQJiTV?j7DcV+Lf3b)%u0KwH_PT9{L)nKt`mG7q0X-!}UsZcSLW@&Y^#^0Ff7@WJqKGo&p+-H6JL;qdi=w`m1r>n)o`72Z>6!OXg zc;zvCGL4sT4bAs~?;UWxt@h%id8YYXGolJ@$+3%AYBOZRj`RGk|NHU?d}=PU^_KVm zHErb^_$pT;FrRI)p6u=j1@s|vd!e%$t`Va9h54F_xYA|#C<%RsJd&!s%m10rCuX+N zh>t=L=Q(x$M{8aIO`e5E+TgRBaa#fS>1kO}0l9w7 zyBKo6UWTUr6#FRV9zU5?2lszUru3U=YP3<`E~c?bw(eWjKlFsSk4NuA?~SwSPlzo% zY2OC$KUKu@A=4eA}0z&up^8j7w(uC*7Hd)FRZ;~OETBJ`R0OBC}A z)ru)|d%)PQ!%t7c8pBD2y6m<)aas3V23ZfkJ!SV6!XZDf_=mXC|7D@?<)65SMcogM zcqC^9jpw}!oyE4pDPzs&gPfEKC#H<|c$E)nu9d$H&tyko6L%j$iTU+f>b>&Ix6Lo(mDT_m5Buq7{GobdsyQsR8zqgY1U&x)T>L-Pq5Js{j-j7wuAatkzl&D=OyX~{ z>(7!tZ(7T~I5)1Qr3wGQ&3GhF4ezP!6;}TaW37+7Yr;+=$ndeAhwxU+%3UR7?OY(z zPyyG5UW;$@@qcB!|3^>E@%c=sbKSug)MG#C2A*MqYP8e3N7;E8Mnt6s}2 zQrRMGvj@o8Hg@4Y^fD8zZxGSQpzfWlhDY({5|7XMvW_^n;w<$mePM_Xc_}98EA|*o zaWDP-BI~)GikCyyc^^%3jDGBw8(%|~N67tMOlA(Seh-psx8t-TYAsK|IhFFa@)JIl z!wsMDo^;H0-5RM@@Kw5bI%{SqWVIHm{x4Uplb5~Sj9(;|2KxC;%&m`6_K_F73Juhx zzXs`l^eoTM@n}63aW!?og;*@lL)LT=sO4Kgw&su6GzOffA(Q7yp@_RJ8 zj9hN4f^-f0^$%4*TcDadAh>{$D|r4-2Z~p94jS(~o1gVMnB(ua)khVDuTM5OP=OxSQz0!||(m%~tCW+MZo?LRDZ_l^E4voPDrakny2*vl%(J z9Y;Kghb9~M3vl@j=r1l}ww0YdPS=~mVm;5PJKLUrnOd1MRiiD^$LP*X zO;M|f=5etazbg7(4~BELXB$Hs_t?QdQD|wXzAsPTme^6ggs#q|Ims$K6yL=Y+GLKU7p7lJPh(yE&qz!0cWy}km9C{Su_8SAIIqrR;|=Hf zERS4aKl|gLQmm&3bs^c!TT>`eBQZ7cb7Fa-XX2muYwW4l<70T2mO=J2qcx(x@oKK) zF>fkX-QOMs{WTcx_QDAt(QA{9cN9taHLeXO@V<&F3gmw%p7($%(C>_JU;2;q<7nYM zezisEO6fK%@i?z#f1Q>lrAIllW>lsbG&zg^={}X-%jk&Lpr3o48ZeTbeRjM#uggkn zIxMj#(J@&u`EBA(-i&qpk87Ru(>A_PXOT84a9$>3>)7$PSub_+Pean5v5Y>?mW3id zCF38@>8L+wwNP(RS*-sD5_X&!)z1HbB{x~;-kt2%>AdmP^+;LHo3nr)tvt*2j?CoD z`gEssm2|~SwQQ(ZKhDZ%N*i7oDXt?@16I(!SQWmz3lftOUnXAQi}^Y6Kyq<%Ve+D6 zJMn?L(eGU%1nXnRqwB>Q=CgA5k|ihEU?+1i&=*xjKF)jkD*xeJ7$z%oGMgRd#vzRYX3H8AI7_(-J? z^~_RHg!-w~dB^jnh#ahRlGUTB$$3xa)kuv^f24Oy_3R#$y&}Vs$ghP~o6}Bxqm5!E z;!o(9(?tu*`;vDh$0R2we@?F9C90defS;&G^6X@VM2UDJHbpYv(?H$Ly*)8Cvfl-DWMJ)O)nOgH6Cotv7K`N-OaTa z{DZ_>$$IB{$Ue{DIDHMng&YGJlg3JzU{+ zKK76EYUa&5efU(p)8+GarH_ko8 zlNs^F`V)^#PD)Nlj!Qn}d1>;iWSitGiD$^%rt!tP;8u=LjlQ4XpB{Y$wf4d3Z>3K= zqwA+s>GWg#Rg1;comGzo*o%EE{_H@zg>HNm>5a#kVyb&1?eLHqe zVrAk1(ZVTK`m)5@#O29BsCQ(tugBxb%E=S_ZZA1yY9p;zI6fmbBYJ0~NPc_Wjb@Pd zFVaSDr~gfLNj;SMKJ`@kUn}%nCY5^Yfx(r1cmtWOnBU7vT&>2;?@pK5k`bl#x!u1xLhjhRNyl{+ibF#l-e1^old z#EM1Vi~Jj16K_t2RZrBSdCyK9NVHBi7fGp;EGCLolCOD5vSxBc{2|>B#>)>G7JoW+ z$Z5^D!z)YGYk#5-;2-Hkx)+aZIh=SHh3`-I&veW7mI?7@B(8q61!Py6ud5=dGY5@~ zqIt?hm&-p`2d_PtZIhXpUYB}WB<9b&l6kM4-goMv)5A`0$=jSR$N%*PY4mb>Pi9g6 zr_tYdmCM9#Baw&2t`nR6QH*T@O;J0sFVQf01z-LjiH(UH`So`u+a@1MJm8FtkMs*V zJ3ckGJ9?OX6v?lh9quHzQkh-puSux->2uPXQ@c|o(;ua8$s~B$3bIHm(Tl}+ke<;$ zwWyfWOgS14#=e2;USqcpV3*92O>%E$Lb`%{jK9UbzRip0-Ff=5)AyggEAMFP+001( z!-|;)Bj~#NoM$sy<4)H5_j}wXac>{|keo0)57xTnV79}!?$;ni5YqEOs zyZF~I=CatOIts0iy&A0%Sj5R^Vkd>xM#5f?t@CAF}?hB=Nb)lzQCLKWoB9Wg;dGZXL$?rrsv(2*C6lKypE|4 z(%10MwIpY55jUzL!?2u8p!TuJy2gI1tMLtqhZ6-7z2kSpne^lIbttd+({YUBt`3>b$?>JAe zXKG%$1XTTBJey;>q7c-_A+S zC%t0@?o9Sd%!@x1AHiq-vW{mL$C^c(I=lTpKFTf-?{Ig0Dt%M>`Sg=A5&DZ!72+#C zrF+*{ojH1l*!Q4&m-2aqzAbMm6{*kW+J;uv!RdJ_=tt-j@T1-b>GX{BZRG!_sUE4v zQ&Up?(^rWQRizmh!>Ok-U9vOf;1%H`8zvKKf8>Ygv$~eemhEvowjtgtu_1@%wuoUZ zO~wnnAev$g3-4hFx)^(B1K&ww=P-AHLu#}BztsO=vko7(Kzn5~D@BqjiCb(; z|CpK2^Eo+tRG!!FOwa5kFyd&oWFOe8f}Q+CXXy5^b99H?VdRgB=l4n6pLjRXD!Iw| zltl}?We=W9G)OcdGrRKZmyKT<>mh>DU9R;1@?RJ8E@b6DpyLmuhltpY6w`bNdI;wM zF4ZNnFKiNe>zvEF=nSQXJe3epzMS7YoK`W4@4JWy(rngBZznethxy8iPaMeHEB9q0 zDle&{Ll0J9Ti*6>czS=4KYu;nRvEtF(kB)Vt%lTx@f> zu8;?0ZS|wI=IA-r#f;Dco2G=8RIc73rszX~h=m3~tsd4Sm42wJp*sD3@MtG4N`;y)YHHN<&d5-oWS z5`B%udOTBFY~ltzs6sdLd=ZJWc=5X#>)2fMD#EunHm0nDE+_mxzI~haq)ra_e9yw6sc+H;o$mO2Zsdo^1v)YP8@(g8 zNYw4d_$#7QwG(5Vsc?0&w@7XKIsDR8A44ox%F}JB__Hf8jj-0=)qB$htc=RZOR1 z{7H80t%=tYza~y4&P`U4l~5;Hk3`QSZ)PO!HS+gl{C%nm(ogiszx+leW&c;npF=18 ztn19}nTzP@Z>{+da;BA7XL-`5r2jjhq8V0xReCGSu)Y}KP(F_7ID1WIFUzR9h)-8` zcj%|KOVhsrp-Zlj5TG2JVrL{l@T^W zXgoi@32J|cZ{(01#jDtq^YuzOVbw1T)et;h)$)Jf-D<0|&r$0**SLbs{eXRFOH$t@ zuj_V^+W~lVfk^nP2 zd5D!-(Rz)OzkCi)+;imOF1;n2$!J;9cd`31&Z3xW9xunAj`udBR`Tdh&{!{t zrC+W$@GEf7@BFEE(hR%V?@!V!(fnmRa2<^0O1-AT>0_mE;7)pJ6ED)=aBtGylyD;E zNj-qh;+tt=)Yp@d*O8J>qVk0-v)@GE5>ETLSf*bq-k+XU<`s6ycd`?X@rqmzH^0Ve zJtm*!M%grPk}*Z)Gt{?U&PewyToe7PP4wo*sm-f z&|qtzUc-J4<D z{i(a;dDitZyU<$nxT~Lb__DtI=QO;!N@wO*#nbDue7_Ylcotf%DkpudYmat^aMEBK zR9aOGuM93YDx$hyMolqsmUHvdB77U~Ulou^CYpWge2H{=+kaNO_5(8`J2xa;E5 zrXDxQISUcAVXhy}y!?Z2z7Q|n)x7%8!9+XQ0EO8o)pJ=2i)3-_KtsRf?5!;#%|Gf1 z_p$8gY2?vJ86=(cJ&K45E<){nNUKYDqfhdeuS1a^@}h=})JY!0@bF+VqCJ1r6FL8H zcOI-e#9NveQ|LI*9GA3!8f)`GmSEEsA`R=3SyfTX)#3t8NcMJOGmp^OPv_!^3uF`T z6zi+WzxY5dt};Um?66)pXW4Aq`Pff-{ zGS_a&RY#mjN*2o1VjQ(9p&o8I8rr}g9L@s%!dyaUz0et8Azxb`9`}B{TOm5z#BP*_ z9JASY{t$P+|QYgB{yuRVv4joL|(PjPkyNAF4FRQ4WLE0|ky}Zp&=z(5Q zG@}OTd%5=u@}QI^hjy7uIKS#^ah30k`!;Ld!up?S-OoeU39*(IW^n;u-BCW|IO?j1 z%IcdzTQcSu+B8IOpL3mm#HIF&!aO7k?KV8S6y1(RKMTzvbdK7}?=+nhYHAGO#KY_5 z?ds=am+-7VA^0#C!=R^K0Thr%m#}VPQ!TZUVXHecC(Ym)qx>w-Q%gKa#Vi0ZB z64i0{_l@LUc;*e>^r>RPy^X0iq)=HT;9V>DFtjt=9K*SIJ^Af#x4O@Yc}=4q8hHIy z-)$l`HZB**ET8jsUT!xMs(8*7N$M#w+zy8g$8Q68=Fb<0X((IgQmcKXZX50Le-f$x zTU6sFw0?IP875vpmLC+={s79|r>d+5?hG|Klf`=TOr8MrRWCKa-% zyaW&9#tEpmEy@X9LhccRdIML7v#l4|tETP|@e|JDt0uzsm|2X#PmRPr--I1QN3o%> zRB}P|{SFx!0SLhC&XBR?6 zM^VV2GJk9z95oAXO^^fe20T)SjBM$z7jk-i1PS*COJ{{qzigd<5trB^Q|AGzy_*m6 za=RCLJscJR`=7B+MazT4#_#8wR3nGNU&N0yNREYQ=>K(fC*U?#ZQQ`mKIa(9)F>sS zkYuQolp+#MDio<2IAc;^-J{ZZI}=h>5s_vYb3fAtMs|QUi1H^dLPP}M>F>Q zJ~G=(4igH8*a7Z|lpHKYy-Fx|HLcsN3K07Z#5u{kji@ZNDPx3(X<@$A zw++m(i@U{&`O_i5MR4d1e0j#-vCBiQRf9EX_I<9YMY6W4)0TGM%I>j^y^5VsqFW%} zNaHn`p3c<;v6n23^66BXuoA+=E;J+X{s)x%%@t$t?gM{s!kbvvJjKkvL#My=*B2%8 z1#oGxJ6A>h+x^6v!W#Hc%bZ80ko;cK)ftw?&W*G9G<{fr59#DQ~I zv;~UA`M1?&a`)4R*fIY!vbX`I)_DH+WalR~_dE9X8#4bQd1?vY?;>rno6!AqIsvO5 zr04zp`(fC0E&cw`cYea@iTult-D8h+iLrml5n0a8MjE@X+-;0=)nbqH*!HO#R+ag@ zNLKp>J?M2*F3$0HLZn@gQHnp2lqDK(RZ$Jkm#1&WihYX@o6LSM zU26%+r@{M8x{b?P{nDMkxXvjmg?Z|wR36T-_i9b2;qA~nw@9V7j}EhQ^v3;(8-w|z zEsSv~EQZI}1yZj9l_j^dja zlL!4e|2^HGAIf6&5>sF9b&brx%`%n~MIlE;d+TI3I-~Cuq~X~VXZBgKJdbDf1RwA# zr$Am{U;5uOtI97nRBLZ4D(WsOeO(>z*71`%Sahvs4zNlC4IR9o459U7*ryM^j z@^4FNLs+9v;M)EA+hSj>SIK3Tvk>kO2W?dE>!yC)P+hpK4APpsDWdHnqP+)Gab7cf zz`l?_HNTVSVl~e_gE#OqjcZLu_FIkig_yIk6Ad5J5x+UJi@NpZtntQpt#v`Iv)<7Q ze4U(gEqx3dbk5J0`934@5=(UwKjAhyRnZEH34GU;eEW*fq5#Uwp!L5%g^?_M3)1$d zxTh?NcFRwyTaQr*$WvP#me*Ov^d7Z`!FlD3zMr1xQlirh`J<8#!o){$zrCvPs*LBX zGksUJeM_RL4$~|2_|CFI<-e-yFJ;ZkTB#GbxAl_0WOf&yHPUB01^qJ<;~@D1&?8n$ z4k6=*cy)W|>I8i~wIM|U^BpS=n@3s1>u}%18x z;%$`}2KC?OQPyX>=CCBa^zEzbIxEf#K&_>t9XFD|N{>RE{Ml@7@u zb>i)wLLwe?=dvRBCQgw%;2CAe^fGZlhFu{_J0Ge}a*n*tvw4$q6S-gI{FpN|_iEL` zy}9*r|H)~ZThGe3a!{<4dT3WACCT*X|hIbZk zW$MSNt=`*Fp5i_6exby7{FP$-rzcdrFHL?XhhH(TOYVueU*wF;S(Ni)ZZUO`=X1Z! zDQ5*oO{*QwR%tn)^X8M}YTMMvtH&siQJ5u&QrZH(K(|{K_5LdW0I%f{Sa8KN-Oezp6_!%KjJZZwI$;d zwd!*+ds~mVFXIlG(58vcbdofZn|e!TYDE6rytDGo&3!$mO-^eY_`H#OWnSUDS5*0K z$-PMJr-*9Xa{1wYkhrd$83xElzRT8>VT-n8_M`U=^f3J(hg~Q8j_iL(zU$d&s%P*p@(zY=iHamH>Zu&V2|m+XrH?$XKL>3yqe}>R`35{W#S!jU~AP;x8%RY5>AC#&G@YE%O-S6DHk)zk^QXbFec5+!I z%R7>#z0z8wYI!YkkLJwC8IW^4=Rdjk=dENP=2>(4KwiWAJ5_b|C$D3(Z{>a26*O_0 zY}b0LL#nZ)k7iA!VPDa;r>tfft*fa@_8#@$?xgw&@;xatCwBd|_NfEEr!=kUXmvz? zD?OUC>iuN+XD9FF+2-*-H`+Jj9G*{0EAr#KvL1RZ-c)rwAu)vS@TvSz-GWJ0KPB?J z;5QNofmndO4-WfTLmjJwmPHt0Kch! z;?B$_y2P@w7t8OqlaV}DQ?Es8p5VkW6iJBt8i-OzQp@7e1+wtI?G2DPQ=bW(X&^gV3@pV%lx$} zALZ4ymsuk<7xIpAXTDWL{pb`h0m>+TLq=vWS?!uinPHQDIA>Z|2SVvGTJ{ zM%ymgBOD%9NcEyKBp#WyEbAz*;cC4&MH8hmrtnc?ACWDxx1Dt_+^>`FD|N*Rsy^$< z^1J-y0)548#gb2EixtY& z<@CD#r^~a8XYaRP!4J-GzEW1vSx!9aEBIAQ_^I<`%%93%l7BrP`d8JSi;d?6Hfg5& zzsuGg)o~anMYp22+r;8!#brh9$+SxrdY=;lezh|C3#*EX>&)9=t#M19*{ZzZ)}pP- zYlIf-RmQ%|A1LcSHQ7zvohj1otk>&9Yo8ZqHkB_QqvxV$cEv)s6slY3K=wD;qp}xU zZT6;mXxpr&*7h0}fv?+VL70=TlNYZNUsBWFFW1&uB zBHYc9F>B4{O=mr7(WSdZ2VX&>FICq**Bv@ve)Al$*OTP?IT2S6K2~44)DDMg8}V6W z;|CpGXX`{-0dbb|#TUZL!FI&BMCayZYTHli$@x`9W(x@^$bUhW`ZHP3n?!?aGHy#0 z&3qv!yQvlxSL-$Z0%)NtFYCmvAC*s4l& zOx0znC}=31zD{MPjfi8RPY$S>XPeoZczBOkv<&ZWk^a42VxB(axsC49tn9n3-kV_D zll2yT>+e@}N1m8HRHX5R{Qu#^3_fro@uIkVhG?vl+-n7$Vyi`C-x>F4)cZK2g-=j(E+LxBOUcMWb@NSXNlBwUi*>z|)D1V{W~jWwK0+VZv;I1< z##zR!yTyJW`<$d+->AD~4ABvO{tX9n>~{N9%@o zeq5boiZOR4*|%7seYGx~Gem1O=~zU!SBQa6C--&qeKz62ovxlG8Dm^U^=6dRBUB#V z#~Qv&6hFz|wcVu+{2VIY%uCjnn_k32?kM9~LeJ&*tl<;RK8v;cV?}VM^D%B@z1wED z$gY=tHY!iE=Rk9{@c~x3?YI8zUOkpma#6)l*43@OFB{G4gOsbJ^qh4vuKR2wJlceM~ySb>6(AqsplJ< zc!XU!*$Vw3YFIark8QH(uZch7^w}i;eg*GnlzP~sGGDblsgitHMRl`S!@12YR(Wk! z8C(GGet;nFo5^6a`q=OEs>e4)4j)3CAMKbhnO09G8|$rTITupi3FRa6yjRU9PK28x ze_vQ;thMU$B-PE6R5EW=+ke0=0v(*_)mq--Dw&CLR$Oe--~OIa-$57uk!2g6sVPcC)|3sMXQX4zb}>GjryF&vlZ%9l5KoVUUrp8u#_BMOM7d)rZ0H0ogUWf zBi*16c9DP0Rf+ATM%al&RdHQm-71Uiay3}g9A`)TM>g&X(ex#$++XYxT)^5!mU5=- z%3QUzQRMPHdi!6`Zzq@4kTji%8yR}kBL}!qwda?VZ2f7p>&f#1vs%lpW*hZ2ELFPq zV>)_giqspZGrdT+g6g|@BIU%KH;F9%Bj&q`hg8Myb97Cg&kv51JywvcIG16z?B4)+ z^Fb;@Pl*27^6CD=8*NMaZjxQ8B}-gQt)--WVRqrcFy6VyO3{HOU>U%BIHsjA@zsoKS3{?%2j;&oDYCmn6Y7c8yT z)j)JoivN8?R`*R=>3i9=M~&qJ8g?(*S0O*o(}FtkocGIwEEP)~OV%pLQNO=Z?y7;A z#k!8Jyxl1zBwo&#tyN-Z(Ah8qrly`8*W9kJo&%dTs2%dX}}Ymt;I9OEg?9axi~>i}9AT z@@cO+>jqK7o$B&S#AubE(P1)CfhJ#?@?N4!dxKo#$8vbzsH3bR9jlGKrrghmDjQ#u z%h582-AGET&|l+qx~z3?ne9!aqCkwAp_gU{jQ&oKWepYACgyh&-54#?@uXVB1U0L2 zD4szk&Q)0&D<4@xhxc@=;V$ybttkGDoXy{|)vt=H@8|pF!j(12_X^hWYr7Y0CgFRM zUy25MC;u|y&yqLr&%cyyd>of{rv#Bh0lcj@fkC_V?R$H;EpX@J2RABknYiE+4d$<1cot{E8iGMsLm)U$wQmv!6b@&OjVF?6kn9c7?*5EeowJK)Hr*k?kbp*%(D8r4v(iYy5`7G z)RAL9uA+2@v9!kZYZD`Id9D>mwiEojTn4v{`r|W3mV;)`iMs}-8nVfNZ zVlFK^DUodkJMjA#mgojrc>uZ(&R7CTo>4)1$BM%OJGgEyh}}KDvqS6waqYPx!i(&& zdLLQ&QAYP^PrVvyf5@ANdT}rEycCXd7)@h)B#VO9^>^|BTH&)uY z^b+-|qXj)=S2xRrJ&x8<_d04!(Xm%ZR-&6%cH#xu_cG%B7j-@KqHQ&FIK^pLud;tBbz3sk+zkjwY|dn|3c#}(gL>3)?R zLu=CfI1#alF+B%|rg>^lG+_-pcR z+LX;tE5@eX>U$mC=~~G9Gl|%$HhUp1?HBcRLz6g1tPI;F`B%cjxI zjA#N+VU3j$tuwpmJDaLT)P$dNr~b1CRg2Eo$^A~M%jhAQo^|NG7YC=Q8^o%?(kS$s zsIGnT(PVdi%d>pNvZ9I#DG&Zs9&b%P`wmfNH8D_Oy~IC@;l}4*!%ttNYxq=hSdu?? z0jvBQ%N6^QudttWoS-+6y{aufdRsm1Q+1;!>gB7X&vJvk2Uq5wtMjl9fA?LSzFd#$ zk>sEJ+2&&I0cQ3fnpGB8p8-ARr>@CQUcrxek2g_~zAxq7lx3M8fneX0<~P(jE{5qx zNaGQ*cd1Nimd@b5Dl`3@`n^KEqfk~cyRB?hEnMQ=XYBhiN|oc(M6MOcJ=vz`Jnc&n z*MCH9>tsGU$coHRr5q`@(9L>+W%*@9OK~#yYMq{YlDCn%eAT3HjkyP{{{vV0%e_@9 z_)~tSmAN(Y|1xILs37*sXs6TiO?`8p(#H{Md&AX?tBV-QK%c^-^Cx|vb5y*3&%96W z_{8j0*26yO=PA2|6&7Q>NfwsN&6Oh69YsWmg2rN;ilX9Us(8ky;Rr5Qc)(>>}Nj2w{aPxc~!bCCEjW~6g2%|c$ zxkf=pxq#jIHzhAI!%FN`oLY4QA7G5kadEl#Co|hvm9R1EgY0eAyG<0`FU+duuWPbL zTjg_>U8tHm^Rl_04J_Od5$;ImJpQGMxnFPd-?=4J1?S`~)fxPRZs(8l&Oq}9>esn4 z>+N7teQ{U~Jlbzdo0}6W>}*61*_T`SKqu(Rcw63Ol89-HUheI(ZA`h2_G*?^2Y6E|m;7qwj_b{n5nRlIz$H3x&_Hmk`K-YeE0Yjy0IS+80Z zU74r5ZXv*qu6JIP5CzVZJoZsDQt!=rt~do=y2hk228jb~Xabbnr9J$q=)cs_JkZ9BI#=onlqu@%$`N{64wN=yx?AP=V$XeGn~!Xg@s(t z(zfH_JtTH`oj?3v`zznd618*g_wl?g*1$~2J6TP-fZi-1jc3awmPhs31zmVA$7G0Z z(p~!i%&TSHSX*_ne^mMg>4`6y-9769D0p_(cB=<}w6gxB%wFnNm3V9K!2N0kW7v@0 z?7}trA1BB(3=>y|ceP2p|C_p3RXDJd_qdk#GLm0VoW)G=fL8Ku()*1JWx?w~(qwY2 zyyFXamucP5DAkwORsEllX>VbTZaLObAv&d_l?xKCEGcXq7 z{OvNdcZ%ftvTqf5B4_fA2D2k?^EG<0pwX=tp3sFbdV##m`?5lnSkI;?{-G-ZOR3kp6Cd=`DUbu>f7o`xcm9n*X&`Z@@+N_O>st{UVmIL`9*% zMVU`!j*@*jnmONT(nT^S=vH~#4%~0>0?H<~snd@WD-9Jjoi5AYU#$P2aSfoE6Is31 z@H@Jbo)t&$WW7I;eO7Q=W13 zZudgDc4l@9Z|ffR_hmg;r<1&7DnoP$t=*@lkt;t?m{&j5sBe^^Y;48AM1Auc#E41$ z*#l_*hNpeU2iOlY8`J*Zc=L7Tk{)!XY+3d)OXqoYbLq?fd(Z1H`wmR9mSP#L{nSrn zK`PMWGv)63lcqCx_?fyi;-r+rBAv#1((aNgXu$uCuE~MA55GaJ&GrF`lUSCcU7Y70 zaqD`>zS+n!d8zgEU1k-G=AGRvv)7GAjfY=3Jll)RyoWh==JT|Xm+Iwy)A;9cN>uFS zQiqK$q!(zpDrH?(`ZHGdGE#M=T3-WQ0hPs6#dwfcSkdsZ^%xUKQVYIB^kr1y$;O$4 zSHZ+5_~J8D@osHM_^jMfYhL~LXjOzQze1$Hn-_i)DVa_eyTPmkKWn0y{Vi(vMD=qs z%70`v(I0xZZ;>0>4y`JQ2w#x@cuh9-Cf?^4xKWZuJccfBno|dVeeV0Grf~Ul8gY)S zUx7k#&dFq*hR=zlo<+l0nN>>e;zIqW`Di%>|5iiV??v$S`8YLTTs6{u6^W}L z7jgtIPKH*c3fAif`&{L=CR;d0&S@129JKR8c@g7ds2HcazAfVIDq zgIISGx{o%S;dEuZ%5=W?KF$PRC=zSRS3dw#kEy8sSETj>6zhU-<>>lI+3=kzXm>}x z!i?h#%3sjpVY9r7kNh3IsOuS1jeIa)H%?V8@0^uhGFtu6rz(GPByayHFDsv}EF?1@ zLV=a|R+DC5qHpCP8Jm^3dpA6ND3#^9h~AWd0LSTDB@ygJatR~kAe$L)8Q1q&?B69^{wcYfIhdcfj5MlC!aN9(g#;t zL#@$Jr8caLa~39&mBwcB9t7T{UvHzad}oA{Q}--FL(UOP&Xx^&fc3N340;tLo06X9 zJfP_$rJFwc3aAz9CFc6}5z%|ElvmZlw-4g;WUnW~b ze#NV=XyY9-Y(=xA6#86C5Ar^=zgtwgH6`s;QTA~oE{&&^ zJ#&mm>pi;Op7ehLfmh2Q#(8Voaq1x&UQ)bu6(o8H-4E*dA7w;ek$`h>xR-Z3;>K2Y zyUB=#^Ep11-H9_120+DLbZ?z1_dq>pMXf$sj_&uH$!gW7p`M(8N)!3Wg~|LBe&R_^ zM7r5C8~W~d@UkW(=pZBWEdAa|p047_p2QlLW%It`HJwe)&UdcMTcX=osTQZ2KIFPV zBsf+A#TgYFnu`)EJ|9Ql+5`50<-vVQVR`NmzIG&|*MUdXaN$%}r2&R)s(9Hlwq z(e^Vm8Ka|bzifU9`Z&R8`a#+!Q?!4>=ij3FI2!Odj&xzcAED9FBR!pDtYH~tIgRB4 zb}aS~YKxPl=}>hu@5)mXVi)eRCAqya_6A=S3&ovp@YitW)MS+WQ$TJ}i2T9nd?%oqfjL ziM76%HMj^5UV$Y`p#3J}?h8|%z|FTHZsZOBfn@d3k z)sNTcUKP5&8iEfZU;W5yDS6TKDdo@MNEy4S%%H79A^cYPzD{IviY(Ox5_UJv{e%Cp z?_){mFpRDrg2Orf-`kzbp#2PUSeTNaS}E)}A8!0kF1pi=j;PrOb>4J;ahGdn8TmmP zUtP!Uy|DgrXrGVU@AAJI$n>1Ym+Xq4Ic68KRL-ZHd_D(Pe)9b_wDUxxIv-z7k-Pf> zMY`e4INEWV+1$&PJg+Ko70+i5DTz~czcKpq=roXqEX1L&-krlSbriEkF!+I^K%9r+sxj@>2SNqc$@~eTfa&%+2Qa|V%NM{{QL6cdOvv|K_6cA z%s9t4R$?tj?Q2=JCs@`v?Kw_H8-q3v!;#Rj=UqQWe&#PvSmoZE>DgRz(Nos;VLnYW zKM$eccfOaal3Jb3e}MIVmOkIa>eOYA>Z;FO?(fTJSz|UmPPTpuT90LSR(Zu~_wS-h zCmehj-LKMZ9G2iNG#>}mon$eRB!I$J)9?g+Dpqu^Nt@Pm#_J2A}&tz z6Q@DPX>p&C%P!_|Kl{@P;@oA_PvG|;pS)=fP4vl>=hwt~z&d900ty`vk2Ql)PeI?0 zRhiy~Os&kKnpX|fYA*BLhrJmo7yk{-`GU?oNPUhp~e6l!!h72VcUz*blGnz`LUQ{b9!}Hp~ zV_d~s&aje!!!MF4tg=6Y5B8oK@@$sqM#%UR{HkPJ{f)mHO}UAOP=jVYrHVF}*ItST zTryDr4;FgoO(^!E*^H3ciimup%yjJB82gwm_WJCoIGr; zz!B@hm&h09<~I^;Wx~au=}p+jWvIH5-!$0M7l=7Zi@u*y9sW@-^DJ4`d!X0%#?gWN zrB4r-sJ}MHEY{)3&$u-l?T7noy%_USnYFHb>Zim2XC(HidJcma?MUmTwDi7I4&+(d z8mH0QRRxB;&bMg}!RqmbuSg+TMQa3?=#;(OPCnDb#uy!9Frb$=bpuOlCK z5)Y;dDcGX=ccFa2AxQfRiro6EzWE)E{e67?XP6Ubk_{8u^OJQH0e(wD-i)nTu* z27SnWoM7FTFVKW%{IyuUnp}4=dG2cZK$>_plYKZxj&ymdSL{PMtFL852U#g!O$`4$ zdHod9Mz?h(dk-JPlkRl)F27gXL8Ge<*&EfsH+XUv2=ybxf0)kgvzp>P99XA*c#;?_ zGm%IBR;4s1AsW6;mH%cjS~Ytf?S=C1ldewi@*@(tgl(EEf*5RmPvFrBPBWU6lCK}x zj9*x|e$eUxyh#+ykbihok4r0gpHIGfC}XvB5$AA4kKUCYvotSyP%h#HVv<+^BZ zKBJiE{?x=CGgw0ZzlUazk=sYvsWqf}DOrAwEpI_r8sW>;UbjM-!D7@YzCDq|eg{S0 z6s_Iqidd)B&{DsNg1z)KB-iAB9^&Vf6Siz3AcP^3RD3;i5e#80OAMlgI0EQ7a5EzS+=mi*RW1EvzG%% z;kq{nFYmn7vSUUhl4<4Uqsl~g`LXSSpD z-K^s=8dhKB?5|`a9>oK&as|{}%HJy~bC3fap2UH$057I&(kl?JC+&KM-qwdYjrb)W zz{qb=a3JZNOf&bP;I526S?b@=Y9gvUW`5P6$r09TG<$gkJt(E0{tT6<&GLX_W!R>9 z{UkcQU(aeWeR)|r-_F$kmZ^8|xY_N7m!IkCts?R+D`Kdji|R%>jI&5*A70C1HgBIQ z>=2z2S&3cf9DADg;>nDWUo9?5t1Sn1z&h-KkmPbyxL3byhFE(cu6&D!FS1D=k<1~a zHC6-P$Q!znOtitJ(9FKDghQUd7(Vd7(AVolJ5>A_;R?vg}?i>tWQI}K}{NX zvW&rxs)ZeNP(MrOI_j^yL9Oa0nYHs(f0jVWH{gFa``>lYsXAF^|8>Z6rq1w&Mp!9@ z;BTOHoRNJa3TJq?9{oBGZY(t7#%#?i?0jQh+I}a&PH;sH2>&0u;@>YDtoCsC?53cTxzDUj(Gh_sX>EXkMyyJF>T7ar8P5M~L#tp}@C zLkzoD_U#X^U)5Jek<{n)&D{%W+N$9;W8=!xz;jeZFPG14z&bRx>*8Q*TV|`cSF?}9 zy(%=*QaWCld>(V}#eC#J&TnmIm-;Da9_xjh>w6dqr5lM?Cdkn|h7Tj@Pn;lfm7O3e zCzjEqo+Lf0;2~`-z1l*Xhj^5&&F(=s^_EXQg0T<#>@NSB&T@utG784d@ULMcxt2I> z0*!cspWlq7o|6KABp7O&eIudICfv$bU#Xp(LBDskHEZA~rxJ?bAP+~R8SIYRA*VB)$T#{aO zH>TsPNHcPNlYJyg^8(bkaAgC%8iAWHu>0-FY5L5mCg^k*Ol|Myar_NQUFNAvVd`k| z{UvO2@*STJ(I4dq3nZeSGI`s2Y{j(J62AZ{A8x z*6D=A67%bWx3Sj0JPjNPWA3337vW$5pK+h8c2POk`SPtF;Mq@lvFE6lyaiVu*Wdh> z_rH-Z`w0H@OLdA4Bh^h+75kXw>FmQBtW-o&C&|uCFo&Py4K9cHBS=*vdbynsZtqXA z!WsPSRzAI#4((>c^JTi5=~J!ER(wH=?lPMv;mdvSr7G{eu->wZ#0oFsXn#NNd*=Hn z(AzwJfMuu4LSLGwVwU^e@lD(=DYN_@-qn+R*{yG5pIlj0asGueA2G`;zVb2e&ycAc zhFiT!=M%E{@9Gnc?tt4^`YX)(xXzuz#&I{v2~YoW^O%zIyFV5QT}DrukjYrh-HDW) zDuekH9==U`&!kt~&3!)U&9jarPFMJv9h?ZA=92Rd>02u!y&DfZ(#KA0(8p#o1DY%( z^G6_5VO6lgdVm5+r;7}p5D8pPPR^EF$z>;g;z>Ns3KlN-jkozaxqRMF=-el2LeVuk zIr$4N&Xvtvu0m8yG}Z!L{z!SwV@Z84cr{CmcLOc1$ZNezu4I~Mw?c~Uak6}-EL}el zT}_>a4P;to%X2?v4##+@HTb~=Ivqd3na5brt5G7$D|TzR4+q0%9&YAqy_V9*b$A^! z%HsEwQ+vMBjyjjHp-1T98|a&qKg? z1MnN{eM`0V_IUTP{Nf7Nm*;DJVU8um7PyTiB8v0BpM+7=Nl@p0GBR}Kib?3*nbFb&cgTJxvN8}5R%0M22 zA+vZALwWFv@T92rb;$p+82v*SvY6y< z5_>H7T5WYg;Z$^cobM8`U-VlwhiZl73-;27Wybbjb%Cm8RG>DyisX$UiJ!uaIOpnE z@;Uap1cR|SSn#2`2xE1UYmvLtkT zNJXuUHQBHGdn|h&-C7xtF7}zP#Xfd%$q-eDs9KV_Z| ziBZpkcNajG$Tr=_eWU*|t;*v%rE2K=^LY1l)!(F0b+?ld#W<92>Y z)Zymwy_Wm^6R+iIdRv$8P!uvA;p6R0S(|zEd@y-w$M|-mB53=wz(d`f&n<8s6K9#$Q9c!W!zc)E*<#ju~xmkv>GmQV%b(0|7BdlH) z?`03|OR#>|n%5AYEt5IjN@xEsBV!OeXA zN}|2eq~<)9zM&a*GxAaJF(T21{v`yBPhmKEIyc)HJ7)`4({!Pt=BDC!&UIe`{C0hjU->N?v9iyO`M>EKe!8 zxD9H>?>Mn*6-mjHGvA?-7@7AWDPJ&lTKmGkzJq9=u|jcv%{24=2wxvzc^@EU?frg; zt^Wx2$8IS{z5bTpJmU8ueaH#^|7JS)r94#UR6oOgIQWLE_sUk@Ov1Xd$T#6xRH)<3 zsL1X7Nf)ab|AV;H#%wQ8TRVxpj2$U9r|4RU2T&Pzn)q(3R6e@{#J`kuM@)2?Z^X)w z{V2S^{brEI&Bl~;-`Mx*M7lT!AMSw$ab|E^bRS@DM^z-#J9Sl7WqA&=By^2#lykj| zPX3@$P(Ag;t7%GQRlDuD`?on3q1`$B$mQh!Ycz}9i(bLc56H%TS@K(<+jD+yCs8$I zV%EEIHohOUTH{hP2>zb%R0D5o(c-EQr4)LF zro`RmkeTJiQYxjpQS;c9^27Slo0ja=leo8r#+*z}P9RN3aA!5T41p6blga0C=K*wh z07tvi&KV@*6ytiH_V=U>^=Q{W?2+7J)%Zx04?EiKlOv+We_?awyPk-`sG}TSXYg@B?5Z})-v(;v_m_(LT(`W#V zu2UJQ=92=H>jMX3t;usn|1Em0hT-*7UQcygS!P!MC82>7uaVr5DQg%uaj<`PXVt^g znvTAQ-G7O>j$ri$(Cd-zy*pL^>I~Z>Xa6gEdMi1os~$AO)wOw9@36yxE03A&LB81- z8ZyiGD#&!*Wd^akj?=Z_ZRgzZFthB^TGS@(pel+kU`{}P^jM~8RgT{X`s zE@v7Me^Z|8nYi`>N?i>Re&tzgT*EH5@#$c*DbBt{56$-wv5gs)g)4=5 zQdzQF%Wz~h-}Eqko^Q5yu=Mwv^BuTX(tbBzdsbIef6o}hvYv&Poms~LJkuN8W1c6x z;7QGSDz}>9$MiB!MA-~~>ywSbGC{Ah6jhA2COLZpM`E4PiS(j~5$uBu8)?PGuD>0Q z74U+-!qawS{5EJbkRIgeVz>)3zQbZa31KRVm*+#WL4IN<$(?+&lAhHPpB`Yxqh`1m zU4QY}O5cxK(suqwdH25>6-(3Y;Oql%=5i?XFYji#+t2iSB@L)({!Ltc1|8W(OMYda z2J)*Op?4qBnFT6SOU!kls}C9fF+NU6z)!H^L)iEOE!YSV4|#VY>oR~uMmN)GMi+Zr zole^;k^Ox}`Hj!NbJcRXU!2S|G?V*jU!3OF$W^&$GDM`;n>@$9Yx{Yq3EJCKeBRzw zm%xo9M&K9=n6(c*{)T&bkfRKFDf+*6|MtlWQXTshjWd_t#u=xU1=p*PgoWP!p0vGg zmTg%0M`7JhMt>N_97f65^{YG1421nVQL~EY)+0@4n8R%2>+6nv{C}d)|3s1UtlYJ@ z5mEY4b;o~jeG{7f>R!9uF&l1`qaBfJz7h6ji~a)hVm(xJ!T&@|2$%Y7$$IqV0+K9wXdhuvoz^!?4%bD>Ta zTq!LkKOMEh;tq#Q6G%-4UArvh%`_o*Cz8uQ(dP?%?C&)JpUS7~M5WYcJIK%;6kkAI z*WlJ+&zOgz-FRDVV8QFYxd7d2kmE~W!D&3)Wu6qf1;ja;FF}z}sCE>^_L|im^0eO= zlKB1|R0z92OMG?(3PoJ`8vmggF3f;R7x1=ErlD(Ti#n56Af4>=oMPx8zn?p9o9 z)>}N5J6XC{J#!7-WRl8jVA#zpYtmSw@^>#A5q**~@qHe;g&yrOhQv;nV(} z8!>4g*Zk=EIQR7w(tet|#5sXkv~35;{epkfR@8K#k$!B}2Vm|wM%a+Vl_aIHXW2Vw z(ubz4^=a&^c$#k);)%4BC+$F!u7uyeL9S0p)`!ORp&9%}k4w_OIBDrUI?;&iFQYF5 zNX`d-kMgYOJv$D|q9PvJ7XHy3uLa)UWA2;XWu~7$%yuEV|>Cvk80rz0b2s8fR4)R7~!Ejv0JP);_|+Z_Ib5Ym%<0z@FUzE9;ZK4A(6; z(%~rbtQaPG=i1@R*XC1^U3o|TW1yVjy|n&_JATR=Yez;qidbJa(&OTs@+9~$xnE`+ zd1kcS`ydPWV84|f6r47^2dXy*s^*=2vQ?#B5%kUZad*~V}_`cb2=YNHW;g(mIDUGzhZGnZ4zQ5(MUvwYr%py4^J z{5p4u{Y_uR*|F|5-)lW7IGc1fU;|I15#b^IZieGX!6&5ea}-*EQYVQBFCc;E)BSz! zv<8mMOv%R-6pJ%ai?OWb(f&G8`jS_4KW1AgJUo?8?E))W(2WmJrYs(PB&r`N2lX5c zj$Qq>cxvd`TkvL@`{lZ-mie7VyG}`I*g^c+Zmye1ZnjwRAWba_lg>4=a%A#!giSE1|_fKUnnwecIHuQQZd;&>W=KDXU zH2W=_7)@KEtGKB=@jYk~x&0dhaC5p6bNP0yhLd&yDkoG==fesJw1TC^F>V+>6%6df4b#&dWBxgIwcM2{{a)6V7;`~5FN#R_QktVnE}TE;-R;n+$1Rvf;BWK_WAoB3e9 zReAdJJE9}1jk}%(7dOML=nQR1gFo=izUFfejV(rN0?}sSXMeQ+9G0v%&R8}15=z(O z^^{ZhK1F9{S@oJ`uGwhT&w2hde$AuqUz#yl@kN)#VHwf6)~|f8?`pnE#YJd1!mQ3CQ%iM%JZCr7 z{_3#5lF0GAw%=7OPEBsIYORpH2KOX;((@11!LGAw($lG``*G-Z7n}Aq3)MIkH@;y6 zNA%FuCSfPT^=zEafEmBjuyJ@@!r1$fi+q)cs_KQMbTthmR|kx{4XPF^I6)1kHLqbQ zIX;c1tYg_aqH&IDKPbdgl#&5fsfx4?$iq z50$e@tCuRq-qap4lVtsG!Q0F5e2L!oYR)ye)>yWS$}{ZUSvS>vTmc0-(wsfkv2?+W zZ*i?R>}pOvPNgq@L4vVlcod{xm-5Hz(DkcmdBHb?_aroqOtwx zu5Xdx8;z)v^%P~j&M@|>p4f)Z^#Mx#k?N7%o$+E~SK@SgE>6$^u#s=|H`};UO@5QT z)Bec6Jo#I)Gn)2MzdOeqT6xt?r<+G?%U+AzPdwc9c3L759z9@n%E{^f4coX&nZ}7#p3{VlFcd$U1>vM64?%g z+o8rLdLL^FdO_#dDgHT9u}l<@hs&E}&ToWLt;GOibSaKd>Dw+VUms)yk2 z211jee%_*imC<)S30MumiqqnI=xo#sULxz=;7OeBbvtA~orHyyTm(OFMElBgB!NcZ z5e?z{y$JE%fRH26{SRZSKrSyL4O#kT7r?tgQ2jo&^9$ue?&7&kg5(Ez&Lwd8j8we# zyQ@CqpWPzH*`!`FM}=q$N&GXpL5JeiKAA?|t3s5w)MSq8W_i%+uQ65@eWq`taAJVb zuQ8GaD!g%uYgheEC3%RI_t&)}@6WJdHqDHXXGa}R4 zoh(%<*r8rJ4qq?u&RGyG3(r5)y*9-v@@o^g7iPC3%#Lp=NBDw8}o1U%r3l(HoTr+ zRj{(u=f=XKvE+7!n)I8xDt0BWX2ZAX9P4C0b5w{|dTs|4|6R=SDB0}+LHCOzdqck? zD(dx9JIq$q0W%E$=8>XGMpOv8UTL;H#4^QrezP*llE-$s6l&0d?y%)A<9`|sXQ(RA zQ~_$p`uw9CqZ5q}>)4Zjak`y8y5ms^$hS*n;VikYZYy5pW#cdAnGx_z-W<^P?3zV*7f$q((AG9%-f#7Mi0p6pb{ z@AWVAkg>YbJ~D6UuV|}UKQDP6YMex77eSeli5<{-Eqr=OH{u?>P?uRXSsd=&q#yEE zD~BIgyve3tI@*PHQ_vjl&zyJzjhXf%BZYP_R=t+xKd!S2-;H`ipeb#m?u9l4FH zg1TOZWSxQ$I!%u0XG`WcQ>A;iE%{Z96 zo{XJ?kHzgCbu@9S9T;xU%(ZL9DiSt9-FA!0buGPjhj3tQ@;)5;%C$GLYCYJ%J5X*I zD?CqhR$f=zEPAvjSp$Zya@XtO&Mhp@nKbBQ_llj+U)9g>1)K5+kLz`P9=)7V^|#Kw zDQx5#;~cJ6bPyh0jcWJf<*H-@cvJ&wN7un3X!#o7Dq`zxH2qU?UgXNAL88dUg}@7>18ihS5{j;s8({0(LU|EO?>8N;@epD*BBn`;92!hbGeS! zaHkkxyx6IvCx;gt^^{^}FqN-2$c(4a%fDHi$hrz51`yGJ;q1$h^@`9&&PYk-V{sim$2AQ z$;~p$+r4@}?%iI{$6g{&*^l;0JA|HZ?eMCC0rpp#V)v4Z?aTIlLN|BD{dWC0J2@qF zy4x1}lNL39CnDNq=S|4oiBxvCbHj}+aTlFjEp(EUt(xeyjqJY4id@fkTcPv677Q)K$9sl{`mOxt@A{=XLyz&Ws2vaGnBL20$j3r9 z{73vMfMHia(1kQ1PTtCa@t?BX;WL&*jeU4gPp+o}%s#+IEJ^jZ+=463p?^c#&`*Ev z^-fM2OLI0r_`f~z7P`>I9WNsXWqBk+)sr_tqM3A|mUz1d$*9UMU+=y>jo>>m)nWP< zT7MlB`Xd$hwBV~%fJbqb*n#8~uxPJ!Xm6!z8VB`v{tV+z;+@uK4F~9bxJw81_~a=N z1n>0u{0D9?w}xOKuW7uff3RA^U?Z!lOX3@Qc`UJ)(AnrZ&ED6a>-}2Ge_Cv0^Yuqw zZqLL=Sn75t){8A~?z+#ck#3&Y#KMl?$-ZJ`!!iBXEped^-{LBgTgmC)Uwb_Qom-;+ z$&jIquDt6}<3}TVNj!2do4J;C+<^-XGiLJM*F)~xUDMINq$i?BQTA=K6##9>YbCh$ zv}k((e{W05yZ_bt+)G%xdAy7l$ZQTi{UhS&DuS9tk{gLhN<)yAJk8ax<7uBA*GFAe z_r(mJRYm^V+jO|54yU8EqYM=Jn$2v3OG`=HCcR;wh_KEUy+6ZVud+YrP!YvRGNnWK z#plTayv3#raIcHp>oJnCh28mHx7U2#YS%!Ny{twj*6Bs^+LS-CouAbmx6jhsaW;?n z9sYB5p59a%a4L_a93N>fgnL4i6KhUy#f97G_$x4C9-4*6F|fYb)KiS6H{n`|f}K_{uhKVDOcXP~-EI=2%+#N`J>z#3^DeAG zRPf%1R2Ldwo_>?_QSSvl$4s{9FdyYy{?fIsa{{-K&-1M15bYRFMLywj*w=zoG;vjB z5z!u>&L$C2DOlnTTcJx9-?aj?DDNAk@G~Fgt(9q+?7F!yK5~w+4^^yp`%~n)#m^d% z&phw#6jv4|>80@`y5eg3Ia4gNmfk##Rt;74ZWb3kCyy~!q#S zyg(Nt60D9Jud_LdS?-T4{^YsmIRRkM>P#+e}RvDeGZ|036R1HVNbFUYm@nyX5LLjs4ZQN zwQ|ErSSBCj0oO#`r?Y6fPRdjH+4y3g_{N@mKaPJQHxs%L`L$x67ANtHqah#gtb5Y! zp0Z~{Q%~NDD}~H5);jOUz4_=k(#ZOw=|`S8h1?gwrCW@xk9iJxru zmPE8Q)6eKpbZW#tkzcS^vB%}(=6y0tGsY92PDyK5939Dj%alL4lI%AzqqF1<7C?_U z^|-AlFivnDOLt?R^4hMsiLZAVZfr5Se&~B2ylD;rI`URVx~?$UzZTW*Lc^#9?~+mY zOdQb>HQKxD2s%?!?9-nWS`0U5!KRl)-Z>=eS<+k?2A5-hA25cHF{P z>BZpOtDaaL9##>#JSV@n7k)M86ZaK^y$SK^;r?u+bE3Q}Tj579^jnI0wfJvcA^RgF zyBwLFKpyYmi(N@eTAIyAsVY%Xb7+FD?f9S9<9}iP(Qlp;-AU02@(LbD9bh$$JA<^u zDvH>DF?`bK^IuPLCzxqusbl?RWa5vCCC`R(mC(8jd5&J4Bz)URpZ3T)N54tboDcgu zD&I@QrxV3sQKg9vh$Zy(C^RTZyH7HrsBf+GoXD>ZHrEe~J@UXyy&t_K#avY?b=^MW zoaH{lT=xm>8bbbNiS~9GeViqECTu+$MwWn6J7_@c5F2NmMvq}ppH!#AP2gE0NECVD z=uI8&iM`R{P4|py*9m0eK|a^}ys6jZr>|!%j~Ly@wB|mPZG#6dlk+7qLa~lGPBiRD zXRgA9!*nU~i9y?5?l=hTR?x%ZC{+VyS7HYX;lKj&^{)GM@_H5rqH?yEKA-2=v72di zQW>4IGhy&xi2f0ch;`0!-d$7_8$qgDajF*jN9ATQI(+S#PyG(f-%k%p<8}@6tnR60 zQ137*#L1d-J$#@_?` zj=+r!6e^m^=Epku45QlX>Fd2V!s0-dgilM7h*L;LR%%|WaCy3!Px3RxnC5$GWOH}9 z?*UKTOX4=LZ!66vdK}`6vk9*K71l(*%~nqjson1zVRM(GQtU81ik; zbB8 zDxOpQKPrOTT@f|Ze3}&WJnTE`a4B{}S?F#{-F2O-4yG`>C`l=VH_4R#2IbeK=(fal z%lx&$47a#?r&n}a$7>fF>@eo2t?o9Xm|@H~u8Vyb)*5x3qqxk7V-BlRcZs#;yS#S# zZagJEiR$}&V_IZm!fO>b2Vpqw6Kp_@jR(_OsaU{k85kKP5xcNbaxbw;BaP zW1=JPxVij;H(T6m8T!sNgXolu-A_XTLR2h^tdvid}(%643#>+kK*+D0(C|x+3Vm+-qfu3xOk15&Z{EqiZxe2G_X9AL#Hq zt_OOo#p|8!5VEz$Uz`6Y9V=6`TA8|MjAcuTlELMu^RDvEkf7iFKll@}PH{vRU?1daRufi|IsYutNViaLkTCO8&5 zOKkJKbt!&D4@Btun*ZH%+yAcG=bFvF8~PeD5?8GD?~t^Zb$q%x^`zC_kIzFcLh5&$ zar9K|O^s-aZ>&c9735--@x<6dy3!o`J9SM+SkNa;`E~w^JEbWcR0vH9J_RC0y*)mU zS;m#Y{m_=+L(D$D^)H`xc_(x*{azr;Cf^L2M;H3W)W6dm>1jU2sM9|o-!aP=eVR*Y zn#Ty@{k-6s}38Ik{wQKq>WvXUNqycc*HC>gX% zb1}FZV~pRy<&cEksox$!fbKaa9eOw#kj=MyCHqE_|ow#>s zRXicIERFY}kKx^gF2#t`R12ELH#Ylzbd7{8g$EcY9M`3_B0i5%|NFFEK0oM+cpY%p zczS5m#uN`is<)5W0}|2tq$Y z`UAb=s+ehdbVvO=J;L-o|!W3yBDMh@a4s*ncPJ5E2o0jyuIYW0XPf^k4Bw8heB1A*1n1`)P;F3rzjM6jxoe_!L#^DQzoQ5C>VZc zK6yBfi)k+-cpe><$M~6<{KKT!GJ02HRmWj>io2xsX`ky3xx?|4HyM6u{N$zNA!Iei z7h?|1kKfU09QXalbz$v~x+b3XFDK*q@e`lNb#dPqSNt9G2s*~BgJ*HS@J-^=^j{$f z!SQ%j;AEQ4@jLzw{s)DE{^_5z&l;Zv4`Zb9ihCdQw6p{U<>Gs3lnbuLlY@65UGZ0Z z9{dW9re!*f4l%ay*Fz)YuaLTsrZnec^zn?C*}t#!>d521lh*g3ZTyv%^te-+HbIdz zRlE7SkmwCQw#hZUVd;S-U;5MuaCbYl1P7^z9QfEg3s~2_%DOcAMt^0 z7Q|%nNuJ+v_dM6cb^l%)r+vqF6-rXCkeKwR`L2&=#qacA>3_vD;+l9yT%94}j}gU+ zq4YTO{1s!*O+787AT6OGEkTcvpBQ~eO3W+%idTFdUV8i$bdA6MrB|AILG^znKdpIb z`HH)RHh~J#w+G;(4x2)dCVfl8uLxl&(vs5L`>N!>IDTO$_nblw_;6H z{2jE7E8@DKTih*8>9|ACwBY~#J3fo|()>vCB)$>%Nz*-jbx zF3}kj_e;NGMM=C0rN$5xh|$Nr;(O_FrLTy0($7ebIX$n0_u{H}@BbfX+&le^^vL7s zMN%V8|NGzjr0<`V8c}qTr?U*PLhJ5GdvE$fd_S@vZb}<#C7jck$F`C#9|{<=@e%7*C96rk@eth&pTf`siPe zSI{w5S4B=F{vH2HzZbknq+~K=G58&vPwQ*QXaPwLEehR9>tkR|T9X24LzB|cZTwD4 z^S_e)|4MgoH~rtgTuSSGn&au;=}*$~6>^q-rMVKEiC6IAUuj57Sz)USCde~=Bir|# z+|g9gYpghmwTcIzYvd`Gh@{i&QfA7zk8}MP*{0})jx*|G$J^+*nkYN+nQvI)>GAg_W$13?tZW8 z*5x_3x@Cj9bz6U=q+N}+b@~n&ogM9{62ne6R2SpZCVAp7?$r(eM99Z!t&=#;XB1`r-KiF%-uDyf+w+ zL;nBjzi*5bqj3xue~YnVoS1-PikK{>i>bKH67$4d+|L&a#bU8UEX8e!SSFT>m12eX z7xyd0TCpCtb-1m@u}-WJ+r$R3Rcsd9#U`;&><|aVPH{-=6$kL@Anq;eI4?-c>7s3j_jqQWf{@tju?HMqmF%>CwXvzWEgx?o+mgf)x5;w8mQ(M$Bk+^>or zM5X0u7rKNtq3!7&dXX-sE$A@1k?yC*=vR83KE-2sEY${zdj&tiWlOYFhwk6GYHaMDIN-$v?A^CtrcVznMj6_ z79=ztTp=5&NZR4ktzEOwGZkk3Q%k^CS5;-uMW zUYd;-r1|hzoaUyPse{_6Nj&5$q_~gFCu2!Jj4YDGi!0FV<|0~9Xxc75kGJN9xWpe@ zC#|j4bZeY7&>Ccoum)JetufYkYqqt*+F@O`URVLEDDT8)@xA;RXQCjicQrKRhj5V+ z7}pF)D2~L?x^x6xNiWmSG>DaCZP+BXg?(gUQdMb`G(}o3?Uj~F!==*Fb2glXu*=(WDNet)?d!Z2C_2BciRJdZX1(h=^j#9JT|9koBS05$F$@6Tdi1N zK%lE$+01I%wNYMQy3@PSe?e1?6J{-Op6+JFsE4Omm-!MhoJLYctFl*eQQIKfGv%2y zo&BL#$O-N>-s;Vk6k6Q9EqIJlk2z(-KG0djnLFqM)?%1iM~#*iTSEiiyfwZ1y#;-m zuZ>pS+-vnP8wK8`cSvoPS}A?6H*?^jKEXOG8c{#Z$m%cyIvz%5i4$a-)YsNGc#R{* zu_x%BZL{*7)gov4OYSDY^f|dNF7ph$wYAk#=|8t;$-r1OFY}40j zE43KoU#k*X!c-;QrUwrRx$kQ3*4)0(v@mbz3ug)YFr}#NjI(A~kFdHSFV$nTuV8$f zd0MZk`2uePNOF#Lt_|L4PgY};jVu$1f_L=OH!M4yWIfXc`TO{mdaHT#v{7l<)0=yu zykoto-UMG%piAJSKiVG=h%#!5R5n(rYbz1d$x$$*hf8q}5A}v`iDVfEMz#+v<``k0 z5Hv3&JanupQ&3Izx22j>%z{=0^Q!jNcR8J>v`K!NR6oT`OZ8;-rlrLvPe_`QTq%8c zAk1n)TG6BMdz{RaqHVu|z6U1Sb2>_^`rU&Gu@o1HSn}d>6cV4 zsee+dq>;%BQ`e;D_hd|Kmb^8oMCwc5PO}o(N{`ak?7SRjOA1bO<_g*3G#nQk3!G0v zOxIku>~0vcFL;P8pFD%U5;;XLo@g%8tNE*Wnx{2OEuH!&<#uwTq_D(a2{B3Tl&G|8 z>CZiLy+wUl{67PejE(#pZ7bJT!|ZQ^Vw`hbFGKH#=gLq!vTM}&jAb*8%e*{uboA4( zM!|*UQ7lL)XnU(Pp-aqahQ~Z;oz**dN+hk155#XzXp-ETL|E#)LIVJyJ(N*BAS`1}trxZsfS4tpBH`2NWx)^!&yuLB%Lp>k-tBmY?25BRgfX2nS)zGHyJ1(ay z#aY)89lSDlzVoE(nEQ*%=g4L+B!`f0=4@@1|GTfSKZ`$~Z-!?>T8Y#$DZi5nBp!)h z5#K7YOiH2j`QFC<$ALFmE4`9FRR3ta<*Qf~^_Bfa@MovTbu_e2SoN?sp+~}UMpTKc zl`%g0e5M?knniXCc_HWJbIo?5FjLqGG019Qwda$qq`_U!vOVP^G)*i3N#N?@z?WSNL!q8FDWYVWBlUyw+Y*mX}a52Hqc-DsqNIN zXn8exnsMXUA zjr((X3#ENdew9cQr1<^uS(CP<`aE^~%L6VgF)%oA+n?g!uVuCN(yxjmXuM-^h~;|d zZs$Jfdh41Q8W;W~GAg=2=7BM)~doU-L^+f+!e{n5cFKfKi`ueAN&ZdSXZ%@b*pDX@M!qntm zX*WHyearlR1!Dcozuec(pHVNv+piX^;7+y5;NyfIBAIBVu zIhRSw;BdZ{OCyF&XLaQ=a%*WXo4{Jp9@cGN-juEJDSsNrZH+&Zm@{>SSJp0RHT;>< zyQGAsoJ@`LJP*9E5~S`yZ=4@O-aCsr<^<`sJL({Hx9wc8%e5@DSy-%lSx7=q1!V?l zj;x}Sc2Qf3Jm#_9OZ(|NnLaM%YU1hmn{lP$N#d#Gsc9b1K;JjtUf)meMsG#m2fwaU z(L)+-I}rTSIm%Ve-POIoT`V*`>|lnE8T({@5YsHCRHo$_{&AhOFOcU-5%NfRh7`s2 z(fsT#-Nq{h3Z>>xaK&kVuEmu~_?mPnP4Zp!t@4DXdXkzY_egbn&ilIQ(}h!M7nIwv zJ9v0dL3=@)q103Isp)nWQaQ9xSZ{Yh=P&yi9*JMlpBoz!XRzdgsjhrBtxk~hXT0Y3Bs&&uMJw?P#`D!W5M8-(5pCE<4? zf}^rVAIOw3vyy3Lq!jkR`8}v~&{TU*b+mMjNQBW2w3E21hkA>r$cbg*$HgVaT}#-O z9GP}L?eEl?$!C)ONv@HaJ^h5|rBBmtSnKIOawWB%ZGkPPElka%lvPsI>p@XY?)=AD z#Bni5weL{Qu~1Tp$C|&4llnTXuy!l(-oMMc1W|Bq(&D79$%9iarp`(q?iIc`|AfF? zt(Z~Nsw@i7S5iw`O^4g<4lkEsb0m#w5IHX5se6j^M(`|0gu7OFZ1}~{Db5&s5xF_p zXD-&NfUaU#BCQff+7PrJZlzkeU zY|);)qPOJ|_BFu+9ErghgWW-GZIOyYn#LALKV+h;lm^qk#cgYo@ix%X_s+B5V|ucB zi+WFcfBA&JM4**_yzi!OLExL-&blX-v+v4Vdus4d=f;qSu66DX?!KMUZLFuO?%RA%~a*FI$X3K@7I<&Yb zW-T!)>9e%$T8zF_-=cTW3+s*aS^5n<#5ioEn>Tnk9WA|9ir9TY)X~K8EqHnGzTg+Z zZ5_`YBc1I-sH?c^NJu~Dw&1GvfyyK%#2|hERf^5(Z#Fh8ZCT)wFV2%c{Yh%sR4XM{ z>Y>!rX(c_^y`Ow7{5||#{m1>Y1CO*Z#y{3+afuF){*@~$*Oe@4jGC-$R{AJYmGjCM z<*715c`pBywz91Bmbk{V^Hx1F*N3o?Uj!RLbV1huvoxAj-XOPgshDJ$%v6sr6eJYcOdf9MYanf=1s z*W2D()!WYd#kf-k{a1A1OROQN*k)LptbeSrRvnCXt(n`bZ`L*Mn;WcMygd0v+er20 z$8ts`zw(zdPf1kvtN+*r*<*s{1dR_$vv0NEu^mwRDP!a~sh$)gg-c)96n2k3COyS> zYooc*kc@u%7wxFFRePv~=ux_*J^tBvOXX z2O9B$KB0FJ5mHHhGF+tb(L5Tp$3<%qMmxbOWt}y>My!$BY-%1dGg}4ub6yBl{YBK) z(R4P=#qO}nY@f75ZmbkhhpBtii|ShSx*DObR~{;3m61wy<&S(scFV&g9~;jG&<^Ay zke@HSB!2^$l(Jr$3(Qt#oN>yyZu~Jc!!$~nE6g3{2~#tNSWeWoXShv-i^9N@4g<3o zC&r^vo+5Gx2Wt3g;uU|y*YMxgDC?Bf)v9Y%vkF>`tie{Sm5TeV))L-;f3Y_4?cz8v zqT{3v?GJQjGrh)su(m9T&6N_dX7{A3@)OA~^~3#mX^GTXN@wfYa@LY@YS8!iO(-ow zPKckt4jQ3`%n0<1h;95Br$7lV@Gbl>U&%+}79k?VPv8jsfwBCD8vP|c@;W?JJmST9 zQ@$FNdN99jmEwdCvU*{(2duAVnDx}k&qrEr%dkTDUc5J*_v3G^c6^Vx&mZu)WG(8{ z9by!jOk;u3bYkP!MtT#o_{g5nrPRw>u(Na@EzjQ32J|-_#TuY)d`7#|7C`SQ0qYKfp>Z3q}?LnW!yf z@tPOoGsJ#A$;!YlK;E*IjaT4Xtm4*cYXwiY`a{-k=+1xhsLUsdo)!=lu?on_P5e3& zFtw=vP$%-782k^}NgLw)cZiJ@2GUlGuBVxSUDcusXagWB>GTOI#UrEwJuSk?DEgUv z5#32CItKP+BOmE}GD@hV2fa?di#Svm3&|YOnV=3O8^u(dqp8rvHhL0h=4IdxeMt-P zOq@a$K3;f5PVxogKLmaH#4lQvu$n=90RP1+iIMy=?*O!N25-(!@}ImGj}dvrd|ru6 z;9a?oS0kw+6K^j*h~IW;olg2HQ-vixtE?I`>e5~g49o34&mjTCW^Lv8?567#_@~z$RMDWT_Mqdz>=LnD07gaz(CHBI>62=(23%$ zH4iI$!)i&k&`Y8xnaZ!T?cy1KCn%jpM_P5oJ9>?@v98cKtl>$q1Gs8OQ5m;|z^Z16 zA>=9S)Fw^?jXMOL8%UdrTUI`nlUTqomyo99GLI%N$aiZo*U3LX&`XKkw2FumMM)15 zLMQMyd>uaj2Y$*g$;fha0J)r5E=ob;jdyw*U zJ8|%_qPcWKjDnnclQtwEM({f%jTGVO(8bOqo8UOhbn6zG3~k)cFOoAfjS&7+^kgl? zb|B;HXleeI&!@+rbxC3kWM5cp5xeLEZt`7_)k%7VXQFPJ6`1KXv6;ZLl2)Q4RJe$o(A1JiW%dk`zlkEs@#8Fk?Bog1s>YCfCVGez z1BQQzq|lu39=DmBjNoP1HR#q@5-J)2^FCuO5LxI!GLC(Kri9W9q9uW+;$O&qF`rzb zIY?&Uzc%(SbMPRMk@X;3!Ch>jh3Qft%{DrP9uOyaHgS%QrP0uU3^bJ%VSn+4q(AeC z6+DKNhSZ(|dEY`mi5|2fR&ps_NH6fxqA?=jI(m>Q27+EinkNg*D}zLA09}e{q`TMIM!b&7{4>2~nFp690%}kV^?x zkq9f77)j@`8X{m#C*w$Uwuk2wgq9{t!Mc>Eok(|B+P}culi*MP7Olx5@|_2Zu5_w+ z2}^E9x{6ftjDCP#ohB7|1~P|6z&8bv4`d2?&&!gjEE3u`R$L?%=`0}d57<*gxfY@h z?@02I%c7}hMJc(;6X6*M738p342EkkDI{8nvgDn(LHxkuE7G>`i&aTI5dw=pZ#l_l z_Cvh3w$i<9IoZf-i=Xs6sm0SQmFA&UxGtu#bnzD6gTf0X@%vVHGM2UEH^gkyb(8eFVO@M@hcxof{+{K5sO4`nglK&oYo;1;8k|vnw`jXc9o1rG`Iq$q^GFP zk0aU+BEPH~m{lyV4Xx?Mmceop`9ksyY|U_D@eAS#tA+VB7RALfM1#{{yILYfPv*nL z5VnC-1J{s`uA>Zj%{@r|sQAV^z;`yJ8uy8Pq&HtC{=f#0;#GUP5^PZ{pDS8GAD;;! zwt(@NOs4Qmz^W6)2}I)lBp*%V#jI4Zfj*=YtO0x&Z4S+ygH?D;{w2NmLmrE<%@e=G zWHOK>f`u3ggu_jjklXwxnM8hzUZgqrw>|Kv)5S49obMoGpkI{`VPBK?;-@tVUU@so zC2j$;Zb&nTLc9P8gNN66OKTkBdrk0oFL@hLgxw|^_+_gWDNY~rtbC|=LH5CCWf8SV ze==5t@GW8r&5p4RfSjt3Bm59ogDh* zVqkRj$aS3Uy%;FY^KHB?9|oy@!e<4*of7glSg_W}s!cwKMD^=Q%!Ic*+f$$oJS{P91K zTm|^*E;wU|m?UmNa|^?+vw;7M#H`O@+>^u{#H?N7Gd>%LtEGyyq$DYbwLBy4lHSCQ zIQ$7-Vkr56QRV}~mx8E18WI=`HZ&tLp9Q2R*&rT3QZ>P0S44a}ia8_!ADo5k?j0D? zMc`~F!NziusbDo5kRixo39U(4$WQwq~8U*CZrQ) zl^r~4e?;5v7_E$(2RR%;R+A0PY%h#%3a+#ZpC=#>DTw#_ke9fXBRBu!jgh%DhJu-(Olp$Tg%!a~~mIe#0lFi7OYl5w%$ZTq2=4E0S7~=+*r3WjUf|b2P4njJaQQg#lG%I555<~?2{v@nj8T|Su zq;ne9_7fI08^><(99Hm0Py&QTG=T1}gtj+_Cw%gM{c>VO+F*tyF}FmlOc_Y0C~PYi zSp!)e2gl|nI;m8&P58W#TNoK9`X6sACq{bBC|F)u@ep(?uy z=`KPQ`w7yygfTi{ud9%+9>Hx9&c7AceE`OO3lNc9bRykKXCX2+rGw})x*XMW4D!;V zcs&s{5qez6KGSr0uUelFnE8;c6? z%{ftFWEW-eJd;R4v`U08AB1n7LF`BaS6>0rs)X4#MHj{y^aqru|IiCmV)fW$wvFup zU*C!KWoy}O_MN3OKZ}s+0%7VRRg@ChZq|=gU=Gy&YiL(mhDK2n^~rEp+zDVNIS_Y8 z@yz@KnDf&>7S;fD8E?(A!102KDQT6l+M~Z_ier`5 zqu#X9UZ`X00>PQYmN0Mv(jw`xlqO}Ad&xKCyvi7519<%IKz4(aH}Xu`CXbg?=^oq6 z#YA0z zqGoo}0wUGf2r~S7Z6nDz1O7k7>cd|nsw{%u|BK$BJ+PDd=rH*U_*qZ(mQ|5@Vnu38 zp;88^tkhMSAsv%^Qa3qS963BZxD71svvJq>WRy4mn5V3M{0Z_0B2v-6 z@)h+_LD<1ISnwp|SFdPA)(x|s$!4)0tOU!z%Axxr3pzcLrRwq)SyuW36Amar>T{)x z(o`NJ#RAK_$-kN~Wi2zi0F54mk<~GS&8Nl{KmPmLqPBDnhmTYR+^QW7sXMWmq#zlQfPcb3j_XSTQDG6(z8eErEZgBl2A1 zcdd-p2D6zN0ew1S>@#*7{~BG5vPO0zn^DNfW#q*3F~$?4ggM`QWky<+fDhIH^41gf zP!!o(B)rB`0j8RK0X7jy!x5P+qR>X@-KxQ^K-V`*Ho2#~PyQ%}D~*&+N{CWY4wEPw z2*l$Pq+AiUH=5R@L+K4jy$5YVe)7fEYSU$2Lq|+C07M`4SNdE1k$xV``A&T;koLLy zYW=FN8Xd8w70oT?XEW7|H&e`L^!dE8s_~=17-vDl+91xpLPygV@}0!umQ1qInKX*+ zW#u8SpHe}2iu_Hkt*lh0D_`Z4Qb*Q=CXw1S0oIzCEr1UziGH&Z(6Wm3oN(~H=1XG) zEG?gru3yj>>ZA2x`Y3&pzDQrDkJB6Lx%GVdcwql0jH=MRx#mLiIMDUp=r+2Co|+24 z$hx2}Vj=PZ9X(Ee(8=_Zq#{d_X?1#p)@Q%jSSd+rFJF~ol)s^0H-OxqlP62LSRRb) zZ}hV4VPn{5I*A^jjoE!RpEU*Qvj}}RcZ_?+BV)hO+F-_O{iPnOr{MUi-_}>^?e&s+ zYyG}H*Jx>eG(#;7^3u#k@M$@DJNUN)sHiQ}byMjB`Urk}G963@LbJAGeagZ^mY3d1 zQ{)hg<)@Ne?W1l|x2l!Ye9Bg-0P78Ju$~Q-MoWj-b9xx)={VrQZ`c#EnTJ?C%{FFp zGt}H*RE5l{8cmE6SkGA9qq_{Ben9V~Pt|J}qs@gDx6Xrw_-WR%!ueeuf&BL+&x_2l z1TD=jv9qizv*_ zaZ+1!KPAvw(%*6$c@g_AHdy1#Za~S0nxBmth72w5Z;UdE8y}!iZ}dd{tX>D@NGE-g zkzgiRG9O~SH-DLXtPL1XW@OB-_)77LxY;vCq^t0WrD0bU;2lb`CTth0Aia@#%kSj+ z$}^=0#*^1p$5z?)Sxr=`%RN~UT8B<#2c)~wQ8t3Mq&wLFIaIkW=_EU!V*X_gfV>VH ziTVcpr{2iuZ3G*W^!oY~{ffRvm-RMUdu^=V$^2bBB>)ILraYOg)p{Uym><8+-IST0?D}R#6|IU(*f-8lab~ zsZkgBdkZUzOhcDCb@1+wpxKx+Cay$#jFdu<4%I{=*;%Osv6K=bQ{Yr^+y!y zB)3uOsdv>2>KJv7dIqtlnGy@cW;-f@F07&SFMEwD`W;DS!{kd+0a}u`!rHwt`WT({ z5!zU-f*!!S3C*sZ2vpL1nhCDS7P#b32uQ{+bGj98I?Z_Fx%tPc!w*`o&1a?wzATFF zq9^HF+KSzUj{g8oF&MddIeG^gR-e6R?<84yq;Ra`YUPtM5VAV1%$9=b8sG$1X@vBE zm8U;NQ&NEakbX%X+C!|e63x}-S!0wwMcboI(;dcYy`|PSFfA}jE2GD2rvm@_XZoK6 zj_MiB{-$XxG!8)bOIiJ_80)qf49=whDtSg%fEn7!9znNG0C#c18Xr<8BP&4ODV2wR3@q4)iP>JnXySg_2$vPk@MuIm64tKX*nsUbRU@I2VQ_zv91~w^pW7V z&g#L&P`#cuJ#aiQQ2V2;*UkhCKM8ydGn-(k-K&S`{=naXYk@l25RGc%0#yP-1OC8Dt-W3h zfL@JYk7+~1k&PJ99(b|5thAIl^06G5fiJk6>JVmM4$06`Wiiw+1V*{nT{X}L`8l9t2h`Pb*+qp z`aP|()R0SE+cUm0RYJ-`VWF@3Q@(1~~oG6!8jw=yrPBl!eu69vB!PBpjBBe&)L~BYT zpplRo}z8xBf{Xl-m=M3eR%ngaxUeB zTvztV=agy6P-TVES8ZUMq3W_ro+?{%A$bRKo9@V^Tj5um*jILn1!z`k2f|+!eM!$v z#msC>*9YtO^g>1jbXATqo*PkEIoT|2PBjmkcTB%IAGz{MehaxW_(0?|Ur7|IhG2Mz z3alX5xaH`sO=5@O^LwxmNTZPyCGC?6$qVJ$@;o_AnXP;WtKCZNq{b_ml+|))xs!Za z%7L7@sFYpWha9Ym)Jz)7TF}YBCGv_!e5m!z9ASP%h8$uPFuEa1<}qwWxX~6F#ycY` z;=wKCHnkwNYZm1l`9&Uvp8kD6henfs(DgeC8OlvM4!P49WGLbAVV98=_*f0F$u$sT zBIIY%C8?%7S$-v-l^x0?WslNPSudBs$coEHq%i3YWHyV9MCLIb8QBVC77I~<{^E^z z2)NAk=5ON~)^3fl!We7JH6|O&kf}zSIgsZM0Gs>7{B82t z6&=&7P)`j6_K`^XQ+OuC;l=1HO+$=q4xigaY9|epdO)Xo$Rps@56BbcJ@O#1%2Dz+ zX&P#vB$ivUBlmfWIgMw>>9_xo7N9|8#UgN%8=y%Suy#Gok{HiN1Kp>_Q{?w=jY7y~ z+au4pXEwEFSxaC8t${d>Muf}muIk`77_*;V+`+VTlRg_)9Dj+JJi!@7vnKzfVJXab|KiQV946>x$<Lze17g7H% z0Gf9KSx*JgfjmK;_W`}ZBWMfeWEqhEbw$VBKDLFXqh@T!x=Jshi&vx|JZ3|T{|XD5 z!HS??;Xbn=qb@9QR+i=mi=0YbGzwV9DY6=Dwq3;Yo4g4(EgMg^82^Eu^ivqkab(c1 zt!N&Ki1f~y545^9u);Iow(9aMJR?wm052tGptpS~kfJDL)xXitoRd=e4cUxA)1Z;# zn2RNVEnLC+AQL--$EU2eRGXEQCbBndKSmM>`QAol)POYw*R8N?nB`UU)Z4))tN@x* z6IjDb$Z;OlIs}I-e)Fx!>bIi%d_KD3TZ2iz1VnZo(B{+p4o~O*@;tzGDq(EP!IBLD zYmo!E$O_bQkAN9eLY6&}76Zam7qZw(%CaP~1XW5au$(o19qH|ynPfwmA}Is>4uHsS(09ZVi0#|;r0Mp+p; zk{!0P4p`Pm?4Q_#wKxp~b|3JX`ap(;0W%jo9tg=jzERWzOI{4PWiB9bKJY2UfDDZW zhUW+KTnKe}C~(Yis4f264^t3qMrqm;XyZq$br;$h=O{*Z0q1)HracGU4^)2=GVCUJ zbfF_91@q5`&VroeDDsfmK>2O~ORP*HNCoteBw*CzkOA)%?MMOA0NwN5!R_xARm27` zPceK25WJhfv%GvBDGOfVk{ARg<(ha3f4$a3)J=kc8tBm;j>&jMfR4rb^g2&ORy3< zc{KeCvYLi*P6J{%Q^fJCK>f-96>JUmVG12Yu3$B?m=)mBvSC#V z0Nu8c9Y7^3@^#?l`tVagf^J}!(g?6fsP;rh{sNq0G+6AuK;2pcA)mqDz>iaC=oVb> zZ!lp0;o;Mu&7q0U6ecmEI9Pv;;fYqRxp}30M5Aq z);$C}!n%<|Rv7Ta2WA3LLJk%n%z~z4-8utFVyNs-@h9eVx?c3e9)KITUPZCcdInUz z5cD}GKL{e#bjTn9tHoT3oyx{CM&JUhvn1#68JVL%5<6B$;KgKXg)v6@xEloUm!SQB>il#CG; zuTCBS)9eA{_cGbX13ZL%!e=+ZAX(_NhzCLqNs@CwEQW)(eZ|`YZGR(jV_g=3-Terj zhl7Xv50%eG!PTHkqbhvxc3h)4vYoE0!PZPYd{?>&Rd30cBW4JiM|v4DGEB zoqGjsSVxNj@hwQd@?G>YKY}d2}ym;?9ET@c^3$!^C%?5va3e475;ItP>8h-zngrWNQ1jI`RQ`DWTLdH@PEaw{Z zuq;P!%4_}zyiYyM=O*~gndpWo3+w)YIh-bYp?AkbGVsZfJS*8ud%|l*V7*=g3poq3)9*QUqq0NZ9>n_C9#_{gmj*gHNfyciY#DBnn0gs(pC8EK5V!> z`1nfbXP6AHHcq6A=Jb@vDk{_Y@ZH(aE%2Ru0>d~OjKn5j>j|_T&kK*&9rG$I!e|K~ zTA1L-3+ z0(Y+`W`ocA4en#CCrTBndkmVuSLC}OC zjN?7N)sP&7Oxhu1`i*`n75!DOz{Z?{zwm)w3@6cG6^f#ZD;BFe3>LQtYq|k`eH(d< zXjKD|Bs;u$Fxa%L=+_ztzj;RVg>TFOc4;1B@(W}Rr;$Va4Y?jB2XLJQ;Ighm0$-sI z7Fdgb=t6!052}P~_X5|IiuqInmsAg{*%i2d5V(ZSINx_@-fF~%Yvc-au>-D_1<@)N z=jez1g0&H0azl5}8H_xj9vGJxoKL~oR)Nd1Lpr}iCS3Ck#u5phEfk+W1Gn=Q%;hn# zH*GQNF^E)S;iXoBDS3^Nf5q5jScwBXpay2O0i;z5>{%ypIgw!N%0U}PL7ysv`>F<( zqBQu#^00|w;2Eo7M2(Og4}~mt!RtPPpZJN)aT)N`tFY5I=v4TC9*R21k2=tCK;^mu zHL3}oq9g5#=YxToc13U82IN(PQJHi{1>78uErEYEN4^q)gA<$yZ?+8kKG)#AZ{S5Eum>=jmZl}Z<`kttKzK9Z z{8Q0^H3ZjgfKS`v`AOiHv$0RIDRxWN!+2W)(aeE8n|XlsL?D+tgR|Ym`Fub(2&S{XcC8Az!ezS9rgcnzRIWwG*&@lH?VS|d?Y^aks^ z03$sK?6exH$1$`wvdmW4C%F@tP7JGzI&u`70UYHTkd#=K!W1b)qS%9SflWnS*&0Ym z6YRaq%hJFVO~fd_0xO=2y|KBF5zi4d(0_Urd2fAgSa+;-=!Pw4`ORzK6Xuz7%q8Y3 zb1QJ?b>;!{5~{OA(`^;O{?b$EKpn!bAuDbKmg58b@*=F>3i=l?xjND<^vPb8ODa22 z^$$_6shV2I*334**2`AK=C)l|2dhEqawS}uB7c?|N(UHYjgbjog*{c{%PnF}LKQvV z$YdM<(j2cX)T(Mc@GI~I*v+>Q$;-R7R;8ZAWdn?cMFo?VoI=Z0ppz|6xjMe{>`dQyZw?fHXCf z+e$;(Y~+sd$OueS0hv&@47ci8A=Z2_2ddFVzoeDbo(Coe+6CGN76)W)CeWrA+6ir! zc28q^6Mdbo>r;%v=5uqT<=`W*PxlP)@L#+JEUPftO$yL!bTEsNu1W3WfV^5Mtln2! z+qT(`WAEl4b)GsJ`&+-ON$OFxA8?$v;H!4YH>BUF)*8|s=n*&zkH3z4u*0`FR%#aZ zAY9UI`Z08Io9OD+wVT>zZ8`9+L)sPXs`f-v^&UW3yMXbyV|+mGbQN@#e=}pODpn_J zmler3qRQTdIDDKm0dmre9RvHbPiiUWP=xYYU1a-YJ7LRf8=%H2Pn0xZOD)vqYA#+IV$N)UG2lhyG(+lbkwQazcK53Sg zs=d>`Y2nyMa2PvLCL13O*(5*;^O=*)uV#L$p*6<}MhETy@SaU!QAL4?cLrA3RC+EA zlm7w|p02*JHM3W-&#^58*0o1kb3`)hEgw`MmV#|GKpXes~zVoKmdX z76S%Z403B=_BRgU`jz!8`crMBRv(!2N=WR2wgLU|ue8$oDm@LFbrzgo5W3sbjN;}> z(*SP20M+dgJ{U}P2h>EF!3DHnuUS3mhSU{ohFAHazO;?7_p=|fEm2o1MU+86Rs(q7 zP#U1?eYu*hJdjsQOV~wJfQ?Zd6c-E7VP>M5?_qAl{{}QMmw^fTTkoQm*FR`If%fLr zS_Anyt-aA61Fs*b?>E{Td34Fjgncl*pqD*OVSF@iSWc@pa^p9A3-X~<)JAvFn>U1c z*-2@DoJmPlo~xy8RqUPY&ux`#o0NO<6S@5V@um7|eW1Q8u(IQoBC>%xzYA*GVDzvnBtfDZ^?0UFfOFyAk2Dalj+Caa`TNNPX zbNJm=AYsL^6TKYIg-)i0$hPXyOTbFcvDVTAsf~0{E~8vg`q*OZ!|Y4!e%o$f^*iM^ zauJ}0f2oy$l@nWtt-YEDf%PXk@G`uanChh6}-I~?4QVvaL! zK)>=^rLF8}uke^Z%$vXwiS^6MA!;Bx#sMYBOwZ6UER|&f=G{j6BfXNpDBsn~whH## z_J+3a7~3t$Bb}CqVn0l>(p*i%=%%Tf{Fn3^QR+UT^8zuQ53`1uPmIe(8SG%+Y>l?o zLF3#;q!D8*)kkSlfll|w{V+dx^7IqVl6+MA*J~NL251S0xRp4up zW9FwaeMZvBB5dG2;iq~u-@VO)MFS(!62P=C_ z74moNp6-HJ?L(gJwKkh`z`PtVx|^@9jlhi}trtdVV+v5}Fyn`|0Hf=zebCR~oprib z>#PqkV$BoQO^k0fP`dK4qm|}FvlwE?2tc0+BCPFX?Aj+IvX@9xO31b+dJ&iT0-K0>op_ktclvv7OKpo_4y9$+3b&+_%Cz2-|cyr zM!aLRL`x&NSvtAMhg*HEZ`jx1w%+KU10Msg^<|cde$0#3HuJXm)spd#3w?PO>nk|R zo4Tft0}?v|Xksg?5_U_>f~{)k?y3r=XeFX|UFnBZOgW-fwOFT^vT8svxe0g|059$ zMCT}~ro%KZy@i@DKWl)oMau)_s>*aV#a1LJG`N}Lt@EKPJamhDvvZ1CLEH}v#jeLw z{_S+OKGY0G_a(!+#;`~rzK>aFs^h<8 zUZ7Td3%0E&_ULp`qHX={6@n%QZ3(Iye9%E$-Q6!;g&ohOQAT!exAex|LxGyQ7C7wR zuH_@wZOa`4f_ktZ!}Jf(dYZkg-9~;btM<-Xr(AVzbnDKkwzlZW_wdiwQQhMmla@8j z@5!cxf~R?H+|fVkn~iSJx?}inf@H0VzSp>DdBjJWS-LK{WT!k;YQgTK?ybb%Tj|ym zK2xalDQh8*QPPz;@GcANG~5*O@!qp3?SdyL&`0YRSmCd$*P=^< z_J>>y-p;6A+}F#$L2qZK>c0b-bW6;!uW)Y-tLXY>i()%@W~+@+Cot2qG__GGpiF;j z<24wZN5*s`!B~n2^VnFczk^R)WfbB+NO@@^I%Af|lcc)r79paJ^&R{{8>4`^)oL!P z(0EoH`O{)$w0hDu5IantIOn)lg!T%{7U~W5(cghiX?n_|^y|Kw{zUI*?>hah^xElh zpAC*8J$E;q+ma*U5XmzpN=0d%UKGW#S_tRo>TSZm2OAo>It>Eq9QKOA;eX4d#t7-hP zIQa*=EXycWu`7_+X9q8H`a?FmDunI{+Z5K*wMhvvzNS7-e4iZcY3iHgZSGP0d&KVG z?qLz`2QvN(BbBF?3*_KU#SQbGUYL)ywF-}jUKXi2j*tafaA2y|P21uHs8!Z~n<6jw@TOLDM}^vhD`~s;B>lT+yz1-!Y4%(i&*bkDBlRYO^S*4CKgD4@7?W5OWW@4 zE@GTlBl3rZD))SYleeYj);H46>^{$AIC!kBdIlwCV8(ZjP5ia5jxR_Xt$94>61Cqm z;!dXS(>)|V_J?GL%&$MC}WPmzRf?92)^LS2cp>bI6sMQK& z3M>s=(OrB1jhBijOBJ_z%~mq_rgN~Xo%_2xTX;mIJL+rrBKuSQTvD??@$qJASMRd) z{3-U-Df+yiW|0%3?l@W-*;4)_Uk%idGTAn>enzlvQdelF7-yDI;kD%}+Be_3zzG0|5;TtJRH23So4mIRh=D0zdO%0-M&_|o2_IqkwGf_qxUijb_YYS)~Fzw$`Q0VZ-uO4HhMd}W*J_Wo|Utz z6XXe`zL`bOhP~ohjX_#v|3qJ%Ky?#+yizY@(~Z=BZ5x8tIQoV-+|@AuZkz9}=4@8O>h)`&i?<{77Dcy1f3JxEW|`m4WPG^CGYH!hkd z>{?W>tQj+2wC^@5`ho*CqqtTvty5f<*iUg8)0Z1nXq;3T7F^UeN-ZGIrD;Hw&Wbbq zxY-iSd>f+4Wo)w)8y#jg(1m^+Sx6hBdti(2u79I3fV7kcs-~L5cHcHK=!7G$Yij7Y z@TwW=MZS)DoY5Aw(%n*u@Xr5}_#+S-lN6V_CAC6IUe7J@$@MY%XOuUnfW9DYt-qUG z&^0HdrQ8{8vScrmF-vxmxsBto`Q97ppNC8`H2q}UuU~uPlGD#xAEo`uzu-Ripi5$; zTnHHPHuMY>=H)F1xUU_enq1b_OATWwW+JMayT(1@KfZy^D(^mLvrbQqn*duPF;t zYbN`XHUC0oR@jRS!(8D~O=G;dP>y#B*RP<@ESyx8OE{m0-3+ztHnGmX#SrPbOqk>Wa=@oe4n({vNP0$l(HuuudRblxeRzBv{ZgBytNv}q2U1MwW-DrolOtIoxVvTiq4mpXuZ{O__h;4b zTVDuAc3YZ1G8STYdKse^V)A@#Hvau-kv_?4LBLg_vup(V_GU{FwspZ$NJZCASC-JO z;b$YeMYo7L7P6KPPs;nF(6>CX?GoE3w@D05_?mLf6hQ^TbXPOg!F!k!=}!AR=cM4t zN>{d48g75?>=!a7XeL`_bnq2QpO`u`rEtoFl=^9>J*@+`&8yTYAANL0ZdFO z>x!{myX$Y@A0GInAGLM>d0Q?e}aL@w?=9Nw*VtawC18df2tw6=OHa zN#2s>vHx*+gR|JG%Pr(qwvE9J97pVJrOK8RXyR#?wlDQ!>dds4>ASpp0}D+`gO!@L zZnj&>NNFgo1{D3Ewb&S=^$6SwwA3e>ZFyV#PhTtk+H7q!(<`9&ZaI4K{zueVKu3{v z?YgWr2|9tL-Jch|w)ox$CrEmf!fr+U7-?y6OtPD}NvBl~Q5 z_q!MRSNYfLM?^|_JBXLV@+3T0iSixov?GOkV$iPOd@zlaPLU++b=b({+nmLHWqv&U z*zD7??4UTjq*@C3#$0G)=hmX*CC=64ewV`^qWKYU^eLj zzP?sMF?Xl@wQA zSk2T;QV7>ozxU^k&+k9)`ce1KiQhMVKL4dAMkrqQ=iqM6SMpFI9=mL_UDe$0969Z` z?7dx!gExnK40`Hluh@-&o+R;kW4pynim4rYF>Y*PJO4g&uzJO|8vd-Yc)yyf@8$f` z0C7cc=I`U{?yKp)t5+58z_{Ji3;WCZs`}pfmit4%(4N#^8{5p2a-@1vO`%?rH{svf zB(GHu+Xgz$Idi-72R}=)C)vRi4O6yAHpNXmD!%K_O5dyetox^G%-lb=KRaVzdQ-~N z(2P#m?`byj@DDliyBh{wc9(Yzc8s_8wlBi}l}n8gU40^9Y5auv)CunrJ|~Xyp7wtx zH){~KT?V4>x_U@$qUfKgLddGuW3+{*)lkTKGHrPT-{&;A`z~q1Q9YipE5Z z(#RX-%i!Uwkm+1fR+I>JgOgaK_DL@%tD1ae=rUJfDMy0uSMs0te`Siv z1+P$#KPBQr^c+fBTNw=`0IT2&>}J)S1)Le3Go6}iqN^Z0Cr$0)YGbpOKQ1wE!kGAZ zD5TF5nt0~>vKYa{b7x7{rSZhK3n?GulyWEXfW8s=9A!B18|W~fZSdFemGxfqeDw_W zX7OFcL$Fs*CZ?O~q+Z|$rt_Ue_}1H#$NWZh+tSSdRd-^I+kLid_U5)f z%0e@vehnO7JI`p(CXen}i^bgx4EX|mf#JsI^%YsA;@iFh8ZEaxgQ(GRBa`tNgpmgh zhZJ7LTh03!TPQ15d@v}BmgG}?B@=iSUgn;BH$CWx)>>v;kiDVft#g)J4t^KhKICmk zhu|L0Z}K)@`h;3>>*I>V=ZLErv*b^=*foh2^loMx`K+y#NOiTQ*{3;%Ik!2T&VBYb zwm92b`1VrS>XCV#*$BZxPmW*Dz>Ajz3v#?KFEgMKc;wUI6K;|rH4miOaCrqW!=>cY z=QI0>iiXGk8lO=vaKtx#ePD2?i~l78?uJjWApA|#dpWr8n({zpuv$=a*+TG%ZgswK zU3bq2QiHn(?+VK3(zT7|JbxPRyTmSu|Hhw;Eg3T=rbc{aZ%#vy(O*eX)Cg_AEt#XL z*1{N$>^MedL}$1<))=ph1UL+4fUG}3%&3#B%lnia z>I3bw?XLYF$0p}A*JO9Ipl3n%-1VGWw3B2>7uR#cc{DSzOML6NX7Rrgzx$>cnTc7| z1NTx=30IG+yVWx4NoA)pPra?Z1^Jc3ehD0TXYGO-r^L(Gh-YOL$MmJ(^;Y`J;LR$b zhvGFosZTQ=fu_%8JV0|h4I{)QaQ;`pJFf*rb(Z*WDg5*bS%m$?V%|~75s~6?^#Pj-zNq9?(l6&ws z5_YB85G$hb^#RtA0Z^u56cJ-BM@<)EPJ9%w~ zSYcC$!i_askSlqL_&`HrDP!=+f7>tV$@LuY@YF&}J*PXd2d*0NpoM=L6+juK;Vw>- zU6hLG_z|Kdo#lh9(%xWQ3nA6JYbx0MKg`!=_|~V{y|$xT3w59T7(_xka{%b<+(c(a zlFyj{I-o4sTUUtP>;*&hkFhJj!g35gkgetpX_9;!{9_emD0v*j8I;ptSL?_X@S9d7 zb~Xih+!&scGsNvO6SMsZF8(VVq1#~(9c>H%88{z~j@8C0QKe!E5#5(=CbGUM2q6j#z9?Gd+2k zd5L|LVqF%;YhI9evxj`t6G)wZ#AW1&1j3;$@#tO5%U{G$lM{u_1v;n{Z5=|i^f*}F zL@BLYj;P`Ycx~2$>EB2G#zk0AUdUg;n*EXEINpFm-XF+1sY+B)Vdwft_T)LDhtr7T zS$w%2iFuYJ-kOndaD#8K6Y&;|%va*7Z#Z6op}8B#wtG&b_#@|g4l5JwJ#Bl>=YE2& zws>#zlGRn4JM9AsY%dYRPsA!i@zs_h;@F%x=y0+Wrhu}YCod%1dl@_FdU|drJT9AH zc3Hx!Gvv|4Nb7P};Y2L$^!88M`bNt;guGB6jhY6h_{bmQnk>pO9DA&%UNU#`#Z z*9JdPgJ;co6&w~((`X{3d-(Qg;-#0^M{jVo$I??+6n>%$BuX+IwN7HB$>h{TP1DL5 zXme&c8=k_PuzZGdjl+NXjds1|cnt#V1&EAiWVb#hs{Dkkm?ykq zJ+oeY&GDW-{s@Nj8!_KM#GMn#E+ptkzsq>Bog6OO7WBU(8CpOJelsnISK!zJ%4G*p_pKb8iMMZN9qKA7DhjgTwg1+Iq{%dV$<{243YUUf}zzz1v_su7jDm3;8yWbo8qaEw=W0FH z;;75_>VXSv95|ZstO>u|jI$M(oL01}Et*c(z}bs7cjumZaCS%g=^Z$GbGO~O>p}dr zSl9>fY-pfYtezP`zYOEQA!vESI7jlz2>u_%<4AKXeK?Y19M8tkr(^j0SKydJjD9NU zmzjZM7IHrtB=ua*#jqPJ=2*&zSe)$3cxG|5FX3?of30&VV`e>D#pjm)?^pri%KGoW z_m?rEi}=nwUW?{R(dan~d9*Tp=X1{GoCmUK7Kkp39e!TmGxK@RYQX|dYerb#Sj=Cm zO;#(c(_)Ob+OvXVH8W>5t+3j&lEZpuDWA4JvxM(iSD4S$qB-XUe%bo{|Jo7#zu&id zz+$SmPS8N~%M^4Si@$y%bI|%bnP-zhdQRn>#%cAO^&gQV-m$puqxc)eF^ShE1im+h zIXsSI5`8|7&shIipB*1KCh+Q*z*t$MGnA1V$m|}(cw2KDTqS2e{_DZnGccnq*7^?2 z;nvI=t1dt*=F@*O*qT$;%&qso+4%+frT;js*R zc`?r7o! zCm<(y*$r4H63}|%(QxB9aqj=``1QZPG2)l_$@3TY`lM|eCwiO-y$`0oSHtXHi^ z>$NZB&c5S(kN*4yzOuLCU-3%3=J7TEzY;G4=L_+izt(BJXMOTr;C1VB|9$5FfAS@t zu+G1#VHD)fgVotSZes$#NinQq(kUf?JvSd&n=Au;tqDNqM{#X5BFmu+b{xA&5 zV;vSd!VqTgVC0S^t%nBYwIz$J89p*F+eagP#<4yu**=bS@ZV~f9C&7}u&Jz&={(M4 z70zI7&1S96V*O5IrOphj+j;+!2ulN!U^y#(IV*oHSm<@^BWpP~1$LEfWYp|nm)Xl+ zvxoCAzV<^Pkxzk?JVlKA9C+Gu>`WIpZ?Q|=!e+j~j&+~&Ay~VI;PPIgEB+hU+diT# zeg@I|iyhD6jfeq75eNFjs*K8}}XcNN&__tAL7ZcGir=V#}rMAHw zG>>^`n+wrKmV?GyjfS!b9mUdGwxhr7MvplFQt)^{v$6Ei)9@Dj6VRRRqC?$*`^?fy zVHiTcdqW2FTXev8Xm?-G@qVM>#i8L@@pTV+o{TncX|8q-Lav++bUinkS}2-qQgpUd z94R@|q1RuXtnvl2LAV00JLEd@N~t|)r*r?QHpvUW#uv)Zb_Ek|2QgP$zP6-&y+XkiIjSc1c9aY-z%qMR1eu>kj! zH}E%{zxmL-3*d3e#hI1=^KjJL~Bd}MB2Y$5_pQ*_I)%dR*ts(}(IF?58RiZ5w@FkSy zzDmIRH&7a)o4#lejyYf+SqZB9H;AE|$h01e#X$NfGoG<8+?Lg~ zL|Vf7TY`_~EE(9#$aYg0uYKlf=H>!gya;RcIIvj_rzX#QA6i5x zyWn+fn}Xo{a-fmrz^XqE?rI~vGj3%Uk74)&#lTQ^3Va@VJfBg#_AEw&m-*PGuZJP z;7zKD&Kt#kz8$oC0@mbaY{aYl&OPR`%xtZJ1w1-nE6-*p+R2qyV^#0MHeW|y_6u0T z79W?jKH{)WZ?cD9fl+NOzt9!7xx#2nCE)jQp-UAfInnWu$vTKf_b%)_j zI)p@fsZIf?vM zD#(77lG#3n8MzIHArN7dR$jDY@k+K2apBCgACsm%vbJ&j(k+%`jCB{`Uo59A| z&4|bIC3Gh!B zWtL=M79Ew`FgZV#TPc2Jwdzxs!L-vC)N332FZ(=4Dd%`+TGtfUYWEn|MO!ENtMQjv zM@gslkkg6?xQQF$eO+Yy^yTzMCti*p6E`D%xaR^dOIY`?5d3I0{dmDc4ZDX^XXVwr2LS&Xn$>?m|I7f;uJnm?TX|73W4W znjdOS?9a8M@?_S(Acyy^c}6$A`4Z>F7XRJm_sZD%o~^!b=RVx9Yn^fCfO8HCb$!*%CWAt zt|QK3j^DPmwnw&ewr;d|hFI-C?D-bICH82{*4QTTJredMtV>9f7*5Tj?TMQcmL{b3 ztnqEvH;S23O(mtaTC1srfz01Qe)2iB8+Do1Y2)GlDXJY-ddjch+g+#k_tygB?$-bI zkMYg$R`+)ChIs3F79{RZyy&UpyXgM`YOpeq%tEqVnWo;hb#Qie7Yhyp1)wJLgifNu zOd6M;xJL=s3TI(w1E=8#b9~TV%JD`kZ}a#^F*{>&$JI_amG~;LWnxfb&O~Yq#P5nL z9+x8iS3*zkaXq~hq;AoMX(iRA@;fp;YbuqAAI#A9+Ai2zJ09DAXn!dwrQe3vZ}XS; zHzE?fSsw&jMRjlr^AZOp^omc6yA!`TagVo-znfm2x^Y*Hr^XKIvt(2s*_Ju81bqrw zoitzaOUWWa%LGG6?fm8}>>A*D?E2+8LYrII8>kb_y8c%Qqhiy?Jc~IWw;*9uVw=P* z330I0{}opwc67|Am;-SUiNU^V@W@V5TBtphchY|3R3(t7)6|k$J=+s|MZ=kCq3nSrTy3aEy3LW1wMQ`=(aXm z6~}g0iQrpFdL+%AYdK}H=Bi;O}rZaf6DGTL)Iv07q<#F)fJ-c>}?Bdr=<#QQG6 zj8a;;tR-;_b43TqAzwn;g;WbJ9#qkt#qDxGbrp0^cYkoVa8Gj9v2RnqO1F(|zI>kZ z37Zqt#2SqKkc1=&Q{vagMZ{K$iHI2(+bh0o;#W^I-z~ouz=4&c$0mzynSD0(KJyuz64mB?MAA28oPI%6G zrg&l!|4p3lx$SwAI3z)hUmQ0&K6BzY?`(e$I8T}x*)SV*vc(0N!S$s!V556z?})s1 zx4i|a-IrYYwn}F*haRB4lmXwZ!j+rdxaBuM`kw|TTtq(%8oZ#M;7_J6)FX{J!v?== zw6a*MVSnmK56l^!1A9Zvm=A?6Ap=RW2R98X4nrR6<(N$c-#!4$SVGvQuuT1?~_K)^|>`TeVdT8rv3%6~9jV~SCS|4Cuc}(VCeVFFw zlJPhc-%&q2tF7@EMesN_;8RKmFYMpq7#w#m(2rw85>VQskpvyd|LzXndZsa*?32gF zC78FiGaCvU?_i%hu3zB3WA&s)7rya}CX`9X4z8o*~V zM@_EnQkTQokx`wcyi<$XhuDP{LtfoGrMPxlE1>mNZE7p{G#10-<=_rdFki|l9bp9d zAjQBn-AZ{&ym~v=T};kUIpr34oy(EFosexMq^ag~BI41sqcQxq8;z%Wr2gAKkQrYN zEPf1GKKsf0iO`#(;dsdrSw&u6JEJ&b6-ot#o+6t_kEgIDHt`|qBFu)JxE(o-GvQOa z4tjPlzZ^nDu`>3+Ir%&qYq{WgT){br(Y#+CVJadNvnkNm=zYs z#cE%53*1IasEe0MEy{h4Q$E3P@|LLS7I`_dc#LV)!}N(IXufUXnyyHFm(#|5;>SIV z7kW4`AG;D zvA-s!`T>7oHL?Wzm^-n}G+KNK4Y@PS1u3xLThW>^=)upVBXS!&7!%2mOr~t3b-7?p zJSgW;Dl1pu1aZL>^HCnER9Bkeb4iM=bQNsiZTT_QfrQ5+8yM*zIV()OYc1v(`LfiK zOpdnb=+)5ZR--Y0!vYyiu77Q6ZVbmd+KcY)Gt$Cv(Kn#M%O)8iXv~|$G(5VU%u>=3 z(HYI78h(nC;*Z|S_$1QFtEoScAo`Kp8wOi;1@!q^*qQ0b2UB^zhfMLt_(v|_1@wy| z@?<5C@_{V!x8`N(w(>$A3WG**>AkX9%|h1wa`T6@RT(0$fycQo@}(U4p;kSK^7z)q zqlFj3@B7&thTgMW4reyBG|!4_*rRD>qKf8j*to} z3?Q4ai(g_n7oZ}+CbO12oJ@9D#bIUm#@tD1b~KWj)5v5!DLo^{{;NnKb(RNWSHxls zFE?+A&Spn!h&g6Hd8X9HyhnC`ORlK~DKNXRV|YRFMaezom&~E7WcXi{6?(Cx6e3=d zn|?urSiV(hf->3c2AW}(nNp2bo{*dP3mF#*3)5Zk-gs?%6Wg)I!^|IsX3l{h%_mwj zz9Xa>N>6i;9z(^A22x&m7nY(Sz8Eu+Ccv@do1{zT3A#f`$F1)LxloUVy>JO&)(;SvG@N4MoT*PKm$Q4wrNvX)$~syF@M+ zMmoY4vqb7_B+(y9&y`1FKYX3%&5lYC5_~(D2frfV9QncS-I!SyjW6wikpqA7K>TC< zjLc#v>v}b4w<<<0a~Z5IEu>e-HVICOEJy_(w&_=64f8CWD1n!iI>79Q-C$6&5xpt| zI_9dB2gK?%m>E(r2X2E6Da^cTYrdz-Ocyhq7!T&DsMJZEHx9rhRFUXIefEPAXl0wh zCq4525EHdVIQK4N^S*`Uat0EvE_ZNPJT;z)I#MlpsPuqLKM--vGAt{p`rY z&6053ZZ#+CH;m&V28N1E<_O~@v84r6nfy+bfvr@UNMsGB5ucI61z3Z1^)S4P-;FwQ zKdkgyqLu$EV1TLAL&yQU$bBkG^aE|Y(#$O%F_ubAL{I%X>{OkQhFf9Et!<=)aiXgf zCKnW4g-a@fHkDTS$0QpOPm5b#VU9DB8duD}m43uh2KX*9p%>4Bni>pvk(5TgEKBmP^?QPm^zS%s4M?)E3DzVTbxhRAI!e3c1Op((*ZR zR?jDmguBsCEr`*0wf4jNl2tyc;%hfb8dI4Umz8L$9=sGwmA1&H>`J1ML|G(#Guo=j zl-qh8@zJcUdje`61>f= zk(RTOn}WzsVL0zV=NJ-G4dfPySNDJ-6SJasVw(4%P9|xeMVWenG}!a^#*p0Jj?{xgz+>M#P^;| z-iW3%K%6t<6aj)uG86FiJT>1+%Y+>skR|Arva-PV*XXTe04o$Hyj=&G164V5Q@IE8})_T!aASf)x=Xz2uU>8Uwg z&T9^$azsb!YSfUf8tu(Z@&IW!JpY@e;#7Y)C$ABE@%Nd=C-yL_ir7*k5-nvT41^Bu zXN72B9>Z^#+p3O3-I*ky6*|F_G8Mnr-(<+QgIVR9s14_cUp`b*B@rC12?@{E=&F6 z%*Iiv3|xqtqy)X8NP%X)2zHX^%!pa+PlH50_&y5byRzcsrPwEvfn{w;#g>Os7o)S8 z8IP+E@7`Uh5s0(5<}s0r{c|{XxZ4(=q<%s^Qux>9$~yi4o?#IiiS0OO7*h{rk{ zW}OYpfmC>T^HR5FGaNCo#vJpP(T6#^1OI8X(F$!}h+6UtbqaS8BdWE zr^&!-0HfOuvpRZI0{ovD3_CX07O8^KR|G3ADTCOd$LJlU9!gp>x2R8Sbg|h`q?cC7 zN7!4Ah-m+N@em1hT6YjXsY0Hrc6fQyH$5^K?X<*Uw-YHpg7h0JT|(aUmRgGbx}jH>MycnO#!`9xvcI_Gfx|OYN+C)Z zN$`b5D6N%Da~(-DCdbdc#YQN zS2V0l{V;tJERk$7yj%m(l~Xv`4_ryA2CfnAZ69vJ|CzYm%^<+gal!E13SjV-XZ|N+KKfDq0&~7&G;adgB5$6+!$;`J!V8DrMB`&ZY1?E4~fxK zYoey7+=*zzMtHOHQdeq-l2mRWe}@IkYxaeCcdOz+9<9eBS|YdB?n)`l)Y3Mw3xC#d zGf4fQ%tuG>XY4jJstxdrA7&T425aIydA;&QsV=9-$Nm)dnckLNLVu;jo|#NNo+Vg2 zMdilqBB5AJU1S5U&0ca7YNr2`5@bKQLkGF<=hVhI#*8cnru8-qe@kG0Jd7l*O^qeP zbSk~Et?>iG5S#>FH3!@_H@VUsDTkO1OIT+3Afu_*G#MVHcKFYoWDQovZd|~+-vQR} zFcq3kn+27sJs2Vo94S9WV+S3(TF$T=fHX>uEAL zR>7yaju{il|2v5KWFkseSAHdThZ(YzIgp6lQK>FD%d!$Fj_S9>RN1YLwWU$dz;GR} zA2kbVraD$VtlTt<;T1S)R#47s1-0EEWw08=KI&urBlm(qextlvxQ#YqxV(k@yz_E6 zvgw;h!>(3U2~%R=#A|J)px%?mTmzE(9t_#*sTEg^YGK#Zu4-pE(qyM8bk+o_nFTAU&FrETXmKai5#FH1 z9u@PEY>{NLX!0H;*?J_p#RuG+J#vLvohVRdDLZ=10uaJ$<*sOtUTnUZRI-eMgD@u^ zrX}R^cTs-fL#LvkxTwb)8(^Qj1&eA1n7HbJ-l>GIvanJH?vK3Ya$}F!A{8Rzr;YN{ z9B+7Z4bI3YycVtH5c01l3M!Ql2h1ivgS+ex$fF)GbFGy#T5P9iXJd?Y;+(V#rqkV2 zWvYt>-Ab}B*E`X3j)>l5=Q!o-?0(0H5gsIFUjfFuH=-U9qil+%EFmuW9fmho00pv` z$bK&(Sk*x?J>{NWN)EXm83(PX)|r~vTX{Ty;qn1#Hk!m+JOU@A3fTWynA;s-Y4nSD z*v(pkj_6OtyfLDR3Gx>7^nCL1FQHrH0GE=8RUAp|t|KV5zC;yE!jC;j2JH=ZolbwfCZ(=Y#=n6|ud&NQD+u8T%Wi`}=4bGw}nRBQ~2FyP7#i6!HwX z`Wbx7!Z8%&Xb)QWDlO;>ioPPT**j8SB$tT}GbiBdm`2nw2lr^zhdGPS;|VNbdC>+Y zVg)}$H=JgcAy&JQ^D{oI$5<;W@$m-eZ+*B%Df+V@THZc*La|Cgcvay}S0KX{vR4)Z zCAOY^8^|Z}!9%tHbXRjwS`U#}HR!`Quulo-qE;-o3ifIYHg{Vht#`qmCvab#>4Qz^ z+2im4y`i^jV+q~{1y%)Xa1+r}t4}Pn>S;9XhF~&M;}M%ji@TxMt)eE!eZKXR-NegS z+@WVyah+|n?h3yYLzHwQE4vE4(1kt0!hlAB|5}8uJ{TQ7KPW^iW?Kqx&or)-4!*7$ z*rCh7M%HEa>_tb+fe#`-n2nrZAL8jb3k_z~rFn$)RTf`g33T%F;L=K?^X8+?{rJ5u zoCAqT-{Mo}LAG_}TEmHQALo^~#923hCJw^_x3Gv_G`-&(Dd@)$w00%!ED!$iJ3fJ6 zum_a`cRH9}&kG7L5e#912*OX*lDW2t$f(7^+8m#*)|PW0u%I4D&O2ut|<@5zDojQ^(6I^ zno(<_wFJ;IQ+2~1)L1D96L>Nuf!vQKL<-8Icf^bGVh`-knh{H0pdXLB%lHJtO>JtI zT%cOXHJH8U>q&^{9EZdBC6C99h1A12N-gP?aDCn(&v7=AYCbc+2xtL62=$Wm&2Ooz zJQ>|&3l)Jbv%Bshr)0M*VE=NfGgKF?nxHMyR=|dHM^kLo;ko}orrlm!dU76Nnn&w6 z4>sfpQf>qOmKDt4g60JHm{#dt{}STf7RT5&qZPia42<$?*o>=U(Om%iIbI|Ki5PDd z!Si;X8CF(wh1+@%EFdbmg6+uudPrZ}#c0?HR=}FGM9)qwBuiQA2+c{7kbN3h7I zfq|%r7uhf0XP^G9mex9I-L#=_q+irp+nn|{WC`AJY<7IMpSJbZa;V9b@<`be%<7Mf z^h2?ks>b_`WX4o7qYl98v5fjTU-Um{(ChGQp2GI-!m8iPn#+z?pccrCGRT(-SO=tC8t^(D zkrqN4Cu=Z6|A3QjmikyNLT_iX9k(sBFLLa0E_8WZQLZA+?Dk9QN&JRi1Z-{aXxD<> z&LL8Z)1VK${^!0A-df(`u#Eic8=zMaf6P*hX9wab$pRe9ks#FW$)AezS=v zMoO@vi=^f9cqI=J{tfuvYr^JtRryFx;2JH)R@Kqg8SZ-S8s)y_?(IJ8oM)RZuQQGi z@xJN(?dweTXajU@s__{a^~Ju--lCr5o(-NS-hTc%#xN}Pg=l2`wW{hU*@NuZi`Mr| zd9BsAyB(+Pi)_u+b!7dQ_y2|uZL;?kQLXmA^*)EcEPV2%e6Kts6Wb=_O?aQs#xu~j zMBgulz!tq+zDiZd=3=$6hqzilhXJWfUL-zP}!SVKW)qTyT0>S>$ZF^;Hp3)@-czM0-g=O6A3^E^v%f+J|^k^H@k zdZGv13ne}00Tl)eF zSu%Lfsrp6bkrCp9*-FV`8wb?wtn0JO?oRF=>6YAk9QTxr{-C(4zh?b*C4?K@)t&b2 zj+r)D^_bc9;+}VL1!8~1R!V3LT4Dg6r7YSf#}fA_cP0mM3cUpkeJ^#i1yYX^U1QxT zTz71ZZ_@^yaFvP zpA;dI8Y_t!=7tZhrasmfZ0?Zfs3)~NwvU=ut*Mm5W4qdj!ncr43vwiNy>$I{?Q_j> zO>+Gt*E6&8liE%%954Mz5i`!yO4?!n>OAV0ul1LIi|l$?@92bQaUCXr2T zuIa1v1QCW#CEF5DYTyU zk&Z%+V$|CCth&@lrJD@%t$D?W&<(12obv_iZ;e4{GE+oNJ&*6MXP76gw}1~WApN|t zh_yTx4a9&gcrS?MUc^!k3XhR!OhdEXgVsL+t8N2`$hoY<5`6Xz8c$#BnVC`^d9+eN zbJ(jo#yED`OW7_`#q6`z+Zi34K6GM|{;qGza#)&f;#W_vWR<@Fq1~!KFzyorJ!sUS zcGE{HGoA4%-khGo-kbVu`K#?8$3^=CZ2)XpW3Rgcmgn)t-k6zcOdscgjAoCi7x%$^v%-@SD2=$2bjxx58C3;!ULzR;2dUx#MSMn!n0{Do$ zUjZ4sh@GP$n!^V6kO(x@3dFiAzyvp&wX{ylpgmIk>S^t#t*v8}v%af{>yu-S?JItR z=J?|$;qmIF7ErRlw%!>JqMNMM7iKmin%3;{jPb7VWz}C9l2luXQ3q<1n9u9Un!cmv zw{5dMWbauae~^k(4JEz02+1iFpS%aoU--7*F&zpzEEGPLD!!%O#oh+K{{Gq2a+nIj ztT4VMLobBAl3)yH4=vB0bQFwTCp4mF*zbE_fX$7x@5}DghCStnR6_1XU8;#N@s(4O zsv+RX*1`q&udTi9rIx`q+up)C)Ro#9rLC4yrNhu2WYjD!90oa9U)y*9V31j{0w{l@O%^R@PCeI?SQBiQ0uo zzlp&w7t@R>Iu*L$Whv|Z=50u|u*_6id#Kycw~nDh4JNmD1zN#qqQghfQzZ1Z(q#WG zq~%)_I~*?=*sr(ZLtRc)(WP<~Dn3-xTG$$cEV$%o?cD5KatY?Tp{=aK3_fm5C1;oSa~AH z&GC{}G3-V**c4-pX2gn3>}RTpOJ&fj(kgxEsoGS4NFp!8vYCiaHZ3u+;l{)Tj!5lt&CB8L92xQKu1koX0!oA5s%-dV+H zK!vvUdW2{JJA#+}Zks$s`KmsrHr_BY+pAF<u8hRM&-OoQ-pN3tX4#V$Ka7D=2q zYfRAR_=Ehfd=sf{wak0L8|7Q*p9c?R4OmWfyt!quB=9_o>uB|D(dJKKn>1yfOHj`$Uq&o(PoYjjusBx-on058>?iO_ETe{7;>j0 z67(Sv_Wt;C%HW}YLoKaf*iYg4(sxrG=9+&TJH#ug3jU>Eay3~yO~%U-hlLkP9OJqeYn0a8QGczL?;vxpruVDY@TCCpQklw;Nx_ts z48-Jqq3gFIs?|_*fJrw6m{u9TS|8XKcatful3AZX)%0x2E9Oxs8RQpX7%8UNzzIIE z<)%VoFUK}VKYJ>6pyODbKd_x{!y{1|OsK~whgJ3kjOa!q49wSdFawqK2_Qiu(S5_H z1&lYrTqI9XPr$3>)6QsXsLgjuU5%G)3|RCS`nZMpN~}hA?u7lf5yWXdv73nGF8w(= zpU=CT`bT!SkH-6s`^x##>1B*O*dr3W6T)qMZBflJnAHKSN)BhKU ztntQsJsfQ}Ef|mE=*|t%+JliB9#MceeHk=dtY$e4*3L(zCReSgm4s<-KOU0>%(`R5 z5kC_lxn#UyUcCYL;x^uba(Sfp)))Fu!6#D*M&-i3Tj+K>ywzdt2muGw49$OmC<9jc z9oFm-EW$*%%u9k9j>Z2pABk~`dRSAHuT;PLtY*s@1FDqOqUMf`UAdR0 zsI}$PSo3eWs}2&F4*YgU@C|>2HT1QAxjzF~p>o(&?e(hgjyA$Od>iYw5uSxL(l*#T z>L~ePv!1Iw0((*ujLH}&C}yaVkqihh3~mIy`EF*fS#wP@xW{rUa(;dKqBPGM)N zwWZ-`LA{<3y*~U4xAB!WS8wK6(zfYQOW`vZVg+wnt?hb->JQkMtUo{8SB_9gs~7GOSJJ_|4F@_V-_oj zeu;{NjrFde^h*jC_=gH$08-%FOhz@LY3g^Z(1GfDYS_%=_lx1FGO)gF@WfWcQhE+b z?IzY_0p{KdxOJ=gVqvyE>K#ozu5#X;-j;B@+<;N%rQR1yXIKD>{ZpjHBEE+FE6lz$ znfUb>?x`jIzz<4Ebvkvg-ceuCP93SS+7hjy_7n{TlSE z74}&LuVgoQF@BuB=rIl~H${vw(it}6B_r`3wCg+SS{lATzWr!TX?>-AlY9@DeSN4= z+JO7&4mv0edsU2>kFRYJ=>JEIRvGrG=dx4DO+B%-$~QR2s)9n=qvpV3e#(k&sX4Wx z)S$Xc8p;z*Q|E%*sE;SGkMtTFxiZ%J3vg4rK&5r2=G7J~_H=Lr57s~FbBKgx0Ymk` z$PS)#3;g(t@F?yf=JE$@%Y3B*(!3LuzkZQHH5a+}7~fAJbFK&$o57D&H6Ag8*ZU*k zz;XKD`sVp6`@($rs0Fqap6xvTiGI7j0O>x$xItC2t?auO;YS}rkH3%#gLD7L`mIg% z)VoS%wXHf#oz3hE(uQce*&_#PDK%9ap;fcpr@mYV?I%e%x8*M|Wz;4@p9$?Tl+~To z7(ykl=kSYfMuSNLn^AFm5hdxtZj4kz>KPTn%Y&vwR@4ag#?DF&V+` zYv^eonL*!RKC$G*P^#_jW?kuUgdg{x^hZ;ZF*7*NQ0hgtquSaTzeDc;x4FiAlO-{!w+>L+r8UCq)R8uH!UKH&`GSDe)&?QmcvaJ?fJ}GW%G*lYeo)UisrI*;Tm73A(F{E&5BzS4;0aR+AA4eEkzCNB zMlz)OV0Obj;@bc6gP)NZoRpl0<;dUccsZw_(|p7NXbFxjkNktx@HhFDapTHxY71_gR9_)d&Vv`2%m5s zur+u)&?2I!Y4;HgHbU*8E`?e11$AN{sCSVaw|Vwe^-!1iE&s0p@t6xV=1k=C6(s00 zyv`4qJIT=l{sv`P74%nm>IZZZQ}E+GK`%>#*Do7+E(O5d6vtnh-An@-SKzhuvo9+= zvsi#$GopvkPUoYawn1iQKx!nwG!sXw{f3hjnw#r3#ap--Cis_JHx;{X!GQ0$Bl^Jl zK&DPO{;EyjD+&JDS}OYsoP^0B(V;UzqdlpMskW+MK4 z5v_NaTobDkS%@5Mgq;7v9ykHtXb3ybF8q>1xU;tCpY6EUelREv!4oi8^yQVBMAK6W zCuklO*^p7>qrElIb;iP}e~Hl*A{{+Yk;qC7{KoA#I^gf>iEnp0>t_qQ(kCivqyqI9 zj<>uaXn_&zw>$8`Ucw{mC$FLj*2e}g&WUnX_Q$?dbeKX$*HUE~vBDZkF3`?CG^z*k z8FtE%%$G>=G;QR1oj{|XKsHWkPe#1PYPAo{kC=u}J)1LdJhz3Dj6$ypJ;&?G=p15%j=w+}SpsU1e`Q zj^FhHUhUkBO;3J*75Qqj@aoQ^HKVcS+Ysq!M=WJBc$>AX+6|1`7S`%S&aw1)0;4%!{X1F$7{Q}`jooBy@D%K^&)Dq1z3_0!t>)4iAT@JnN3GvE$%*kf>m<#Z|;v5#YO-^Du zVMKch@mpmX-L#BuP5Q3_IgNGruRfm*;dPbXvJ)q;a^#y4%UA{)XA-j_Cl+-EWKS}l zZ)D~~Aq%Fl6Rjj?;26=#uC&8qya^(cel1ZpE9bEQ^DK%P*pKJ-oJ#{EE)14I{&)Avx%w z#9im|&NiZr|B(H#8BdB;@nW|5o1jc`ZTG$n{jJOwr59t3n66J79cmB8WY^zQ2JpeS1N?B zehd*(n{3cG;qq^O;TFGUMdCcv?#jvYgY;k&tx&;^6vl#IORVt#*I3E8S3pLW=Widr zy_0*`K|fhh>%+{EzZtPC%$=T$L2o2(5s;5D=qTeDnQz?PawOLP+B}kRN=GC#DSh9b z9)F9?=%ANYGdHqROYsJX^WkXp*|-}k7kDUru#s!d2PyXlo9qR9#ZzYVN#Yw_i7K8p zYtf!D%#QK&dlp*K2>Z_@s(FiJGcm^^%!korGk4+srqc@#z+ziD+3)Z|ePRt&XO6ey z+o@<@0eZF*_0&Abjt$s<`EeM8|Xzr&o*G5bY?W0qS>rRK2+m345ZCk zR(?3G&BvY3WhTXw5mG|#ZfVxU#)7F>F&0TThaKh|xWOo{a)h>|L&{{}+=@=OoN?Gs zCTM%SY?HY66x_ulqSDLhn@32eIL5OiYwslNV6#|-R>WST{|2zLWurYMsbqHX)Eu7qTSVJGVqIJNR1YHR``LBEs3F;cejdY^S3~afV%|hyBkaM7j^@5+ z5*^>qyKeT}f6#HpGs6uSXO1w}7o$mBU=NPQvidBNp>_D#IX5$JR}#~Iz_-^17_sJ% zG1VJy-)-j4eD;V+?7N#pbuz^+fzK=i%IY!bqEhU3nV2(giHy$^i_PQczXjQqE659w z9nS--fpgH;8}OS0xQ~>~>7V@iHT;slnDd>PlU?EMxgh59x*$jCiU|^!-!8@H?{d#M z$y|Al99cojDs%Tc1NUE=STS%HHuU#{fKr^tuccMKEM-GCMV2i9y^Gn#AELK^1)$#a98l37*;4p$Bp-N;xe!#eEA4mpb1REb%(0zLgH zBlwRw3pC|MW>7zB98N`#$STdpuau2($}9Jy>MblcNP=FVop#Yii|2C`nB?WilIx%| z>VZ%Jsf(PCrMFVhmg}ssob07D@d4dtWyH|;J&@tk%$9h(Qs6=Eg^aGjwH|`a+R2_j zirpxROp}!KXbJS6V(bTB>4kjkMyWxwALP4fkR9ET+A6Z66Mxr9>EK;R$Lwl`yt3uK~qi1 zN}bN`w;rhi1BYCk@i`{-;}`ew?JDekmB>11iM(9OT_&RAMIq-3(uQ>Goc+1OCG=W1 zG|D1qU{CPBSb9weyIC!eR)^Uy3ri8`kH^`;&e1xrxfIEI9{!OKvbTpIz1Faj_kg7; zh%A0!-T-kno1JnreYl4Ek7a)EWh6%+Q?}Cw`RV0B$f@tF4DwsJk4b2i@0jOJ(O+t@ zUt2g(FRxBOBNyjW3z^+>(K%MKD{FE?cGHbu!hPZ-_dE>SVlq4DeXiS39!K9>+#|W! z4MN%XzO&D_W~7F($B$wb3?NTuKCNrbE}afN`A4Lq9Id{Uv2K>}6CxF(<7|JQqHbUr3;&)I`m|3Q8`=BBASmpg6~Bn1CE_Osk@q ztp(&WXjxs^o9gn7hG?+2Sq1SToL-p;dsI*E?jrSVSCQqC4{7|-9KzfVkxzkG%*}NK zGwuS?q!v5$S!CmAa{QCYZP4qe!prUw#P1o*zLsc36QmjPCPt<*IOB<2X$NxQ0qwJD z!bYNpG(zGIWp<_lLwXDRy)&R;EJT_`kZrS^yo;5LhX)JqC(rEiPU@dF;@uZy=e$P; zYRo9NLe_Uj`we9UbY^XgM5n1|W<2T521dGw8y zB%@F4WfeYSu4~BWhUng8lOW$tum|5Y7cj?;Ac-_|!`@tf7rSvhbNB&0aDm_Y%3eR7 zm1x86*~YI9BEFNH74SFrQXkZw$?E-!S!rqX4_F}=_&b(gt`DLq3D+#nCk^K1Z)|Oy zL#7w&v&!utJI^33P9e*#FsDXQht|c&pGKp~jAmFHjqEj2`aKn2cX1A4y-wzC4r0x} zWM6Xxw22bPtt|BKD(3wMdOHpGHVQkVEr{~0%-&qs;2qEevhXZ||1Zo0RCgovn4tT0V(9%FM6F zpcUq0K9^vYOh6kSh<4JHZ~4%HCNaC~GA<7Gth~(a%B;Lvtg0wxp;alg3|iZITJ)V6 zE;D9Bk(`}aQPt3IZ_*$2kZK9cI43%GUE1@Mb-15bhtWfi*jLG^K@wQ9?+H*zKCqy3@$1eZ1d;*}4nP+)Y@72bdk(uoiZ3oaY`!@!nka)Wfu(99IQ9 z!+lz8(=YG}HD@GxGhRda&92j(*dUFCjyOXh9%isM{4m@ZbkV`XYZz%U$g?n0r4t)Yy+Z5kgLHROw+?aM$LIU?h zRwjZb?to9aI@zZ8@m;i`cN#KJmT~Qge0B-etRGS~a?oh&2ztIP_ipVHeHp9Xw09d9IwAF!@uW^Fdcx-8$QOdnNe zK3H|mn+7=DEPJsYeOH@SPeEpFWyEF$?8bJy-;hsr;GGfN+a&IPBr>E4S4ziPe1>d% zLA!pSX?$kpzClYli|k*Gm0@|ZEWX=m=t36X?FL@`kJEM&d*db6%3E4$d4n?tbXaSI zI@0TN`2C}JWGsfe%gCk6cv?P#Yz!mb(FPv%L7Xk%3Xdd9x*0QL6dvPgVE#woUoA+M z(<#QT6Fr_C?d3Z8b?vYp4Uh(_#28oxijqkcN5tj0vB}tAY^5^F1)?u+!NQ~xML_8G z#rJ-dxLg)6$`fhlYxI}=w5&gR(;?=m4R1?NeCW4`0A*215K(PF9Dl5`7f*XWb%=UO zbr2<9M3kf=)lOW>E_A97;C+Rd#C|y+4Dnay&n|O_2r*vybApoi%U?^+XT*^C`G#y{ zasa?&9Dz!2oh*?B#up+! z4fJ069%AV2jK$zOPjQ`j4eA9^AzU#T9IkujEfZo%{ zB*qch$VMFerQVa;fSHUcSPfgqR(dI?Bo8J?c@MY5Xjlls<&t>9XHwmII+@`=nB%Wu z3+o47+%^##dt?$nb7R(hf+F z)-K zsgOMbkAG76B>kF_y?-u9`u9W|(!-i^hI#@^i7#j3-nxS#d*EN;AK>pv9U;GevYs5a zj+@{SJ{jMjGtEsDZyWg&f1?$>k_J#U_AwbshhZ*wN&LPWEE`GH!PIEYKyy8IvH`YT{ zcAHs9mbS>Hg6zi@pIma}Oc=e}jT%cX5J8`m10e3ofeiD=AxcAK6>+YU#3^sWFt`j2 z`Y=8jD?$*Ag;j-^$ai9>CB%0aP`0tsHxn7WOZy^WcbiJoZZv-ij(5*&cn@mREyT*FSgM^%b#1F#9JC>f>N(AZ;)xQVAlk$o>EKq8x zuaq&$E%_UJR68QPu_7gwW?#ngG!pU+mT)d}DUm}Pj5K|Tg%-h|62x8K!@lsc$1k84 zmb3S!Vr`USg(TGq?=U@r3$pt*l z08jnRSi;I!0D3bDZ_@yx4L{Mpv0c#ZV(@LOr$uSmIcBgIu4RA!C!iVhX0OV?-jJDb zv^1ib3skam`(3vLd$1AYb1N;s%D#VU@^$H>Z^d|;%2Es7)NgA(H0a!owm>BXF zl5&b?56V^?Da+w%^?NBX*$l0>MSQlGnTC@oikWY~ik+drDscN-R=AF+V`+UKBe_Gk zezr*Kqoi7Mw0Q!p{*j5=ol0o69{*y<)?6ws9u?(lp7*>?gCBxVzmic6fT}*DF;7;{ z`4G*Kjb1K`WN|DLYa!>XW&9(3Z7zuZ8aYiwb)3B_RAF|$>X>&)u(a9DL{C*jy1yh3 zcj4mWBAYd!@Tbva54)n;461wB9{d~Wh^udsOSv8jP1yeyVk$AcKWCCm15tSusC^-O zemO+2jTJsbtnykB*A~3A`&ISKl=(Mawfb1|9q0LMk>uhk-)_J;&)dVl_4K(4;_89c z8?rLi;-|08bPv?lA$QC^eWTK@k)7^GDvskN+F-;J(0F~<80XUqc~RSna1O&Wwc)Yn z&|Yy^7V$jJo@SY#SMkAW#^HXwbj@?q6+A^-J2LY5^F2#_x<>7a`^s?4x>R2 zM=yz;7yBOly)ILzj8})~p!s*?0*G#6v>j~Uf+R`UxyCHm8CJcqT+59pq){F>j$=90 zkJdnmvq*~(A|MPEVSCZ7|1Ivb;-)V*^NOvXMZ5rwMHJ`zkFl|4YVm`j@54kS3D?1>o zGs%aa*+(CdE=kp{F&?1BWaNR!Ik4;9vTrBYxnt4NveBdHc$eon{Ix}2+ehHsYvJ1_ zvSH*t%Wue`(A_*b;XIzl(QI#gdp_HC9fUds!hX~k@5Or|GoY2&c^iMbi7h-!&c11^ zlg(g+xN)fIn~6)Odwq&M`vjGxc{#eUi{62_{}7+6g7$mrrTaCix>WYmYq9OlmmVCS z6z>+_DUa)@bH7@}o>aZYwc)t|#wHd;Z*8=4?bWs%(Me)B zt+Sif_>Ly2Wd==X`Y-X)xBTqIctbP1gBz^rHhSt?BYcQXznTB43!Qh^4o!!RN9Z|I z9|DO>VlSFKRnAN;y00Y(_@q3JkDLHDO_u7iY=oY_m1(Iv?q=4A$7-+dvnp| zKsMh5yKxOI^MOjW5A;0y1F|S@KPtm3^v8zW!)PR|DG>M~9?%J1xqYs6V^mbG!N zte|(;g;TBEr97LDqLcIGjhqCD50)j9A?Xgu9GkCtpd0k}a%OdA50tmVtHtT)cBFF~ zzk8AvUyH_67E9=7&dbqh1okRJiw5sR@HZw@{aqm=Zf5is)jQYY@}m&PTy)dYDE`y= z;tlw-Au2gf-`Tk^^;s<2PrTEXq`oNn35zcYBef%as`GH&KpVWwKQxeM;&}*Tnabq4 z(c=6Vx7w4Ad8>DTSw(n3z9((=*tco4b}tfW3JSUd(mF_LhZ7<`v+nIxH`Zo7H8r0e zaMTm<$tO_r_b^3$*497zmJGBj6Y1%v;M-qefp|``IdoDPUMnYucMYu45*`RX&z2sY z?bHhNc#@TF1q+Ogj+XD#kTgG!^mIL_=%$*c~ zgGluk%p_#wt*1-d+3Rrb$cs4ZD;D|hd_w2JMNi}PjeNA{@;uBiuFeo^Lg&1F^65Uq z^DWt^U$LDg$aCwakKuBS1d*c z-HqZ#ldq@22dA;+Z}hk<@BcoX ze|whAlqT`6rJwJzLo-Oi)6wy5Q06OS!%z6}PuL}4TqSryR~pk`R{hWJ-3(T5N|Rjy zO*D7sW1>Slti>T~@;tp5a*xW0FNS`b#o&)?X|@Ob^bn&Qi2B}QX-$W%=lT2=(e=MX zBTA9et?Am^V9cBB+P!&i%hM#@05k9HwN`XMO_Hxt9wQZ4r}gys9R7v7?Mq#hlwfy# z4I8~E&+A?qtG@HIzU2}FkMH5t8V5Co+Vj66XE~4H9$a}Kda-)H)@blNu9?>AN!vKd zv6udXBXzJ_AoF;yRVc51w7c&uRzH1q9(z4bGkk0R{)JVJK%W)uzzm2g7b$}}+nL+L zIIyD=9on+pAM(5%E9G<%fz#M3vFKjDnh)Wn7x^WdS3LWNwH}?hEz?moY`0A47k5dffjsS7GyP=!PM5*Fmcrd@kD?M)MKRx!+w4rwHDIq*SXd-GVTP-ejjbL8!~K(Mn~}=tabik zDcy$4K^eV>aR}IIo8pOh@1Gd^F(%{ZWEmX_xzp-o2jvJIi-w(Q)VEu3J#+&8)|Z z`KpRREg2O8ceC57qPeT{9kMP%1N~W2C&L$ok($tXHad_`X1cRer>h05?Y!3e6P4p@ zq1H!Z;=BS#42D;>LrYV<_x%(m?sMRYVV_wY7q{||>uoC)goa>N%M-9>9%mt+A zcvAlnSnx1^&)1pjt^P-J)}v%*O*>eXhyF(~qhH9hU+@_W2ZQh>jin@r|9Z`e{7bs zP1m!*rlZZ?bi&oKda*NN2i4yUgB+{L4Zac*I7Y{PL*@*kV-K(=-xDiM$aq( zpMEq9bcFo7822pH)u07jyMz5aIet7|HL)pPB)*?4zd$wr--%I)=VO=30EtA#MIYd2 zgPmxdMkshV-^82r!w@u4hc16kCQHcf+3CKOL<+vLLm!eNr?S=7io#zHeT6t`PF`(8 zTDv{EP1DjR=a!KKyJRkHv{&8H-Bt8fSrvm1WV-9WSXWPsk-UiOcn(h{H_nDQeiMz0 zM0SvS5%HDs@cMFHA1C9Wm(6Gu59v=V{@dAG-;xy<*wJz9_sOj7zUcKqD_<+Rh9p^K z*Nf?Ndx5AyAG`4zJ@A;=OP0O34SKthhHr1I;hfp$@&9o5euUKgm#^g$Cqj(j#q2}U zy#QHU%>v1);JHaw`DFNEqD}@Y=<}TXs-`TlS8)6xG29DD)Hbx!b&$g}nU>+?%_CxD zhhT>uyjn+jb?dRRZkC)1i|J1h#|5eaHi*+4$7$cmzFEf}o6BQ;AKXx!9-cs7u7$o-075`@ z;G-w;dINPMnzd*D6BSu5Ua%!okxZEA-cOV1tvvrwmB{Pv@hvHM2CQC*4ylA9n~CmE zqz7lfeRcD+Sb{}5jYhnc$D#x)YZsomjEA8F&D)TTG6JvNq#~?`sNHRLGRJ3s9iPPe z@coB81Q+l>T^PG4b_qM|XHv49j=-neu~VXpd9?Sq>a{#ed*JYO@Y>6IofR1I!psku z2q}COtk9W1?E1Vl_*J$H=^L#4Tj1gGd5!l2Pi%;Uokt=b<7K+QEQX7DJ;4*#m(OB6 zyzmW*xDr->8=Y;AR>R4EtM9owc2#tc-J2Numu&tox+u3IyC{dcjJQyIzqM21`r7mU zyz1-F{7BkzGRyh_ba)ap)J$Z1HEdLlUA&Ta{}(;Qs>}Q-E3fMT^U`M&eZ0*EuA7^| z()cgi#p~zimXq`KVAzvUTrKmxg;yr{W7>FrDb6{Em2!|o9Y(vo3n@*{HYZWqu;R)a z)rWLyLtcl5*0O^>TC2%}a7NkeNMm~CAWr!OZHD;CQ|9_3+vHkE?hBUjES{TJ>DqafOZ!?r83Q)ta8+3h@Cj~jDm{+BeGzKKM67gg^^DaA?Q^<>LA{2cRm+~+~u1L1** z{2xV0me-sj^fNnZozB*s)lUA(|L{EPX;}1Cm4NeOwWB|v)`ug#Nd;&?7T`%__~kq$ z6>y>~0QrM^vp2)Jjbsaikp^$=rEIJi8CBViP2?TzO6JVPU&E~7N#yDI?B<%eZy=t- z*-P|Kt*RQfADv$Vp6$wZdXdh5#Iug1+S4d=3@tT=2kA;0L3i z*`%-W^$#G)`p|`Y(A`=Yv44^bJ=wYs!qgLedTFGC2+*H=;CI`JC@pmv-?SoD=cH()xhKDz$jNhXBVjgywVDvw5DD z>WU!LAk~KBt4it)9>Oz?;l`ej;@{Tf61=WAB)YDT51+N?hm3TiF@Fo4H6n>O=j+?A zj;Yrte@aHiM2p4$7k@X}J6DI)x;ZietvTgagl4jp`a8Q!ZpsJNp;>NKHU^PYp{*i# zvKU%x?Ab!n_w#(k-OJEmah=X;qLgzWvaIp^1j%(#g;bH8I?8H$6P8)X%hHY2vrW9I zE_wC|duOcP@(KKNlQn1w;Z%f8=llFocJXLxq^JsEoCI5te%J}6s zR?$9@fqH5+AEv2ap&f1`yPMGMyKzTD5yIIX-|!6mDpoj2TujN}{6obZwZ2TQ20i zx;WZ{To@PK9@`V^9=$I&Bzu`$$-eMtcX?q4=-X#it8JHA{f0R630{N3?7MPW>$%52 zcsj<1vV`nk2(>gOFGF7Wm%NTum;5lr;7C7c1y6pvCyEr-eAgk(W+Slm-GWGh{hbV3gXQn@ins0zY zLI<;{Y_K0;kePY3D~pf}+eDIIDiYBR`u~F0wHv?K7{0I%_<*-Zo~CUsp=*|ra>JpA zNxU&f_%eSLEjVU3d-9HTVi|5Ena+`qeFqy(-zE{I4J_1Nx*{}YOZRp{$@}sJay%y$ zN#u6?LrYlft&Q{{+U8c{pH9ovHj0qB6zXX%5fd7fmt~9KjhD%@4g8_pd7~zvhf*>VL$y&&*3>s-&u=7guy>v#_hcXP&peKI3Nj5u zE{^ckj5p_@DtR}kSAUa^oz2F%4E6PZZ<<5VABtLr^N!y|5C0?mSGj)`8oDIeREDQ6 z_~~wBsr;)ZzMt4xTevKvcJ(Fxt~qiwZW2H3rI*MD(W3D2ORD?ovEdFxPv<{rA8SJM zpDRn?0dpG1`*4ue@r|sXCv~b^4>SI*4mU~)-vOhcbd@uW%EyZ8(i_RW=!Axx@5Lf#%Z?wHtuN!XyST-Q$dKI9Y(-wN!BaxJNV=#R$JHH23Tyls_oA+=h7ap z)AWaQY#E4C+QFBfvF)eel9^@}qk$&jxNq&pJLKfAvKwph3%2J!Ybr-M#Q!hlCHohD z48u=4wTh=jW!Tmw=gLEg7r|6>X#Sh=%653?4V^=_lWl3TeM96b9`Vf(*&RI3p{;0X zwo6MH4qbSB4p_0^L;n;Cc#pmr3_*lD9izJ|(rZW1n|xlj!i%)WM<``7%y|!A$|GJ6 z=SRN_F^)jXx08j#$=`YMmOk+5a9Z}W-ur^RzCw=N1k!Q_uWbZ|EdoWIXI4)`Chzf? z9*@*RO>pv>H{`ggI{|?>0eL^U0YGESw3E za`^k8jwYweL#fLvbGz8%YW+@XIuYitSbr39wkN=TqpXd||RnAGt{$}iL8Xq${cJ#r1Tx?3c^y}kv_`LlOstFiol^86!GyAL`37#TK~%wA02Chf~{c;YNbq84s?mSt2` zO#W@0`8|z)6%Fw!iM5faX z|JRbzFS0?u;<;%kv$aNUG!)R!=y&tX3{o{d148J}d)O3xwaoq^hv-eZ^+})jG5ZsA zoknfl`7F+$H%G`j9K|OcXLG$IQ|T*xgG=l6@UT6a7W*bvB7S|mt)6wI;yqQt4~vhD ztrQ1qr_a-E@daqBqrN=H(cLm~ek`oMyBp{N{yf%xcCsPjzd?ICFGXr?O(*Utv(v2z{Qnx zl=zIse_1^9S<&&sx@KSqIdbRX<;C)6}sAAq;%%NJQU!}+;C&z53)K0=;_Grk`;)|g#= z7;+9Z(fvu#;`zwIYW!aiZKg}bud(3!8^_<*iDv8>pBcYD_NHv|ubk-dfxL__#ph0m zoT-=Agxv3ugKV`z_VDd|N6qo~N&Y(#*dFz zL$+)rm8-J~uCdSkSd|N8YJ84%ABLks6!9$9VQKGnX7O$%-S?@r_>Rr6mb3^t5Lf%e z#dOCyD>09s=nZ52bBGSu$?vsZmR(IcXRL3HVaXno?GfVPN9|HL4S5eA zMbNGdpu&mC4AEwExnhz09LyO;%!gcwuI_G`+=Ry@BQZ$~2 z=O-T9U+mm0lKmb3oe=vkDRU`gEZuJp2ch*5X0j0vCupOPDW2n-3YC&QSPc)`*GD`a z$ImN`s=Bpr3`I19FF)5M`k~l+@-afpc1@%yJU&UJ`G8EO>8>_J3^0fGIz%S(kv3on zvzSC}*V5h(ll(JSNR#NE1@=3fu=*NJG@L$~Pm-SnVgAOpY-*%+t>YI^!8kko2~0hN z^}QQv`ktOEp8Z~bppsC<5FAw^`y=^rJX19vgYL$g(2{L51OloVHoEha!~fdcrm{D8=;H`;fj0hRLD7g$6EZ1ibBp?jP5&K47@fi7-ZtB_Glko zuPK`U0u)|0cBOO7YUn-l6!iItOpI-)X9KKw4QhSV=U-#-R5!l2;H@YdHS}aix%wt( zVhc;mSwG~&TF|@#;MFN6k?Ud`9fZmbMk>W%FlG`Owzj- ztUUqF+bADpF+cf-=rIMa?13NNqtBYKFhY-mv+!gm6tEwfILE9~&`fD~^Gz1{YHL3Q zr#}bzjHXexilHB38ErGy^Y{)rvNHOb<73g@vg{g>5H~<|O`*Xb?a2_H=_?@RBCfd$ zmk0EC4;k2vG;76!b3Uu}WWMTupoM*G(3}W<5|5m)564M}65elYcELA!xjEkn!_Bgz zvw39Z;GMTb_`k4Lf7y{8{1dxqxGPvncaj>9!YIE+DjUy8{IZipP$zZ;-%cwzCs)#^ zr^o&x&ELV}&DaLp4A>!BXoh^Kt_E7!v!iD4!zw)4u`CV%DlPT zD&Lh+Tzj(8tBGKDX|>N%d8B;}`Qe3nIK5+Ux7PcUy<~MjSeW?;`qK)deO8y3vK_u!Cid9~T|bbQspVP9uki=& z$#cO=vc;B!KKpR#1#)4E#y&?WC(zPpvh8j>+!D$P)%n{+CW^5+i}KrSMfWT5!9nX2 zvQ%5STTOSXEDPWODj!Kxb~UC>M!qZaF1+#?f5$;u=WQ|4XY>DWV0A8qoX7g_7kK7E z^SD^fZm_Z6BLzYxLFh);gFM`=T2$qj{a+CIjWu_r%$HBtFj*0`b9B6J7;A%aPK|Ao z4>BD3ddMf@(NEcwZ_qDqk^p^3g0tzUEDw5wzE7}We!{;atk28nW+2V@w`k#JnJI@w zR-fh{E8!fjgSuQT5xpHDzJ8M)+81a3$DeU8gxQ^Eqpe)~d(7u;(sjGfp2(E)y`ebx zCtUp{{PqXW&ExE%E#~mM^{XKp^Gy_yl?Bq0?c5VBJ)yJDr!3~Vq8Uywl>Ku9IdwHZ z;Xah}350r>U+O2;S9jipGmQRE68al@;SN5Mli;hRP)P?~zs9t>U3zjokQttN143LT zgJ21Zd8v7>L`|FJm>#z72W1c(<6$}#Z`6S{2jnRzl|2OtJxeCVspvA~Ts=lA9a7(W z84er?jjv=^?8DK;A&2@pA5@cdd@`QR!7jVW*R>GT9yaAZNPP?rx=1vkGK_PAojR4y zc}$j6bGfhIL4%{vOvvrH3U8h%Gb1YlY#I8VCepP|4#t@%yeSXa9r7Xu`{~PrJjys% zpp?(@+};llcfbMldC6|(v0Q}*YQv%fpn-Y(lV7n~w$Z#7(!E2;??6RW@|dHUvDbo& z%6o)Yx}mEcDHYlJm;;V(KYbey>)?`3XJu0KV(PRZN*b6FT4@i%-=lO5v|s!Rv8 zrnl~bqI;P^MRB47cIF3Icnuu_ISE*Fu4`{dw0TT&%hwvAeNG{rBtBP zMjx^tpM{)y%loNqhJ_HtZ=#eTU!jVK(A|w|Ek(?nfgDnV%!+iz@C}HZOPk8^r|N8!D|vl0-MqVVxkR=lGSs z=JhJhqgGAytI%A&W+l$RFHaix02*g3%W=AyP0i!lkl)(Zp0%)QmBoIG@X_w)5845l zgrD&25Pfxy8MmQpx>$u<;J3Cs`Dc-RN9f>PG}<4$+c}YrkVjUNCo$y8tdQe$61q5* z59lvz`i0Rx59Qtq@juR%8D~^)n&USRU$87T`%EE=tS+3<(obVFR+dL;z4?FY+uL}; zBCeG~r4BX3Wd(5jL7cUR4By0t{EF9lsZTGqcgt~G;KV|{(gOLfDWBWmZWE2*b~9*A z4>e+Gbbu@eTb+kVpj+(hLr}~MR%gEPZ$~YEy2@Ho=RK=7#oA3U>M7Q48ci?}bvSj? z-PXB7h%|=tDKD_{C5$oylZP=LgJ9Ph&k}Q352>e7U}>K@ja&#(kgv$(l|HdW?s14Q zmxKN5vqNgS(phrk5BlzO(qgzZo0zBR$^LuFD&7YF-oaZqJS`~rn!%Q}`+$Sr*Z z=A2}7qwM>wcCVFPZe!yaMzW0**JnFjFS)ta}>Ty?^ zX9d2(KWlyJOH}rP6?+inJmTt8y}Q(_EAiNS?)Q|}??#P-y*J5f4YT8&-96;qRiUq1 z;LULNg)q$P<};Qy9ctA>H<-7q`+KP36{C3B+Kxi$YQxR<1+R_s`vu>5-`!TZ->10y z4Y=Y7l)V-@+6~0fBYo>}6xqivbn;$XfBRX7 zq2>{~xn09U($n3av%arcfhp$oq*vOIezoWnJtoNDs{U(Z_QB`X*Oj~ZUJDYkkzKvs z{|{TW|Ev6M%yw$b4t1uv@%MGD(AjCI89eBY!6LuO4tDiknAJ!-HV!90ZRX*+-HpE; z4S10|)^weagLwz8>TiVo@}p{OBu#NzPv3tizauxAMGILd7qA4+HQp;-{|@hWvErAL zMb*iXxJck($Rt88Rgqa%$0tMOf68kINuHdjUwd~MX?4SX2j9poMj3j{-Dj+KyJ9cb z?C4Gxx>I$dyNnHgt@+%Smq1VWb`SrzLg`oeeW`Ujon>AECJMF9O^l_D$5m!^F8iPa zOBP^YNt(2%qIsdnIultQ5bTOy_*Jfa}<@`TO;lk}ke9TOWxpMWqp1R6h z`&s7~(fJ7LcmoN0h6vwQ*2iY{P4I@-bKfRbqn3HcoK?g}ViH?PDgt`(}g z_ZshFw$>Ehw$P2~Gai9W#uf5)wzG)8r@_ARdxJR@*o8!XZ0lgs@x1mAL%z>JlJD{C zeZ+E`3JX6;2R_0pJ|>?58O~*S#-~ExyT`qLpA4a=^BVFsxMYH>yyRQcyfWPTLukbp zXwXq~>~s8@uX%l(@AjoP^;q|8O#Z#Wu5u^8Ot3Snzz$VpG}MP?>+*-RhHIbjt%>l$ zXxDj+ZG8{B?0VL4OE|qTtDqUr{Z(ooT6pDZkM``(8+gbbG{W1xe`@Yes3ZcdG-aPY zU~J=D^EMv;@*-8L>sy4 zO-2(=N}ZOsn&;bvnfcuu@7e=hFLYjdEFUj<1pXLc^{24EHlezcQC$^u{ST@b?^8E= z^fs=y;q{HLin(329@rM0f4gYL}^gd#zPvbFJiaz$C!W7%?Uml+AzV#_un1h~X z_(mA@OrKtdc2cOP(5J#DcY9@?y&q|NZuvHGD!_DsV z9S_55zt`t!b1S5>2X^_LCG$O07`lwT;P)#Y;RLHCc`Dxvt?YE~ES@Nj--^O8!GcRe zFq_f!BJeCK7XU_;PnahbcM!n28XynLZDrMOozaMTW;3z%xV$5!J!n&%(jldKEfgG!j)0rUC6Sbu~SSLWCEs2az! z^75+=+0&LR?nIJbk(XIlk!{tX?PKP?$I9=|W3h<2g}k}YpRo}s8{|l1ziX1V6~vT7 zpN|M1X~?<%gJ&*8$fADAc&C=nH=_HlBCmsIICV-e^hQ z^&s^+`$h{t!AsYj9B!AFnjwEStZ|{$Dq@^LhDP{nBO)6?jyX9L#hpiUR5r4+tXUb< z7rIN9_5TI#5Ty2b#(RdO6NT<6EIKYz6yOhV(6+|9zTy z9kJS*(APRUyv^e`GMn5}H>lT0a`s4u=wg(*s z4Uj>Z1?cfGstm7&&mHhupxG0~@04|~gggD@=l|AWyH6kT{qRnp(!IWS%wwO|4x!bf z-U%A$VE%E`yW!LO^FN2I=Mm%F<6D3DIpEU={T%b(5%)Z9#ZK^hhWxCP?PTEOQ~jLe z|G1wLc`gh8MeSqEXNvnw={#qgj84M)#e6bQPTc#&aaKuOnaN9$xGNR$+3;TI=NQLF z>AYN#px~}a|3_qJo$zWHN2mu3G+E%?&`Ia-{O>>MvhewEGF!M#(&$b{Z^e8++%NQL z%FxTleJ1d8)K7RPTr1FX!e@#ZM=^97D7%E8@LK59bBb5PCr&o@B=46q%ky{7;fj>& zg^`CVM%_2?OVa1!9^sV&vn=!za&^M>Ps-C;G!_wh_*)>BfGV`Gy{EW%8pt&o=`?3>p75{l3Ic4I`~#oZ)1`AZcp5c91)DjkBKD zn)rK}dtc~pP4866e>YsGy75$Z=W^~K=H?z1^WzIr?kuZR)()QGy>s*TuI9BWt{v_k zFlzXIbhcUn%Cb#Q#6zlHLh)kU;%` z9|Eo|?h~ij!4lRXP)Ko~F5&8@T9e?5y}}Q$H zIacRXTP{a;DI-@L!imG+ab zN8Rrq{*k}rES7Zlak50pn?rF_G2Q91C1{@@C2GmO588Dvsd&s@tRi*4qvhw5 zd>Lu{HmN_A_TNj_gr2lJz4M`aehH_CzOR4yUdrf`R%fT|#^W^G3D^JKXDX5^pXl@W zg_y`ux}mtvVdceY+tSEETXwbb8{8wJ*Uw(Ha$m}gnCtJEz87R;4ey3WAtZQ!mJ7Oa zsVv&pT|HFY?t|hgs5{tX-fxga>x^v;eRQr9-2-ifE*Gn;#6D|&w%Hc7Tb1mUvvcj; zYTw-^!c|iq>=kO+{^sFYWkq+(#(X@#qVw!}=+jt07w(WBei#xyW>isq62pE!Vnh|y zZ-h>H|IwHe+*=mecX8u}{`Ju7HNEr0V4ycMg56?{d#_ zsHcLA%+L#JwpsiyzhaxsqNp6Fto4bTK@HKmVfqJdGvoDY|C74b1T6S3?D!)MvdNwQ zG^&f-W4HP2f#}mZ0?)JyTjhqg&|&Ufdv>bchd&zUZ@w2Q7C*8t4Q0~Qu{!reW?8cm zu+?6E!xicjewOL5j31^j`-;jo6cefjPV}0~agUz>E|9M9Jf$Z!zL}O~< zt{2%UqwV8H*_|0H^m+C{w*CY=%ID+E@RwaDSgKtj>Ffws9BB0?vv{YYp$kxHYdtrE z)Egs;ey{V!|7E9iiOfO6&2oM9a(N}UGV*nHq8yaa-*J>Yn;x*d{?3q0H62S@@EVQH z&M~tG^&HxsJsxRE3*8rK?aE8h^3Ytf=>4pQ8X?kXjAuo2EUn!-*v;S>4V~+DM(VRQ zDq1bQ+eEIuwre%@x;PDr?PyQlkF3hh_FA9Z$2vY9Wa+HJW!Ipo$DGP|4y->^U(Smo zi?Y34=me1Wa9mptO`p>Bxkz+WwyJELV$n>tMQ(buARCaz)jTov(0NbPlM23aQsg9a zE)rRp{W*9xGa=sH*a{tx>oi9E@oQMSF3aXuTB4kleq8^XVQ6bZq*Znx&*+|Pf)_0j zDPivaGnz9aJ#l6Vjr@|W=k&*0R0`b`{m!nvjRMN(>3T4daZ1@bC#jE+EfVbNP;oWE zS;`;FO#BahK9qY(FTvr~DtNO7v67x4x8GwcuV*8S%EPgVq96cd%Y!AuA9%7qi2 zzp)NOBRjK=qbve6yd?KB?KFbSnIEZ@+pd13ZEj1XA8UM)oS1>xYeGJz=<-#ObE7k} ze>x}WL3I8>ww@Tq``OQO7e=4aMf;W9Y_H#Dm77JP(SdC8meJSLzl@RxQJ#JEAz8IV zc7I!(R-8=T9?52}FyA`(;<%NVZC;XsR9A`uKRz?n|7GD;!{%M4ZV@8^6}kFKDmI$s>&wS4=8D&_6NBA$3B58vM&O@zV3Iu^XeBq=ku7w$aJe zsD-Nc(zu~&^&Im2VbbVHeu{p1Jo+cCTZep{s5?3P_G@7-j;vkJY*aVqaf$l9GCeirn0M?Rj9-D#LH9inc9!Tg6|UQ3=c)?X<}AFS`5LdS>i2$9pAgOuoXl$a3LcnGZf_DPpXYvOsN=|p z?Y<-*;Cpoh&75s{FkQw8P|d`HFLGY-Ntv8eI^*i9$7RaM=P1P|_>*eb`{ebGBxe@# z&|YuN3Lyzqelq_5)i38Pr^r-Sx0#AnbAC~CJ>x!%Z-{>;x3g!wYP@m0OMGtprudv# zce(f5WKGr;#hJuAe1?v3MRM;OTOT!9yXBNF(^uek;~VU3!}rzC{ou8onQ}6$uCa#C zsx~<;|EefCbcW2CQoemaEOe_{hh5q3@Xrwvd=7~>JDW83GU)bKp3zdd66Wy_q!msH zsEax)+wsE49cr4^$#EDUcXO3&vPrR}PWhSY)S~ZofmA`k7dQ2&!<=sa@on_m{KWJJ&(BVAyKxYsW(WUESdegtoTrk@r5YOSZ7r& zidC`eSH>&G&vz=^u6Wl(`NaD80;iT1##h9L#2d!D#Wu>^J4=?pKJrKJ={P38Mtf#WOaE%aIBRrwtqVZg1r2iA=46^s0YmVa8#?=(50b(}+e zO?9f;wQ@=a4ynXt^^dcweU74@CT9(-)yZZBN2dnrrKCH&u`z9%{9mafGC3+X0 z;)GaT3H8uh<^P;+Wm}TZzlp1Dbgv8e!29CxL1K(=kxe?<$|w9PR@o}Ij(-r(#%m?A z@ybrY>z{Z!@q+WnE={E5_&ye!E@pm{T=;$L4?QI1QCukB`|b3^^b_e)#(F`jPHL3% zha=8}Je z{F68(S&*oas1|RkGt^>LCHvrZr+Ud6?wMKYWbtoPFQ;Bf4RNC0U8x3oMQ$v-AT>XA zZTel5vO8ThGg0D?aU)D4{G@Gx|L z5s9&%w$Kw;1Y*9k$se-PZKIpy(KX|)SrO|Ke?Iojms(46v6z`7x(gn-fSsOV3ExNWY(|o!V4bHq|jTG1V?rS(nReojunK zb(~1wCjaJGrh=U4)v{Dxq><(1l#fQ`x0W>Dm|ggAYT=m!XnPxrOriw$Okb)se-l z_r0R%Z|dv5RF=pf_`k26-b5M~lPmm)n)>$6aXS#dC9yqmUvhKuhvZ$!6^UWS-PpN% zb(1X;`(r;v&lk~N0<90tP0fDibnrpxQ_^3i+N4fO{ZP2cxs#Wtx}>_M8aZ`xZtBeR z==AB%Tq{s%f3pm_nQ9$Z8gs9_7x__|b{ubWDISKsymCLnDX&7`cSAu7SZN)_Tza9U z@@$+O{LzoKGLyF63RiYh8{AQ*Ujvo&H^)EK?ef*cY0gOfI{AFEMlzddntUqxNU~gV zRAMWsGCsCOM$4n@m}a>bNsW6lYts$W!<;MmVd0O3RZ{n-Zb>ys9WFdk*e7*%dQ$p8 zI*UH0>i4>e94;-&b2~YFfmq(dVxnE(rW~wsIDb~lPa-*iOONNmDP>LpB9%*t^{nAV z90N7ZQTx`1T{b(?NPgL4DxA~Nma!jn>3jhX{+4($`Em05SszLg7n(i_^Qo;#=LAsh+zt?3vt&oc~Qs(@f zsALNb5^8&%!*6BqVhfo48gaN1*7kM?sR51BfnTD54v(cF%Ej>9ul3E zTPCBeOJwJ zwKJ2_=ciYb(}S(@ZK)fq{6ndyQ?pb1Qx#~QY3a@BDkS^onbJ<|drI!*PuX&`VQ2n@ z>-kr1l+D@!uQU}@h;kFrQNLnKRS_;yfK1*g;g7ww`? z@ZnhV_{w;T#OA~e$<@hYEWugsxVbIzvj!W`Q5VqevpHB8ERQ1SGPEn(GXTW z1$HW9{hN6-6)SCPP4~m-XR&79&&ORSi*%ntZa2ikRZ!5oEUjDghdGm`*%G@jJ~Vzf z{$S!ibh0A(XY%wS#ft27KIcF4f=l}kdX)8nh0|+nFE5>o&UoiH+ z>H}-?L0lsu`~x3Qby8+6I(mcV58WY8W?Njys%y$3TdAJvDfJ7@ofUl~c4d4NGa(*ncD}8(VpVacy zENlKW$=x_rA7^$Y5uZsdN)@D9(fV`K8=P_4hJIfx*RzIf#SswSJI23N1w!18D?B7JD^9UnfE6ShK$E>WgVs574SLH`} z1)H@3y_3d~)r@(ZY{`&`demB958?kAZR3pY2%PtBJQcq=@o}P3^6}&^PTKtkwY-EY z?@V5!)6F}u)H3}!o{FuI1NH#>zgKRTo)h2bwDk>~_a|!ZmFk@8V7+hhyD^R5fMj_- z^<(N}C-dH#ejJkBPbzlHTQ%k6y7pC_@r2CJX|i7vsO~P5{TT^U(nzbL=MX`yZ)fjf zanIyA`9UsRQ`HGGqla|vo*FxbMxKrDj9=zN-IGbY*OOl*m-{^`*(ceBAor7bfqu$_tV+lVhy()%Y=;SeEFa!}WvlLvqnq z=m7P!)xH&OeOT|u!A_%oAYB>`oJ{&%k2Y#KzqeQ_mMWU6=xpq}Q{$ZMn{=l0!_K?? zlQhKSAZ5z@; zD>Df-^|z_ioR2SKR{J3sJe(uf$kn?Td+2T%@|Qnj>-?^^w4!YB{yGyZiTQxQU_8eQ1?0MdN}tj(y%;hxQF$ASB`%fw%lFn zT9%WBXOe*ZdHZ+p7zDg>1)s#-*86!Q59bh8m8pEIT>bGX<4$8y4^&WJs{?v* zr$(F6sNzeR04GP8HsnF3YcQH=B4KN;#e>tN!3_dEDzs@Kfo^ zYvnR66iGOgAAN`|HidA*xc~R%669JtaxuW@k*7L=fmC)vOzwzlYip=hE{!|obNyM4}C&)L8D9sXguWv z>?M%kg!CBrWGajDduNFsWmlHx3u(hno5mszyjjC{J6iRZ`D&u{&NcF2mZkl3p&aV(EQ;qD!+SvwL;mm`!55!nzsBS{SYh zVF>PhKo7iUy*`(BXPLT>y_uq}<7daqvDu5ov-)>`Qsg{+{2e150HW95KrHPm|BSSdG~vNPDvSpnG&pIRhnSGN)MCdZ?nN zdWb518t{y@GUoOy&5m;QyOB-L>W{pHZSf1wPdT;4O(F4~?6a4Re-7{GF4oYQFiKY( z8@lf9R;_uB?k>Yo&Nj85C3S?m!VHGTUe`}-zS`&s`X=||)9Gy7wdwgoFyvVI{Pk5P zY=(0Bz&oLG?qhjZ14xF}=%+I8M_iTXYMOizX}3lV(Z{Nvr{?=qPcg6ADlL}ht5!qT z>PdWBU--oLst-1)a{W!6N~rD%RXmNLt!A)ePjd)e2X^pio@wy0iGpwma`>}=>T?<#{+s-j5Wp?>WAO) zc+Gr5rQCzj+tK)CYD_~<{*cWc&c&G^JGMLja6PMku1AO-ABUSmrq6QL?mJ|~EEdZ` zQMRf4;LkwSJ>?U&w>x$9V=T`n66(X-i36;5?GPb94@$fN_4k+Y{GpnglH`5pSbv#EYc+kH__NOUxSBsWWV7$56;?s< zlgZlK{O-)FFf^YDRvNY_!_QutZ@D(vvloTNdGSM*-jAMbB=0^ohjIKlH_*P<8gprx zlsDvN8TUnO*Fx1Q%~-{u|7z%qRNAT^%hp#H__R@91%VAS@(+#s7C!oBl9;NuT@=pk$2ejHnt6l*M& z5Hg(p5g%*EqkapjTw}c}`Aj|Dmye?Cm&PwEtWNY$hUWHQX z@XPMPC%}t);>u>Ev1*~>#>(l zPbc&4VqFfi@;&{njSoUUmikutD4O{Km!2h;bt`LbsL?+prqBtetiqw4jCdG2-zVO@ zT|BiNSu?^I-{rx67GC(qh)d~C{{F};!C*k20KKY>4?uM#gg)F+Gl1os= zELyL1K40b#=~diHR}hCeVqZfpB+VPgMCZ8ZXZ>m$lv> zUVMQ7sC6z}CSt9j7%v)27Qr$#(s)i+q3rFMHKf699QE#xaXgx~(e z%Nc7vg>OD4dt!hn({pst^JqGpB>y$*^HGxVS{WnDd~%Qd91owZlv`X1ZH7MV-?NvN z`%V)vr}B8Kn-PV~ucqYxRe0}PQg9Gibhb4Q^(g&O_ch)<)3g5U*^o(8g%4znkrldD zmJEB*owD@ECLZt8%=BlYe=fiNX`}vKmHW$9`>nj5Ut*j+-9L=$3!fQ;7n+&DySVvb zGpfYXzaQ1bBJ;(xD#-(=z(Vb$a$qK%`+=z4ND}W})X)fh_l0;uj|BM`Vq!m$|I55u z(|)|chj-NO+$vI3o^)@^7HDP04b9{fyBKm;E|JS}m@LTJjlcPc&y!1WgzXSI$A#?M zkYgEVN&JELcF+Y|QPL;mPa3Z1PkP>irr(g)G8UDGU71biEjPbzZ0VnQ^N!h(kJXR8 zWcANNJK?00!}hfXY4RQ^IKaHR8|x7Ng$kH2`2Hr7lB02K7h|8Vj_5Jps*U5T`dtgc zdm0a9jJPf@a%C3TeXd**MSj7*Q^HPGw=$>dr?wuognZWb(CQ9U|CmVi2$Hjn-Rg@= z{)guKqxMeTd){u%AZKoq<1x%W&GyZIMKw3$uaHw!0j2jhf~LkXNR;why&8tvkvCbd zr}M$ROX|D;cZM_EUeDv_1-P`e8TT@m4xTl%{#~qoDSLUT88x$G;oQW%sG}wc`a5df z;;tL5-%0YJ-a`pDqUw9enNcXZ73tjtwa+0J2U_|1u-6wp^}6@^q4ghSZu}{>x`bv( z>*Dm7@mwT->NKcwG2Qc>)t!{DNt(`XEu(Tcob@(>&vym7nP&Wpt^A9$K?`%Nk8hvF z84b|#6+Cqp(>&)`{RDe8bVUj|YI9I$h-2?Gp62lQi^dQl!K1}@Zuj4-V#Gtp$jF)b?(musWk1y!Wv#j^I`eF<*f}JErsD|uol|p?%A1Lf% zn58i-vjFd14_6i8S!oQ7kHu?M;j$aWw1?33Gx8M(-`JNdifvlzK!B-5)YF z%D~$9=?OMU?|~cCHDAKde;Gu0JIvBBx|?2lolj*f0}e-2@7foE6Iowd{f2C_x-5z^Ivd^uZ-1{U@OPXy zgg#lq+6fgYBgv!+BCJ2T$08or2UrPzXHxo@uVLl%%5>3(y|ex{z4Vd0ix>Zn%q>o% zTBk<&4?f!ZYJ?}TFwcRZuI9N98D2A>qZQClPtoxUVTKMQ&fTi#$!lWy zOZb!PWVeZ04pIR=hPA#_O?&IizkJ^(iTORnTI-=&ymjW0OiZ2f3p^)7c_8+RTyB6_ zo)cXPoosqT_xH0?_KE8!^*y*N{$~8Q_`3LVeLMX#QN9?z+1!(Ql)2^gkT* z4(ap^c@{DNc8swpWdQO`Ak82UHU@fs*x#{d5!1n)^tzag?3KhD`l*U)0Nr6O~k3s)YtB9 zR@XRvyynZAxK4kFA<;X;%U8!w@Ebf9e<1#CynW)e#Qlk?i81kJV&zxFZ;6+WPmJwh zv0tvoOeDGp|L*6j|ArsA5dSU2!7tG$dwIt%rhj8%W8sAHH%Xy;*h9ag(3P3enf~ef z(ks%hXDVku&df-6=4CyYI!FDyI{I|A^b!8yy1XEt<-20M&LVE>dOIRl;^J%h@W#j5 z7|kE7>}TUG#czL4T%A}Hzd`>vasU7Kk zDD#taCiPEhcRHq@QGJO0K=y4CE%b={FR~;0wiwF=cDy(*%z}8`L?03SH+j2mO012K z^IDtuMLf{=>J#w|Iq(q;xst32CvII2>s_9YlU25shh^bDM5hJx`IU<0Q&_Nv=!~0< zyEtp9ckVM?#$Fbc8N~PdcB)DGBa!o#>6L|V73?ionCgTcn`G*zFOs=%wZ01d#nIm) z5kH~Tx{%d1MJHZ~e;)raKGvuv@GY)RR88(pR7gyT564T%M1T1+pGNPslI_@bi(uL% ztlTj=27IAU&mGoPrX?+spmnc?a&|?!=sNy23-*%e4rBh3%{r8>e~I0^Ti(pk)UBzF zg{=$66^=_SOW&92k#15LD`-+sFLillF2Bc(nZD_QR3<$?+lYO9srv8Vt>{hBy8J2& zWEfm7s9{~vWiZ)Cc&!@G&Kml6Ny1gQ;zr-kj+vdU^tx8hvq^DJd4eP5Ufl8P2nBtM5P`-OX;+ zlm0$Ebe{3;c}_E0!khgr?6AR%?#>Pu-(Qvbqi}CQgMu+92A)`S;#}ZQL(M^DTA-$1-kog9jOkBk~JpDV|->OJFl*P_IB3pNqU6-!shS7!#+XG ze|2oK9H#bqfNvtAWYLPF9BO7_XP!Be6{) zUb|wuqq9Y_XTo~3c&Vyqe?h6w6qYMY7LF)rTrluN^%KvWxVmswdM4l7J?RmJmls~0 zek7NSogSYYD;YgoCgID`x$&R7FHz2@pZRH4c8}bNfb#mk9UmC zLWvKF{0xoNh>puO%f2Q1tT0mz=4r0a*J)zJOVVBSpInh`&u_6V(ktI*dH|U)Io374 zQQp-V@&Cof^9`NQm0>+3dsVJvc8|!~3K8*Lg*^)&D42Jm#EIkojXklkU{LyG5w+Jd zgHvk?A4xrtEgfw{UR8^&Vq2__l#CCTMRIW>8~ZQzzxcI!Bnl>-Sbd^WK|D1n zeFxuQ=gfa1o-e11-dtz6`e~EsVdq!?oMdx_+&i7SHw2zdHI0MgTAos`JzbUqmv^|b1!B#rw^y=$x?YWRjIH{!M`WU6?{_g zWWgT=-xY37PtKl~8P`N_{16UJMDWdSsMQ< zQa3p#{#oq7c%|fRMb0d8Rw5UDA<{rD$NG5J_&3p~a-U}kt!Zab(lMDx=6#t1_mD@e zMXJZ6m}jzQ%g}oY&iz>w^xbG}@rs9O%FE)n81*~Q`}h2YJ995(2WP%czat`5Gd&as zjw#rB;>#1w3mz`qpE?~^G|!Go?=9SxI+C88J4^S@>9Lu1s05p%EeZUwI8BlGZd$lQ z;^JiI*B9P}FMSs07QzK!e`1AaiHDV_enYWK+8Yo$8M zFqxg6M01`)Klc$U>cLMtgvWM%^m@qn4cUZ68MagCCfzMJHIj(c6$!f}-W|mj#E!%( zCVM3Zk!^1#HYNHbPbsoBxdBc87ApohmQD=D2aiQ6<*v);O&-zv^-_`d*R0hbry#e{ zKeb(ERCb>X_fL5%56V#3rRV8U$YhHc#QoO&Ig)2Ybd4&M-X5Jvj@{~@I$GMS+pVEqQjD=3;Oq!krXby$wx>eyBS4yl*j7#oM9v69k zTBgPo$(qTN#jv-FuC9zPijA{YgL$*>mxn!t75hB-_pNU4?}^q8W(lY((6=?%s)tZ` zKk<;8V57PuLUHQtqXAjM~XCO<_nF_nwt)6eAN`jS_& zq^QnP_Qn~xub{YV&36t9_(~K!Km@Qa>|QmqTkcE)GU?j%8_@labk@prG4ak}dcggX zX)nudu~BF~efWCc!3z9^TmS@L+|3AwqYMctdo;W;cW znd47d$KTQ(+8NBAoaFg!ytpBLaY+1sU$&%orn2ciV#`&9;P#7oox}5W5ZY;!jk4ko z=W~oZWa?z9XXvZj=;+Y9^J)Q5SwaCoHB1kpa z#1rG!K%Iq&vy!cnLvZ4{{i@oAn92EOs1TuglleoJVdbI=s> z8Rx>kgSLB!bWg(F+tP(BscTsMlVs&p5+$x{#MOCS{(&0fH1&-T!sUq<6B`qijrjo> zZ@y9gCIj>FWQF7;nLBUC+mW{`oex|#_A2}~mKQ9+FSmgg=ykSnccac^zZE?nV?|y< zFor=gT ziLYHMQC5XSMpnue8VbqCN|KC{y(uGv63R|iDMgW$l?I{Ad(S!d|NNZa|8X97pL@>d z^ZvZ&>pecV$<^PlV)h@^4|8G*V&AY|#rXzZWx}V(6yJgWf5m+tL76_2x1JoiQ8isY z`RSX~1Z>qVT{g=Tsx613@K-7q2V^OlWpi4`v^;5_CvQscAllnyB4DM+c2z28#h~xO zM>@h@?v^v%F81jqKJMdlLwDKdq1E%_gv0XK-*{|t^Lbyf#{KFcO2zM?HOl&2(5i3q z`Bu{SZ|r33n4EkGSj8+T&(-*CCK!%aZSo@g?=cv8InhxWwO!|!CbGy6#P4UJT@TaTvR|kD32cd1Ri%yH{9QZ;E_!zXS zvJQ>=#2`7$#@`2pm#VW@dbfl@byu&}lN`Pb-RVpJ4U?n) z+SI&4_-T~$x}e&>Gt{vdzBucS(RHeJ-saiYrR}z-Ua{`pwDQih@6y(#ElYckXId)l zDv$aNH4U58G*mXPa{^rNFSvAHSX5E*{}y_=vy4y#va=DQuo$&F!|EEzu;1yX&<*r! zn4~_MKh=VUs7He}qz~%j)@Sqr^yjtANIqwd{Vj4HXRY@cm4aW$hg}dAb;W;qAh+L$ z(VOyU{uKkx;2*S3{V;X48vb3WzouSDjilvH%b9jX*Te|EPFm_uGVQOEzl0f*T}{V3 z06!SVUW`bhroX`W`q3a^iha1b<1R?u37s+%aQ3UJ16sIE@^&3Ex$*S@(amJl=X2Cw zZex?S#kcr;03vk)mXfNTun-BT4Y?eT0_Rl)Hk5-|3j3-HE!_Z3tIx9j%!g{IKjj-8 zDutkD%~Cr-Rx0z^v!vc+y{FU#435^~8UChfU^ri>CLHJ&$nRXWOC6xEmFeR0G8Q+J z$#wFt?I8Kp;o?nM%_rgEH)S4GVKEp2*hqa(M;zZ%{X#934f$x2TUBCIRku?aK3Ls+ z`=`~`^;MNI-ra(`Wgm0F8u~)M4$=CR;5YN(<$1*0Z@O>fR}(nvM|-Hb9ENZE(hyzw zQGIYk6Zbs-r@P`Kcfs^zSMH9a;f{bIE{3awJD8e5RNC_28mbK{DDM&`gXDm!@3V)G zMYeBy*S=8lR(zhc%->ApUPB9p+qtIUq;OB``|yPkR@#>Y_jW$xX`rQ|phM~z(&Ql@ zWC6yoOX>2HjUhT?;Nx2$9ED-fA*OpDJ-@Suz3y;X8JQ#EpGAwmr%t3X9IFs)HC12L zK@rvG(7`GC9|p-Pz2@8{Jftz5bfKsU|xFv4}pJ{HA25sM^pf8g0Sy|UGH#S;b!c&Wi9%94B?r) za8DMAHtDp&5_k13vjX{)w|ZRb&vuw6UQiH%uiP?x#^Q z8(&)MCKV=Y)oCopC10vl+R2aJ$Ril-SwrP`yIW&>>u$jRc*J+@oW*cxU0dreFAH@|0m3SzeqKQSGoF55EURc~LGgOtfro#RbgUP2<%>)V{`j_aA$Hjo(_s zo~lFHo+hzR+VzBF{%)fja>yy)Dd*D{_xAQK?PX*}(;#2V&#ZvEt)q=5%5}X>I!B}Z zQmAq-9%xr6UbvHbw9n;9Y-gF%@^nsTxu!7DZ=Mi+$t;*NSE-TsvGC35NyzBkyc;E21hw=Ile=mn49^%QqW94CLUYH?xKlwcj zcj|}!zxt>p+c-syK|80>!~W~2YaT8FDlB)OX@z-`oxB~bwhb<6?NK!IN0lT?$iZKU zU5Uqe=Py`o4g7SyXIx1%a2^k<>ODvwKPwMAURU;3l@wj@K$=%>AmSX4dY|IcuHLsX z`RJ|}@^&=rh^}*R!AhDXJAUioFKz6lh%DH781#CQdOwLu!AXxezee_2Ktx#0^ZVk; zJNV)Ez@O8SmNaLzE4Ur)yl!{a&Cm}Hm38;tJzWvcFkW4C!n>=zvtpONE5 zax8;MxJUkUuTj`tJjDLT`4`Je9Gf4D6AKm4xZ?xkpJh|TFv7Ah9 zQTAkyI+!ryezKKikpU0WV-Cple$MBM>(_chCrCfC`yEY~+lgi1h_A&Phf%PguJ=jy z*q?U!0S%VRdJV?;JLE?ed0q?q8f5*8b^M$~lNEltLWWPuZ7lHlu*#uYWZ*va5@}ZY z8O;}R)1gP%+g=M^%bwXHdWb{T|r;r_pJ zDSJI{6Fqqs{?8}YU&&K>fhNCAUCJpD?G!fdC99mM*5L(N;NX!Cm5p62TX};D;`g0W z+>T#|@8-3F&_Qt7Z^A^PDx|6d`D<*gmr$r6s?Ngw!%*{1yLp}d=#k7-*Kj(uXrHHO z^ZclPN*zO8l75>ij|DPFH`~Pk{Im-7n&7Z1&NWQ$Sc#{?-9isS?Thm%mdlzqCQ1KT z-+CO;83!M*#yK8e+2tna^m->W&B=V@*^l*-`!~ibxGKt z=+Gp&kG7~D=214LH>RR^FRKXs5oPH0k4SM78Zft26=8h`_|HSMNvP|;*WbcqnsV^` zJ4ny1xF<~8YCyBrvD@ESofY<&6{=BCy+)YVJcdP`;W3e4(HGCZLaz+;-E{K0Q$5CB ztLo>OqwVQBG`|j?g-V%IP9S2PW31~zlnHY_>(KFU&@^Lc#JA~nw-8$WbozW7*?2+q z+P!Gq#I6Q7lhX7_ntFkH$*S|R_7tYARIraic6J75WKpScmd>dlmJV~vvg=|DGlDj% z5BLh_?MK-67g%9Cr!q+O#x%C~RT{Cgo%OT&C)v8XsF{zH)bKjhoJ}?~ z4^?3oX}T~OEt|hwRQs@rM%l?irl<@1l|Gt@$A@{&MLe^Jg$k1x``g22zTI*(9?P?s zNm^&%nzOhg&Dn1B>@csViWNVM61&yMw8RO6aCxVsXYv$D{0xteK;6guCCn~9!AqEL zN9|GiUEj6wT|+i0NJInAYj59KNJ^NRSJm%|@@$Tyafo8Sv!0)QKgWA-LbJ6f8>%43 z;@!Z(;nuDB_VHR$Zl`$ede56k^USgKFtP1l8X|$`!--bK1D(+KQEP3_X3j(1$*8)? z8qb*@ypcb--sjb5eHEwdz$>rN{sU#mhdZC(!<6?bMNv78P5KMEvdap>U0GB4FO$)2 z82i%JqcutFt{$x(UHu4jCisw*#fi1q{;P?q{O0C7m?r*Oi*A}v3+-`Uze6QMFF`~0 za|17HsWn_lsia2ZepAA0xoIohwmTL|+27X=Doh)xO&G0yBUfZG>W)PFnK&aIWx@>5 zF*MlYnd$uVQ983fJu67KlCYF)XRUJTd?;*Mp~=a zSm5_pboXC|EQb4Wa=7d36s#*ZKjc!PD=S)!z8&lA9yYE2khucU=o6~8v!&iDZXXC= z8!4Mx)l7lIGP);I?@C?l@9m;(L_~%3Dm061#g+e||42UQZSbdAaGb;D*)NgLyAfh` z$%OkEkjHPWBbz9*hnssV%XZGyzq%Clu8Ax^@%=P7!{70`Zq}TE(xX%qY!nO6WYap6 z-CZdu&TC?%kSg*Y^(0(YsqwEHp1za4y*+KI-ie6HlF{yjE|)r9PICcF{Rc761CbAL z+&rFp9ra%&%sr@UD*rKEV(nFAWQx8I#M|p~JK=_jigGbck~i4@D@H7zSRkX*RCoS9 z&)))#_#76p!Cuyyj!;|`dUo}~%~`n0&iHPqxEB&x;HTTowAkQ`KY~J~qv1p-YH=TfMC*msEq?{ zlbMl5?ks%IjUFH2ya#2Aj_CyIthc_p{8Ubvm^Y#WMDC@MH<#y({4PeU$Ez8}8xPa+ zi^ybt>4bJC@{`bRy!H0tre@@^BVT77sh$goOwBxmqBRn;ph?s8E58vh6EKqaNV&Nh zv3t>PrHK7^l|-dbw}cF8vB+{Bz!_6tMnvkm4R5+=y8}+lnK}jMT~qC{)vB{P<2|@> zK=fCVR83v`TO!=Y+>n$dC7}2R64ltbnKa4mc<~@q|8|xoh4u>fY96QAHqwsa#-kkS zIp1)D%|nTI^^dhuE%H>Xnls)9W%^HF)8LF5vDPx^Q(+5-pf+>jugP|=NU0w=V|x0H zG9G`6vF}8~=Bb<1x>PV9j4ew^?z*`<`Uk3YY{mJaIJ9#l)Q#S)5t0mnAB)n?`23 zUGe6~Y<6ml?t|~dhXqmw%Z}fZI0@1JD)x=;yGJte>Z7U{pI{P0rHu9&4P%|-kC?;p zJw&%1N&hVKWJ)u4I-eG6<$+n{fUuM&BVAI}I7M&K>TiOOmD`^(2dKH z+7OuAB2itrli1@~H1!O2?NSn9nLv(i6l;h1EgK@6A~PWWztHWaoLAS#Nqqch<{Fr2 ziFn7@INh&3Wg7ay!t2ED&)A#3JbiOUJ{jny^12H`7g+rH%u}lP|MQAvbanNDsH8`- zrshvAocgJ`B?VH;>k@t++Po#&hz^==BvUi@Q?z%>&stR#&+tfp=0`L^yFW-p7D&l( zGls6F3~^s)T^Pr0bX4x>Z}I|b-K({YjmocPD@>j}#v+bS6pMeJF)6)y`pooaGaiH^ zzY+U3V{Cd`9rn-1cHn?Qi6v%4{38$bG2VVOWiwRvoLax z{cEG-um?gnU!oz)xM8d%+c%3{4)Nh)k-||{VI}MJoGi=hVx2xPqTWuoo!j(og5l*! z{TTjr$qCejjK#^{6cZ<|(WyJ3-Engvj$Hfw+Klv7>aI>^+>_Be{iF1^GTx63h0{JQ zw=*KXJF(pU2hhpAWrN?1KFZEaGDToRq9e52b$Wq>It6 zwWC?1rBXi9&GwM$g&$e$$@)x&!~X7c!}0HtUG6x1A~Hs1vTRfy6^H*VBm1<<#}d(Z z@IW=OQCnyKS3DYDm9aT}pGsn&(>ijQ#O2LHg#z2s2tH#~Y-a zi|mg6q#LgeKlo;p?US0}hOG>8)6|@S7u1LR9N89mP6n-d>Sxi9BNOSBHe~pij{?bC z>Mpa@h4~X3#5qU!__O&-@5vS34jEm>hDBiGkE)+}LVZm2=w5mvE1UYA{*#9jh2pzn z?`HfjV`BQ#>D$r|rq7hkdm&?7dam^QWvaiVHzS!N)k%zr%}z8+`5CuYiTo)yI7bK2 zb!<$kTC9!csVrfwr@8xai|Iw1qfez?Qccr4GC?$T#N@3dGE?c+eMpWVBR(v#T3nO~ z`;k{?+2*92i4-v{q@=FO8T9hAsZXV5Po3k8x<-qcd2`BqwqA*KPIy^7fBgB_>GZzW z?zndC>csR*8DHy3T$KLiwQ1KL%_x<)p9H*+sLUg&mpGp}7!40W*zbi!)^>;Cy3`NY z$ssJyUFtG+x|MPy-{-GL85Zh0t3A&9E5^&7tE%EO|8{rcd~zDYR5P%?R(~@XHZvsU zQC78$ymaf-gI=XIRQz?_W>?kvWpz@&i-DWb_g|P6lM?$UQ_9P z^U+CsgpR3CM#rmqo@>g&U>3VY;)K~HoimPPlr?prdPa`)9oOol$J5g@D#l93ZqXUM zIrf1n%&KZ)%c_dIk<_k?TyZOLm&l@&o9X+7spr+$j?i!Sx_dI0s}aqc`dYNAy347N z9?_A}HzNg8{$|O~nYp%)RK^o;(a^0R2!EQ$)1O5usw%rEO*ct3&lI<5O^H@BJE3vv z$molaW2$b>o6b@w<)+N3@x`(GVnt;X_p2YgkiI^>2aYY6aol`?!YUqbkWm^F?`g)+ z6jjE1S<*nS?QTlFDcYPaxe%!n&7V3$HREuV9QQ}h;?qavZx^~Dr?DF1PLYc|!KILz z9T1ksbQ%3j^Bj@)%ahVWm+)+U^5-%pzejT0<;v7DsY|0zMK`&X^7hmg(V3|FmWZX8 zPNm+7k?{_(hf(xQ#-5CwDn{GubRU?0F8w0PZi`hi1M5C@ufN7d#D7eb7v1a-z1~6_ z-pSs#j1E$ncQjJV>%K1|Gs{Gx58XR)79x5w(pVA=_dE2r~J?TUTq?;r1Z?(K+lbqXSpNZGH{W_i1H;V`^%ZvUL>uq|?fW#|iiDX*un#>2?jyuJ<%~dINI8u*ax=)Y#OVR71 z$Nctg_VsD@svxbnhTQx`Urm8x%@(zEV^;_Al&{Cy^;z{~Dk24Io#36QgklMJ;a`&Iooukn86Pf!Jv_dqtIOFAv>ai!|kI7Qs$5&|( zUlAXf_$qT8J2*-NJRZMKrSq?_@IOTUb9eJX^>g*rU{({|4Mp90G)+Y`sV@qxET2>$ zvfGMt`|hCF;6wF3?>d=YX3FhR7xs)h-tHn-1>KuENZ(+#$V$(?q(}4vSY~=x!Umss%X;W z@T*Rd zv!~-$*hwcwB^B;ber-RGSFCZY`uvl4brh}qt7m1>EUQ?y2Th=DCRTY?ZfB>v)|ZRw z9*oz}Z=TSJ-Y|9>z11Q1YOF3P%jWa4SON8XJ){cL1g?0H0Km5+u-fRc`#Upn(^_qOV1=hcwZdw`nkp;Sw-_(~C-D<7v$y9Sc zjq~|;Rna#@zJZM2PpeIaYh0#>!yLP~_;k3)ZoRsa9jYFtS#OqDCSUMvys$R5KK5O# zh&=Ohv(4&?TJy?`%!cp1PJ(MV_fF!Yt)k|Z(N578-mx6XNOu=#cOui)vXX;ei%TK9?ToD=M>wsdd^aKDka!YfHI}YdTYgqUtVw z|C2KF_rwdt&zlA|+%%jg#8ho#FUH<8L+Jpa8H;o=!E9O{+7Y?xShfsWd<)g zZe!?y^ReovzCM0IX8vhnt!&53FFKw8xBhyL z5QhD_Z(bn7Lu9ktJINZfV@+SXpzcFf`ls6Uy}0v=O5loqQK=KY_Za&a5j^-GJk?h)K1(a;#=W{!;bEd%x6Bum$4DCf#^BP48g%VS_b?2 zzBxBnV^6VG->d&CuYzWm>X-2_(K___>6Fs?r{$_n&c$-u$;)t_Z{jJCsu61a&8~(aG!k>{ zOu0q>7yBH!ja3ShD%a>-TI?}by!wIP^u~)7@bN7?YFhuD6p^Z3vjhkNwXk<}WCnm6(XAA6a6-V4z444B_!IyHCZ9vDea zt1X(i&a8}W?hPHSXRM97m&a7U)S>emshMx3_GKW{?^_&D0%|$MoQ9Ol`(Q?Uc$*#M zNVbbQ?xVBb)ysB51;#z3xf08GJMB}*9EJp$`-i6e6~}%Kg3e^<%%n^~RvAf(@` z*ZoNq+9edN2oLPat67#fEPGxLTD=y|a5o9)FJ9iu+pLVfLut+#RuSyfXE29v_#t0A z<4?r)L&XfQxD9x)we{lx^rJuDrgevk&j-@Zo#D`TsdD&(_q~*Cb&%D{3xf_b_}_%B z)q$x-lWs>m7X7DU9c}{G{^YRypTZkg%_K z_62#%ogh0)RUVw?vlSvAt-SvLXWN&&ynqwm@ZEsqieBUaG-i()srGJ!mi1xhHDE$F z@~=}OXJH{%>D3=)Sl)&hJjG&}0WKH#8)Rs*{L`zDz!oZuZkKsIVH(tI^(|w~>KFo7 z7_O#eh&?=KUes_EPqUz$zd~P6sT9cp=V~d>xkz5(r*v)R~quW8YX4_Ub1wj`j&HW|5F}6@q)jRx0)(0>}I94WPR$(MBMfN zYbTD1CDy`^KE@mUnUeH%q+exm3j_s1qIW7nCmo??!X`K;6?5CUmH2d^s^(}eT zY5gjGUBmnSghxBZ|2!?@R-QcwH_U{4sS>*7gQs+zUhQ3Skq3Dr=Vf`nmR(xQf-d7l zpONhew-+B&5%Gm6d9hCCelW$UG|B`Neaqf<&@CUzm`so-e@*Uku>V>wvvmb>b{5W_ z0~&CjT;7wik)37LO8Hz@-tta>*XZ|XVcof9QP7BSQzoKV?Y#OL|myD-e- z1rgav^a%Gv?2t`bj|a~tbHL$F(E=VddEqs@OC=fSR$i}(cL{mVsUqMwjPr=C!XFg7 zs!Qu>-9MlE`XoEv8@*=x{brP?PFF619@h4Cnq73@Cx-hRYNJ+R^DI`%HvXI0Oz!17 zx%n6Pf7z{IjFokTDbB^4Lv-NxWfi;A2r+B!Aw5s-eGWez zBtKO`&b2V}|0PGiLZGgo+h`|RM09?)tYIy@SQ{_=n7A2D zi$NF4+v%;Mgmm27P1M$g%nXqw`%#vEH|%1Hik?<--l=R$C1>>@PE1WkOMk=5=Bjgf z)%zZz5&DRX>#MO^fx^eoxw$;#VA-aR(YcqiY9lXw#_Z`QS-*B>q5o&v`!q55e~t!bP6ve>;L;zeZG^GABp=|5*~3D!dI&LMo! zh@F2l5^m+VZ1R1n$bV`ZzH&pvW1b(fdc!g|;NbQAjqBwsYq+W4dH7*Lv)Uhy9P-ZD zoM%s~J?EV|!sZ9!_>U7?t#UU>+8>aARY(=tkeVqSN&KIDwbFLhT3y3&dDBLoS6F^7 zpZECHq}7G84A)y%SKLvOKDa3|UMIpNvE$3SCMUpl-bbgS__sH1YDPy@^NwwGyB{PY z56WI&GACazxt|U=oiL4kG%o6^JFuQ?;7sQ`l!TVSr8`J%cQ|YrU8B44*xAHcbLqFs zeS9z1x?MGRZM$8QSYw5GaatF*0PKM+|3wGfpi`x9=1X!X{Z$i|^nMM|c?ZdP4@FCu ziQgVC&sJ+Qou6KuWR}O_8=Ug!NGaL<$yQpy-3)6ZUEMU&-a03_S>w4#PFT`SyoFm; zdF-djj;7pcX7xU?ceuN&O5}Mw`yyR8h^{)Jit1Apf*Uipr!(x_M7*`%R#cU2w5oN7n&^Y4!Y?S7q1dxq7& ziB30|-M5K+-xQfah7Y(8VWl}I6(WzOY{?9B9mdmskKy*!{^v;MX57=Cj<_rGeM)IL z@J~IjuWE{DWG?Qx)qbL|l1rJ{Bl+DJQUjKCTgnY4_qRvch9vSRiQSLuN2N5B3%fv% zCR6wcfs5yWHu5Dr_)~7*p2ULj_Y9@pV@IwkJ=<~hcjQ`Oad>SH$i85vev6#WD1~Yxoq@b`=Hbt>HAz>Gpo+e^W-;|`x~;*x4GQX@daM` zl`nY(Kks0NKDC|!dLS;*iDhZh`Rw3he=No!BgX&R$mhJo*pjO{{kQEm@vptaT;wcoglM$hyAd zBwkW?*M{w?Ms6m_WAyR&US4aoU40@p*+mw!;ow5Nk0C5nus2`A0H%`Ax8Q3vB5TcJ z$R{q!;J=lN{KhKmO=+x8!$fGv)-sa)I!XVDZ!yANdwBIwCs^Bziu$yA2lVQJmd~p@ zm@7Kjq%LBq-i@ar6qO*xv&{N?J8`eu7UuiQdNs@+iSf3mgS=VRcpmNe3!b@-d@SJ) z6_n$+4E6tBmLiLq^@?%{)kJxpi1&twpq>+})Q_Hr2*uzNop?%{RAtUnS5g@l_q6*) z^5F$xjstkUbrZSdJpYmjzfoVtLb%?;GT{~Cy&w@OFsB~+En@MUdUrgu}m+<7S z@kc)q*^hNDE$H1ftoRV9cMS+x1M$WHxL&$Wff{h{f6Se{-Q52{sa@rIi^?p`SEbPo zMs}aO69$m7aKFm>A5=hW6Q%v83+sqJg*7@G`ihF0 z=va6^K7mVE&ycDMPGE()rVJ90Epxsz3!Mv%QLhbJ zOyipuQ!D>~-}F^|aWRn& z@+JO_4@!KXOGk{bb_kK(jz ziK{aHTSOLP#g>1H+q;SH%A1e&b>^%5nqisqpei}UemhOXpRH>46MYYfMAQ_6G*+P! z>$uJwz_lg>jKYV33;&Zr{YZyJhK}y9qqoUVOp}vbBpTgdHr*V0U>FZ@4xKSMF~(`m zRE3ijX5U>W(3aR97dv){$JJ*ni`Ulw(ZGC%U&#H9ncqNb-%`IB>bWnocs-qU2lCxn zh0_v~RF?2-&XdTiYE-(Y-zmc`e9G&Y>hA{>)WK9j-%(bZrY_+#_|)T(aXj}_mZv4jDWz6uDI9ER>;V|+XS{3iA$ z>?;fUhFOX`(R3tDc)eV~Hr3Q`lHf!1%22*xsMfsEr2T*3{`qivYng*_GDZz`6CY9K zF*?y!?MY>JwiSGMeWlW|+@$JqDradNFC z=*M^XXnE-1Y4QhIAnPqe+Vh}o&B*FEJlzKL@oC!YtVsVYe3QzX7?}BCqHO#m{I_^w z5KD4vN^5b_&32qyZsEVk1X;TK)I_Z`@Ap|#IL@S%)t8km%UnI}-^+?#RA=#a^em0F zk7twD^oQw*$?;_-wPugCG@G?^?0fTl*QGB`|1!Ov8SPhNuOz!4Ytv16;mgBB2+Pc< z=w<$Xru^~^sjriYc@TwPJ^DueqK^lwYPlDJ-9;~7OL)rkL_V5&VWMb!kXaXl;SGRb6yl^@8jsB(nvD-5mq`!jF567ygl&Wq%Od;LO%-!R>SS`Lxg--!msu9OIkqZeo*C1P zO__PXe3S;}Rs5+^>ww&M^TcPFbs()x&CD&CdcBGGDSF}RW?7r%yh-P^vK&i`rM1X1 zDNF7wPvG!V(RZMk)guFVZhcG$DVdlKF{TtOu?P4;cZ~i@_6D!)g1_+6EYY9MP8rWeKH52^+#qnEEGwB&@$^lUy| zxVN-E|NBL=)2^l59y!h%93vY2gs!^@pZv@2gjW;W#Kumgmz(rv9N$WN$RhY>uhC zq@vMJ^_ouK6N6ec&PTRt$D{+U*0~XtRfS58tyr zzlvUN*K-o4er*+(eC}&sRn{-F*jIUP;}QeW;xGxC&s(|dMx`12rmu7bSLBO4<-L|e z#NyD1Usdhh3eDUl8~V0B#pUegu;}M3(HVZoEvgAyr}i*I{2P3^UdP<`attNqx)wuC zcd-+9lfly>h#`>GR;>I|kI6p9^MUKb5AUM$TS5(P5e?=`|EHf`g7 z{(@hQnand2`dNZ+&`?fdNTMT8l^_iQ9BPh5I>PrtM#YUOy!+ z2pt)%cz@Tk5uM@BFVpsy+0>@8<;Bo@W%LPEX{}Rt%io-$jfbmQdrqfa8~EezVza5Z zcRcD2&%9PP0t>6OrQEB&;xL>fnF&IN3k#dno#o21lHL2|QgiWMR5Qp87l1_$Roy&?t91lh^r=E5uTL#Vcz>;#uSX>pP_aqL)@ESs1@%buu}vd_%HgdK8?! zlyj&Kn;s;WU5^Jh$%)<0M!)GKkGjpY32pEW+jma4`O~J3^ni~)i?+4!Ne}q+R66bl z*{GOY)*OGSq&i}iXkamK{hVm#Ci%1FbW47*U_eDbRZp1$<$aDt?JYX1oy0Q3{q$>{ z^(QdGa0~hW%@&V` z@!Vsg;AZ06CU%+%0U8UJIV@+~-_K{Qf4it;l05l`CA0-vzHl@^4*Hif8NP=VTyJf~ZKGjaPJ@a45|getJQGc-a>wyUxl z-0_gV`{<47@Y$REXSk~&V$fz(XF ze;au6$7!Jg{wR9qBfsH}ngTvfCH~eUT@# zTJN{-$6+4zXzO}TxW4!4234I3`57#8Iv(aUGKrQnS4|mafQ}VOEN}(`?PLm$87mGi z&NhV`!0UOfTH>81Fp1i7AysjH3Y+^83$TIZDePTpI+^Q5iGx)^F4G6T5O*)a@t0}X z(tNkjC*A}?JH@G-qi0Hr0k4N1y(zEu7_h=v|qmxe5`u#|mm# zXKO3&jwc(!azn>k#Mzd2Hm}Pq-71fEgLvX=6-ck@j49GY1Sv`_zbq;982AW&DcOz^gvqMIof?Dq-CR;o^nngDk zvv2s2Gn_+TsC{{vyfC-^7Fmi~EXhYa7Ki?y_^Z1R8d6S(l zg%$<$`sA^%wQ@SER0n3lJOdIR=0T1l8ME+OhJ4INsJ|a-)yp2Ydk1$U!_yvPhp)-I zl%%E3iOH*@{UKW6IBowBY5m9R{q3~+;--JR$9Pt@1#bR8b>4M&qZ++X#rdD}PL1_` zXOmyKlinFhn=GQOCfVUT*4hMjl_NJ#(5PcjVLdCBhHFE{tcW;r3q9PCXK^3tX+f&` zllW(F|2*`o=I_gK@j^7)>X|9%x>7D?ImECeTC73I-RdIl!S@s7jt+|EHjtozH*Q49 zA0d;Sp~_*Z)qZi(RHt+fMQd5rSU6%{yY2&x9>T}Y4_9xaFMm59BbI225_!l|1G0CA z{@kYg#ZP3F*RcW*d4)qszc61Cc{vD)T*nfuWOqWZ`%pUWc^a#a&#nD*93C)+%)aN< zO3;p>Yp;(uv}FZKW>|$tKmO(`*+qcbk&BN7{{RS zUy!ZikjY2%OcXO|=y{ZyNrPtUv+0Uk9;dkvdyIgpJ_~Ueuk!l=nAEw%ASiEhx@8RA z_88v0PMzf;DC%$Y=p~WIK4<;89fawzchIzLPK6PdhgQMAJN}A zlR2<1WVDUdNBO9mWmsq0!BHGrmsGzBt?xw#yhz@jBu|xTr^cjfirszUJjalxMoIlt zj2}`F8e7+EH6v{$I*NrJzg+rd3bA9g?IAlkKWf)oJaes&6LJ=p$oPTxEJ{n?fHtN4 zrVZZg4u^jU2ltf$ZED|z_*!4lXP=Ob^=wTZ&+A}^i)s9sG)hlD7iZ_M@s`3I_VB0z zwb(=^pCWU)>9vNaw1UnI_oLk?7d4Z|ct2mW9L{}_6!yl6Em+PHvi13R7p28QFVh4& z@%h&{GEBr7O6%+r``m%{qiM#&Di?lIZTL3ns79k#q2-^&t)r|hWJJQGfKGP$l;;of zA781~j?31c$IEk2;#qso1(6Cf(r^&cOtplX-HCI$>=#add;J2>#5i5WTJ{n^>4m1zE~Zt^1k%VdF$=X zBd(|au)KJyrK*k>VXw1kl=CpxfllI*`1ob-yVd$v(ZGGI?_o3#HfI-IU6JqD*wm5| zChVS|58mS?%=X-M)_+wc?DdiV?D1gAZb;`5n8#msbq5^sDOIfv#W6+QU3(mIKii3R zVyzxWpGH1cO-l7CEBJ`qw?py!#Z#4en)k8%x#iOLqxp2cV`p=RN|>K^v(AU}V%>cr z-O0M>KJYagpZ_yeAX9l`18J?SR#_ZP&)|uJDcQ|+}* z6&u-UcRaHJ#Y2_*adZ!rGrz!2FN+EK@%?`m^M@+Ieg3zbDVFXjxBjy1aw(thb<)*f z(j`dtcle<`J`Xd$uE-O_k~3C*bOLXP{u4SaN2viV6VIxm?Rr^{JnF>Cz%s5uV9VgW zJ5lJWo0o^G2<#fUOBN_}Z2iVtEUW&gquAsvQ_@fL z*>|(6*)p%lczi83e@f@<5tzsT=us{8ZADc(=2!1p)HK{k{FHmL`^Bq~ig$6+SE|{X z=$`Fm%_)%$v{E;6+81ZtV+SLxsDY}W)W|xz`475t7*0*-5^w1_!C(It&mX}{i+MVe zcuAR1qnFh-u2A{5SM}j}9erO!M)+NOSmPKXa%q2)*xWE(tmMQmRrE6_%&>}pCD(+}%*AiM=LN4bi(*OGd4Tnc#<@>_54mc#2z!&&8I+SIY{2B5q#}e>(|7 z-UdZ|z*Mqnr1%>>oxNn!cEH<)k=JaIkbf9PmsC(6G7vSdW!A@U?eqdV{Y@imbC%yb(Vgt>tMp2^W8@V7<5wQkc7Ew3 zcA}=pY@3;L4NYrIkngkZt^QUo=Swm)P3-FK_=VUmv+w4sSgjCy$j;`*e@f8RDRou! zZB@s2RKmDDV$m?`*xT~fFdznjZ z$aMDPQTe0&GORDdR>E|vn-agNy&q~nVP;;c`{g&nN&Ch6#9q?b_+ji$9YT{c=R=Hh zt3m#s`tN0SQAtj2FWUWUPje#?IoMp$FWkSh&%5V^jh2Uw?om53L(~%P#vI277)g>| zVPV3JG|%F}5%xCA9u|1aXFvP#16s50eRcSa;)Blc`!IoZfm7YelRZR-Y=qdZBg^47 zkmfX8VVrn|uh3Z3utUT%7uMCuY5l1d^BcVan{|e_GQt0K{S!iKs#rw{p0k6Bk9*wG?q)Y;cYVuHykhHAh@w(y3}CGO|%#ngRFa0>J6 zq$ADSk~j1WNgs*=A6eZ*+Ut2%r4D;?FQ2C@&De;S^P+u@@;-}L(EaAhT~SkcBO9F8 zv|p7qwEZHqc)|aq^F<~?^d1mt?o*xH7us9{GI>Ft@mMV!LAp+=VyP zjlN&UzKq|kp5l7_AY&je0WYc<*CwlTg?Uu((tv2U4t#?=XXy6tuYx)v1NcMoX1}y} zw)kHCKV!@b&!x_6k?CYBNLQ7}Nt{v*w?#!a&!{ec(tZcy%p2TMF$}%uhzfsU~s{Zc0cWs5IY_AILe#J#g2vB8C&o@J2=9o#=*?)$(r%GWstON#LkyE2A~u3IwOQ=&qv-(CcwLXd?W2(}LFy%T zeGRW_3(Rr9z3qiJg?qK8kc=PDKHSH2)}U+9k2v3~JwiM(`fUH(A}JGhN@?D0quZexx3O3=oI`%Q-AGC<~zx!bl^0j#QV=>1X5%F%P zR>hf)x2JdT>oQSfn!5sOLxJb=v%V(lpE8uK#|Ai^v7kPKV$6n;8<$Q~g=o97_9pm%A zCr$}>I)_TTl79OvjXsE6jzfoUc$8K78=tW!dszJ`urd;lqWt*puzPdS{PwBPmX}<%Kym@1&fJwZZIa+?LX|j4d=!uP@ zm4hOY{iNt^XYo5tb)5A~<$1JH8`6n1Kg9=_F77{~qU}XG`T(u{HSImiNi4MDNp#Qy zqU+US$WgKsA44mCLaTao{X9B;DLisF-}p?*4ZO^bqsaF;O;fn#X(2=^0+tAm@0()RUj(!K;v^ ziM);P^oI15U&&mlTXTr=O^JLfZ5BoW_mz-7?F-VORamN`vAMVPmMt@wQJL|AF zrCIgo@nN{{dOjQ7m97qR1XExJ^ThatM9PD4XAdX3Q*C`IodV&VR^!H-`HuH_kI+4E zLA~=_Hn50{?F=~5|M-XNRce)yzdq}}qnKH@`&DO6(p59s`Sg^#ZK4OXy=ks-YDiMq(iN2biS_7KWOcJG(b1Dv>CeA7vD97L5`xI`;+%7GPPA%=sa-2wN5n7 zN)>?Iv~x~@2PcuQFX@A|POb)I_(xhkFB?%C0$!WMe9cQs&@`Wu#o2cCwUhmgjK$^c zFR;nOp#c%hkZzaT z^q1Zxf?t*RL|x@<9e~%H>@$HiX~q_x!XwY){#~jFAB^OQ?6J?;G*}CM+g$mIDsZ*O zU>=icg~JepoYoTNLe4{z6Ew&&cCf2g2pCZlr+5pE-G-L@ncn;ZFE4efA9=mtFBQQ7 zeaO;ecD^LcC*1HiMg;#H?f#Egd`7Z+q*UTGYrIWw<{#u|o;~!VAC~YHf9BzDSJkwT z);}AsD(CxQ<~u1J^@ddtrS7ATo}$t7M1Du>4QIL&lTirfX8&YVYQ#;*zXQ9bd~j9h+@M$zZv^~O^o%g zj8SoT(QZ-J^P+^dbZu39_nBP!b1;S$eE91UJLvNiab%vv9@?)z?)wxc9=5B+PHtN~ zQ`mdo%^F{hN}X$JX5bZq{$S%8K6Q`^23}ZxkG5H|yEj za97#`&Y=~b@O{tP;jzLl=FowwS+9>t$42N-X+F%oUiT-@nT`YRmd9JqcI1YdmE?_l z>RjHZOGeQQ*;PJlhhrU*t>|SUN$8}C>7L4)*rdj>9giv1?DY1kLC)~zH}UIN(Sc)~ z*6_sZYEKG_>BsW>diwgF+59i@TK=`JAK3l}cpZ&JOn;&KQPEz1bex8My?G56Wi`Tl zvQx0oYCM77xO5yF9N}3uW^b}b{^Pgg7yVX(+^i>!y~)>Ndwhv+d5VmM-kJ+2^CCUk zo$enY4-xY6BSpaP%K!Flx ze--UYiSOdR%K^zQWES%cPU#V-LU$ka{59Njw}I?eqQ}o!|53TmG#*14zIivY@G%d3 zH+etrB=WOo_xLErS9lo0@c;}eYvgOE7w(U3Ok*~XYkP=>yuq}AFe9@yj3uTbO|hOw_^18%=kO*v7YTzeo%?lp11uP^!#2@I~}Lr z;r(uo{KgyDNoJ4n-*2{(7O;`}^lFrT_?-2BlJpi5P2?lP_wqE_*~cJ$#^-#(TSQ-X zuwNzcXG0feR05GlKCDU z-@^Wf8_z=r)~z_@46Ywz)%B9G&@*&ZZ@Zer@^0lP1&^i>%6Ecp4B{_N6jwe^bA-OV zAN+kEJ@$iFJb_-Dy-O_!{^xRC&(R}pDI)hv+21g2?bw*XX(*#s2`&4V<}O3qAcxQk^Oy^q|JscjTfi%gfKk@waiZ^-t2K>ivd-`OdT7>tKc4iBG+zJZutjdn3okIcL)tC7*=V7O%{cbwm z%f~`AQl;`X&KxQq*puuI^!;3x@C1ozfnE#Qwg1E4r|`1_KXiiV3`hU@{BpbL)55c8d)k;{>qwQ6u4a1On!d%jbcX(unU zeSMthBtCV>W|}|7t_|RQ&0t%`Ig9sjP)|sGd$#R07Hz0iEO8n|Nl$N8yYKKHKV)Zz zIFmZGZ882yKyP3Bxf=IljSS}t{Sr(nX@ zJYvxIlPW-dP`SHVK76=yxL>#EGqhUhW6h@0pc~l=nfvi_ByZTq%g$vmfBp+Ovw$y# zDVbphZ=zZ_i`KXyWaPud$=-gyju!ZzU38+W1ESoPPh6chUWG-fNL$^Blm2fiwfN2c}fb}CzGus>8lg~{RxyZVhz zOOf+_nq0ppYdfCx36%*?%heX*FBa!}cN3Yc@`^cBBD^aind_Yg(v-S`c!#gxs*tJb zZxxeSwxfKgQYK)OCLP5|^AVnv8)eA*5qj$)|KLWE?FGKp4$ofbf5!OVHvF4==-a9= zqqc190}%THGGh5eY<4&NGHJ6o_Xl4Cl z>?TY<4V^UuorjJ$aabq&`$a!lm}vQo)sG+4U!d|Od(G#pmD?1G5x5>VClY;Gd zZYk>bqT4FaO1GhRaaKIkVl;u?y@sRSK&5bZdO3SK3Y`eJ!eaQ#K~_FbGLIYjVY|_& zCFOt;>RWzK+z3q_qLv{J<-DC_Wz`3C(JD9l?=NY+6SQ7-Ug7Vu^`D9idpnsH@S#y? zuuDBwO;Www3V#ws1}y7(e#KFEPO#`Vu!yDUoR%c0r+p5y!jM&N<_sGpbHUkRa9R0Q zkJ|I2eEo1k<9Iwf)2p`ipOskCEbgLtZr)&CDg?}f)HDQ0e8wmZuew8;Vglbb}|;XSL9(e_Dt7See(kEVN- zzU#miKOr8Q<+RH=mm$3Ssko>m3S3Ltyq~Qh)y{5CdNrkzndkr*t;a%_dn~t?Kc~MNkjnsTuZj zP^?fnN!vS}^KZ`Z1f=PE^7)DwYakijhZ6tc)~(LIVp*^YLM>h z)L(`A(eRfEz6(6L1Am|Mp4Z`=%ed?;iK_q&c*5!ZMviCFjG^kR0ju5GtFA$j;?~mN zz6P*Rjc`*Ia{D(P-I}aiIbeS`;5}C%Igd&-M5tb>h~KhWbI_69pZ8w(IkA#t zCI{|sp41^V$v~)Wc$`nZSIyBX{@ZG&p6)%?`u+bS4<|Wu zHxG6>URwoi`IknmNS5%-h@~DmfhIpmxt)P+J%n^w#r6oe9aV_eMAbVrc zZx?R=8+}Tl$g?D`8$Hq$k5t5SSIEdw^f-#+&ymju@o!T)=2et=(X#@N>9EEjXRP98 zdC{+}HpH~`*x&N7hO?w*0UOkcK5yn!#*v?$UhP)z*4oL3int?a9;QZ*V5z=zM#Y?E zO*Fd>qPUC9%^^os#I#MV`WrOA55K+5E={8oYx5x2;qCGEGLLj#BR3^j%%XTEJ9->& z2HEVSG8%>148>`K`<%qB^w??pO7UH&+iNq;71R5pJ6Do)~V;Pk|&(c06bKYWMq=}EI$8% z#z9-1#t9efH{36N5eIi?fj?uT-@>2e=$U=qb&&PE?4t|+<{vRwwWQuGr;B~Lc)OQV z>LyNIMkAbJnXY){!|4Axj$DWGm(VucLekwn>eI~^o%|SnYf0E$EOEQnJ(k%ni8Za@ z5sapTLdIhSTmFdrWlv|n6;1#1x~1_^M~`ke=pKA?$!iB~^aHyQ?hGGj&!_bIj_^85 zd`#huJcb{>Ayp$~D~Vbm+& zl>R}Pd&p`>l339>^!3d8s>?%V>>U3cCS2S~n@@rn4MXjGC=+DuD>Rsbe&1SsW^xDF zX`;%cytL;;Nqrvnt^c>y@H~QY?f3iVxN05hy*!!)t z;zsK{<#mhl!NVl~j=tuS+Zlq_y5jxreAjs>6J~T2WG71D?n2`6eW)}Pe~cnGlbz8{ z?|YY%sm%@?quFn;u4s~Lx7fpRd;gPkjwgi!=<1Uy>6)TlH_{ZU)%xJC{q%Q5zGLXg zU2k3c*^5@ZxwCxMxye2Jj+DGQ#9U0GhDIX!f0HO z?XO3lnYtVm0b7wlo6XWiiy7m(&K0b?OLC!AnikwzhYdhjMrdq#Eob~x{PC7BRX|sC!?0JA^0l zu(gF6#eKBtAii@E9&~$rxdZ3sWNCZz0zV{M9Z}^w6se;s_N44?MbdmqU-lpoVt%=g zz4nmb)QQlYUD0Yn)pKXm$dtLL?p;EB^*6b_1&=@Ob^nKc2XVs+QqTlimJNadMIStc&zi7Sy>N?;RQ9bnpC?ob-b`1L)kPr3&o3IP_)}SAP_t}oe)R|>2#a?}g8f|d; zJ6`Pzn&&QO|DjX(j1*L1^S(xtC)v+1bNmI`YrNMk%G3E0+OgJ2)n?nbT0>8?KPSWb z2S2|a9{!DdR>DhzJokB8rXl)nx4Sax-m{6voGZB9wN|(#@)+8bp^qA)Pr$f?*LbsCAN0K7eNVBE&Gb(yJ(Nl>Z1(e1 zzx|RvJ3xy6QKMBKCGNxHIg@k1dXj;rNr@ZkozqEF6YCAPJVf!tDrXqxz63exjs|^k z&(}PZ<5sng9?5UbkD_a7eqE?pdIL3wTK`G5`!x~Ew{WVF-tCZA=^`R6ovb4tPNU?q z-`O}J+@AD|(<#ie`pN0f@}39q=S7mSpYBShK~tPXs6)-eR;Q@seGC11(bJnfBPP!n za_@Dlq>w%TK>{bD#Ux+PiaK)muL7)jDPF)K9>{A+*?fl{3f0leXo$-=C{$jQ#N&yi zkF$lweVq2I>D))NA1C=S53!KpBp*cgd+GH3&ZZSPcm#L6g2vmdvM(6=eBHW z%sDploX2t5tJb@nOl%=Doo)c#za>hW4D7 zJ^$J2@|eTX(aJ(UNOPS1CjQBgjeU*-R~2;uznm0R3N)r%=i}<)bm~#kcqPDT=42daseJ%M7vnr#Ry? zoN<*#c%0Q}gU1W=FMmOe!y=ahG|FlcI@W(}G(Fo-i!M=jV!0ycOKfX z1uvtV-yC!L3rXW7I%6W;5~BNW>52y-MpJOjbTad_XXd80_oH;cOGet``{W|@4&04z z+9k1v+GMzHQqNwd-#1uon6S0OUc(%=PzC)8zr2&Pd5olIqQG37-_RL$MzP&!k(WHA z@*8vcE*p!O5E1Ps)1ji}8mY=+?J2z2>v*$OP`xIOC`IFhxIaO|1?1ury!ffp+>cTr z&sc{Y>&CKow&N;Z`!}A~_g?d7lne3YS+9M?E8m9hA-{90_s)yYDmay5-t8D_gxg`h z!~2(bEw?zY5YsnGN?v*IeAJmvq_4-I;zv$u9zDO_YO-3}gPzqo$+>rXhYar>@T1pp z+}rG8R_kwOWxYIlpi5Qf5qzK*Xt!Y5&+>vhz=-ES;I_emR`EL8d({)(=S@=i3Quj3 zom}D<*2UK$NBm{dtL{P`D%wR(UgZuawbXfhYu5o?2>xsge-x#G@+5835~n%B$5-fm z6Ph)!mQdUOFbz`}#b!96Cup9>t!p&d+lvnM*pX1NxRQ_a0eyL&^E&RlH=xxvYdVYi zb*$+_nD99A8+`ICWFYh(CfJTrB&{A<3l(!cP_q)7amxQLq6Me<9BN{|vG)B=|8J*% z1L?>~ioz{RA+z`;ejS+fKzrb#-Z=7O@BSBmBxu)LlG1cLxx9@hR5*F6xyf3Hyvih{ zF^>9$t-3yI_@Z?V!|7|FgF7JT%lX6|?Y|6uC~l=;(o(jhBn7(NK}M^iPjfcrc01ZZ zW6xkq##qB!bmZqw`8X~3sdKI=b8`-&yG8cqpF~ksc{}vs4G854-B4Ru*)HgPg4ESa zRt9IGWybM@E0UFk@)5uBTURCXU2dm`5&Y{BOX4IgMI8%h&muBowUbq@2UxX7NXWkA zdIHA%2_8F1_B!x?Pe61p@zSQ#r=^|xQaZ7WtkNMG@(}+jKYyVnEf?;3e2ASX&MH>m zpZ#C;`#Ae~#93#P|9V6n*}bwQ6VPuu^!Zcu@At@gJSLxWD_M9_g~^jD5`Qy=_@b$$ zJ=DIu?99gSl5bT-RF3yL)v4ZN2kYdGv&(TNaJP9;&g&A3`!z`pT^QNvxX@3t8(MwD z*^GBu*JzDq>P${2zEj#@uyq~xW*Nry27i#wZWih&-|3*6<$Xbu|b*9~vVh2yFDZC|e)h;KJ;9@c{ z!>xP2eBNNW*3~*6swdH@T_iN!O88O^l~K8+O9*q@BNNVfS9g}gy4f_B8PF#<+{b|m!EcG5;05_oB*X(I2^Ix9xoNr7$ zTA&B~Do?#Xo07nHx6{fyt@=6tTUf=@k4|Nc{7hc@WE-9JlAK|OFz3iEA`5k#o4y@+ z#?9`P%xGz*sytL2?si8}aoV&uStup<^?EW}{&3_X>r|DtY^sxVk(}bC#IwAs>*=h% z`1CNJ;t#s88+$TQq;lAE=E$quDDwNlJHIKb*+BkjwJNP4av)!j=x_K=F?j!AIlV@z z6K2Z0JVq<-qBF0jD<5Iy+N1Py{HT^}Vtp2<0v%C_JqU5uQK!?%PVd&2a}Q~%BHC}x z+y9(A2lTi%PX0aRew7$MsNveL+NivW$QI6RxW4g^)d}3B60EF_>wNyF70GBMPqx@p z?a$=<9*t}#w?kxBp3}2&gWT^^vUNwzLL5n_Ho#Xa>EWko;nlQsIVW;S?l>2Z$}4znV|ccxd$E01bR2}9S@;$ax&6Asu0uYgtrrUa8`XenTJ(&ZL^OT z)K!c}tv}?vnu)+yk+vnyt_7LD4P8T3^uq4}Xg(d&l znPg}0A7&+U+UFL!asp|40GIC3fBLK03ODM;dRTO^Q`Gso3{y(uedGjyxf=xP}$#ZQ}d@In5lT~b-@~gD{zcMoSrL1%E2USvhX#FQ-go}IauXv&(>AY>~O5UKm!-TH? zSJ|0IXIY&IJRd|riL!%imMN|vwAP9ubw}|WwJlhrXRM<`OQ%Pa8G7urwOU5zXtC4N zDtZv7ts=E0vS_U{MFj;HKsLi7BoGn^TObQ0>|2ug{qB>uNqhRwA|KZ{(Obx=o-lZjBS zWv+Y*@z+84+ny2N8p>uOpLg&O_b?mS4Zk)4A5sD(1_9f>j89&J6FphMy&O+92i+b_ zv>U5Q=GasKU2m-8uZVQ!VL66j7maQm_xoh*bUf`UG;ADk&OBm=IY{vsM#0OOb-98a zFqV_)JO!&V3rb&1+_nW=|BXJ2(4sequ7@ChbD+X4@Vp29WhnM<5Pr2Qw!Al%b#KJi(St(XqVizcA$%xLFAT~30e?J`mHl6k(N;?)!a(+M=t#WQgE3fF zvQVF8H;E_l0~h2zLc`cskUna$AXSa+pvx#c)^cW?yJ547h(|{AT*rCv$k~AbJfZL^ zG?<>(mghlUM2`!}*Zp#CKiYaT(aTS;UvGoqWO5?r(PBTwpHD)eHLPfrGPdkPtalq_ z1hdt?bMJ@9%@js3L+PswUiohDr-|b3!p00C(wqXfYRLi|L~P`HtA}8bZXlNaQkq{h z!)^w19qXar`$*@%Lyt?yn0poe`wrzFVOuJg)t-Q4Y$59LDZz7cTj1a2%>6w8Rz*A& z<9q8LWPQ3TTv~;k-hr0Bjs*IoavS{oaPkU1f@jmPK7YU-e2#c~6%s!wtqeVdJ-rHh zH;368pG5D9)gJ?`H=;q$5N8cTt1lu(T82JdmnhVmxM3N-FCP!_7&y1Xzdj26W-v~E zh!IgQq~MQO*W01MC2)ESwSGse{bxQ)&@0yfi?Q17RdY47V*gBxF%3%nkZ<4Tdlok4 zQa=6RU;(46zezok=u{C-WRN$=^lsQ_zqBfkI~3*>|rSq z{XKZmKxV~3e26)LW08n)+&z~$kUntoHb!4x!t0$R!rYFAy$km4+~V`A?w4{u<=bfb zFM-)7aTfyR0%YP{@Ocehy@CB$0o+HC&+F)UDt3P&KGj{A&t}!|6*x7J7^OE_JD2hH z^-%sfH1I=WsJEfo{rIQFSg>Je#$~Ka|B|soH8S%RbngMmIF8(lc>$VrRf?_iXzOY` z^la8eMj!Tbq;Z@WXFcz*_zHPZUEui$;;O#*wa?=f{sdS0<7@6i8hwYuEI4uv zHm8xNe?Ci3Jxs(mbjrOC=eEG5#boRCB5&szBJugmgf1ah%t=TjA3idF{xr8I3;W^BmLp zc`xBH_Tq)+K-IFw?nv36;re)Vy@Xur{n&+xNdJFf(cCAfyzww~zDi`)2A}aM{`NBL zA352KaVyZE%c%7+QR!;@{Z(jEEpNqs6E2=Zo?!0LUOXt)Mx`~iA&JviKm z2G79b-VVJwLv^3_yN%eQ5UqLxJz7T=<|HiYOfog@Lt>vKxAPltX&TQ-+??Bk#uOuC z8;IR*gx_BUvL5LA2snNbxk-=k++YK%T$UyL^yjNNQXv$D&; z>sdJUeeC}+CC)38BzV9Ul6#n0wGt?V%DAd=@h1`<%Z9-5x_Ke@>J%2I=cZ~XrU5oS&96)=eqGxIlZ4t&3^#@E|i{(crK@0$dU7i?k3?nz-%ZswHdaD zkodXyyi>@^6;SU^Jn=WstQ+yi?U*-mb@nyrJDYLjbg-RC>^F-$=HlthBUnMiv6MEK zz^#q6e-Ij+VWwvw@t@C-^KLCH`Q7mFCg^@K-X=%vRZ3J*ghwm`<}Dnr<=(AWv0Y$R z!aSn+J^Rv(Q8%bR2)^D%8}~r{FM`)$X!ilWXcqAN96K=s%B_YL1+;%H@!)l6s!s@> z%iN!Rtpq;b_5Lyaxc7`J7Mtjyi1_qR#GLLOHiOxp=ZRrn1hb{UQj@Ug4EOVi5z2{! z77^plrKh*(dn=SKfX+jajBC)n>!H_hVw}GCZC8uh(RVi>=ubT7?33$*oq(n;&GHrj z?PvJh2E6c4bZs=!JrP<>z?b_T?@DMohY0*X(32@h(KFD05%3;hWvqaf^YL_*NrE#4g51Ed-}R4+%By-8Ok6kmE%G!UuB@fClg#S4meLK4tw^(nGtri3khbY?Wft`I?d9&f zz60JVE1!VrLV9_h`RColy91!V(a*#9s{4_&!Dv_|R~-%B&$`TapyO=no?$2OZ>KSU zdr8ehx5mS@PK??97fE`Y=>)dTdk^ns$KW&B(wCFf%i4FK7GE z-(5(+WFl*y5Bes3_J?NWX=R^N}aOzSp{u&xFntN_V2742Q??=+taeXDS zwG+;EflI?^)qTw_#BMY|zxSa0&#)TdTQ>*{vM{(aov@crEu`~aQkJ7d%c(swGj!d z1Hwk6%6zD!LXww<^QVa&Gfod(B*n=Hx51!U^ zf0*I$;S#K0TWGht+5W5pmm=!!qEA;wI^yfwu)cF3$@&IxSOOOB0jYZ#ZbpNvfd3?` zB8Q<^75o&>Ma1ES;PFzj%nPsp%Yb$l*w$0mES?Uu>#Tcdeov^?2ifY&|L$1RvytpP z;1}OhQ1djfoCbg4aUHlbHm!hTKkX^5?&M>|^N_i={LeutVXB8xH9)lw{PaoN;CvY{ z?}FZSKztM{bAYlB+U!ocyavv^M{$>(55d(nnopp}XWYA&_KpKZJ9=wFA8oM?Cuzsl zcTy_S_fFcXNo}9z>M0;^!ei&SBOgdR0(;j~dhji#ZGiO{FzluWIqGf;JE7DO?#-uu zHz?B&x$R2bCTbpp6MMj7Gqu+tnI+&PK6?@b6=1i6-xZuK=4ugTJ$0=kjzZ@Xkk;S9 zz3Y(;vsP`%TqGrZClosb?B);ZGx|_^@LxZ2ivG<&-cG$zzPHm(HP|+xL0y2dUn*M8 zPVhHJoNf-MF@aA9*>e@tsfLzPYzKI_+oSSU19j`c?-*P$W;qNED!_3yTzH4F1c_V4 zZ}Hs;3|ij`?p=?~_y9gFgzNP&haI1o!2kEI6 z7>ys4yfUDbFWaeUM!^QkMxfVwtl^`KZ3Z9naBHEyd9vNXpg&Y~1=GAY8lUY-CqGpn@^`fK-jDyDz; zxjF>|YTrqYU0vIrq-i~EZUpvX@F@q5YObmE%91n?tF`p0OnnLj6~JMQun;HUigc@k zGBsdwgkS1wdsa6Jke724-OnIn<*vK zm9L?RN?ho@+91!>{?Y_{8NaquZyWc>xl*plx%~+;vkDI;&hJWiZE1#N8?EdEe&4a# z0a(o0aXzqZl9gIoD5JfNiAE&}`#SLTtyAXWoew9waNLHuVsnV}E-NU`XO-|f^0B4W zX=r~M?5dh2M#$ZZOvyK!_!yMbGD=II)>DTzAwjDX-9JmXnAcqojZe^$l3IgYX#>kC zVIzD$(jjW7ab}^^(2~*_o<$z(pX8AoGP_S(bu`&3#|p~sdgQ0{FI3S-Y3rJ>dS;kc z0k1k*O3_m6p*^8LNvkL5(d_fRi3_E)CzZphsBH(i>bj)X;23rFFWN2XAcXQ;8z}F! zALg)Lz`SG4}eprKwyZ*iPH)UI9hpk!z-_fxc2wyVA$ zNpKvZg|PWr5#`>Ij&dZ9N|3r=$G0+6OT zvy#Mv{Yh7N?+%A(wg{Lw9V4N|9iN@HofCO>fc_}7_s-N{e~uaJ)>w9 z5Lkq-niBao>c5&NKWs&c?FBArq$iR;!W4Z4kGwnjQiHM*b*r0OtA~pAZJcfl5K^gR z>RprxDW)$BSf#r@PCFy+%DnzepDi32U8Pk(FNfkt`pai2thNLnJdUGEu3PL$xS}ub z4U42T&`TI=Ri|3kv*r=L)oCdp7W-3=LBZf}Slr-++zW{|(y}e7;oZuV+8MGEk)&~D z1w|P!dXWoyo|BX_$fVX-sMUlhK{MZ%rUz=V@fq#xDI`c7QLc%-EoB-Yg~dLjXYf2cztVP)J422FNAC-2DP`f44keuQ2xdJ+5u%7( zGaT$)D70?U!;fuBXJt^>JkDZ8X&ik8Z`6K&$9d&B;P6{3t9~da){q9#Uri#v=&m*R;7HjX}z#faYx8p^sCm%rHB*4QhLT#ghWb&2R)s%K`f$`&<6Xoc4(eD zDttzFLK;?C9)`}^Vo0>uSXUhg9tJO^vL(Cff3L{dXwBNrtqF_cm#!z{sG99vYhCDq}Ha(-?~i{P~W+ zw#|3Mh~Kfl`J&E0IPY0NTx?|Ap0TpCtXb`d>3kN|S&k0P-`4W}cB!^)ThBB3>5Rv1 z(^YFYvvF4Xww0`{v;T6%9xb*KAF=aKOk4iaDb8ilx$`7qBOPMSBxoZg)XN5Lbtc7c z+sbGbkv*@^N+l#h+FAUI|K1blg1*`LtWK($zO3ni|NV>O?6>#%XRocHaDFH7^gE+? zP*VD36!9qg)tZaW!#H;ny!A?4jn@3OzZk~^jiMKivzkG13 z#W<%FWU}BHe|i7keurc@!_~S3=dBx<$2BD)`Ur_T2T667OgT04-QAUG^kz z9J4zoaSrE0pYjq!1)TAE2d+Dl*M&Mk8Q;%p|IS@%X@V2Z5~^38iFr@C3e8Sz`ol{m_-+k*Zlqi7Zfg=foJ%4+++rH1~`I45E)y5p4 z>d$ALY7upub9Ys6DRq4x`g5%0&SV|3kUEQKa|L%ec6Jzv$W`LaQI7GXShvb*C>v&9Q(W7=?o_hc* zU7w@1#?(&C%WS6=<3OdrRho{>6%S>_ZWt>|t}TsXg>Ni5P~Rd$VH`(yvaYN+nQvI)>GAg_W$13?tZW8 z*5x_3x@Cj9bywV@q+N}+b@~n&ogrGuP1F`$M03#| zx3;1+9^2t)BRYtVcM9{62ne6R2SpZCVAp7?$r(eM99Z!t&=#;XB1`r-KiF%-uDyf+w+ zL;nBjzi*5bqj3xue~YnVoS1-PikK{>i>bKH67$4d+|L&a#bU8UEX8e!SSFT>m12eX z7xyd0TCpCtb-1m@u}-WJ+r$R3Rcsd9#U`;&><|aVPH{-=6$kL@Anq;eI4?-c>7s3j_jqQWf{@tju?HMqmF%>CwXvzWEgx?o+mgf)x5;w8mQ(M$Bk+^>or zM5X0u7rKNtq3!7&dXX-sE$A@1k?yC*=vR83KE-2sEY${zdj&tiWlOYFhwk6GYHaMDIN-$v?A^CtrcVznMj6_ z79=ztTp=5&NZR4ktzEOwGZkk3Q%k^CS5;-uMW zUYd;-r1|hzoaUyPse{_6Nj&5$q_~gFCu2!Jj4YDGi!0FV<|0~9Xxc75kGJN9xWpe@ zC#|j4bZeY7&>Ccoum)JetufYkYqqt*+F@O`URVLEDDT8)@xA;RXQCjicQrKRhj5V+ z7}pF)D2~L?x^x6xNiWmSG>DaCZP+BXg?(gUQdMb`G(}o3?Uj~F!==*Fb2glXu*=(WDNet)?d!Z2C_2BciRJdZX1(h=^j#9JT|9koBS05$F$@6Tdi1N zK%lE$+01I%wNYMQy3@PSe?e1?6J{-Op6+JFsE4Omm-!MhoJLYctFl*eQQIKfGv%2y zo&BL#$O-N>-s;Vk6k6Q9EqIJlk2z(-KG0djnLFqM)?%1iM~#*iTSEiiyfwZ1y#;-m zuZ>pS+-vnP8wK8`cSvoPS}A?6H*?^jKEXOG8c{#Z$m%cyIvz%5i4$a-)YsNGc#R{* zu_x%BZL{*7)gov4OYSDY^f|dNF7ph$wYAk#=|8t;$-r1OFY}40j zE43KoU#k*X!c-;QrUwrRx$kQ3*4)0(v@mbz3ug)YFr}#NjI(A~kFdHSFV$nTuV8$f zd0MZk`2uePNOF#Lt_|L4PgY};jVu$1f_L=OH!M4yWIfXc`TO{mdaHT#v{7l<)0=yu zykoto-UMG%piAJSKiVG=h%#!5R5n(rYbz1d$x$$*hf8q}5A}v`iDVfEMz#+v<``k0 z5Hv3&JanupQ&3Izx22j>%z{=0^Q!jNcR8J>v`K!NR6oT`OZ8;-rlrLvPe_`QTq%8c zAk1n)TG6BMdz{RaqHVu|z6U1Sb2>_^`rU&Gu@o1HSn}d>6cV4 zsee+dq>;%BQ`e;D_hd|Kmb^8oMCwc5PO}o(N{`ak?7SRjOA1bO<_g*3G#nQk3!G0v zOxIku>~0vcFL;P8pFD%U5;;XLo@g%8tNE*Wnx{2OEuH!&<#uwTq_D(a2{B3Tl&G|8 z>CZiLy+wUl{67PejE(#pZ7bJT!|ZQ^Vw`hbFGKH#=gLq!vTM}&jAb*8%e*{uboA4( zM!|*UQ7lL)XnU(Pp-aqahQ~Z;oz**dN+hk155#XzXp-ETL|E#)LIVJyJ(N*BAS`1}trxZsfS4tpBH`2NWx)^!&yuLB%Lp>k-tBmY?25BRgfX2nS)zGHyJ1(ay z#aY)89lSDlzVoE(nEQ*%=g4L+B!`f0=4@@1|GTfSKZ`$~Z-!?>T8Y#$DZi5nBp!)h z5#K7YOiH2j`QFC<$ALFmE4`9FRR3ta<*Qf~^_Bfa@MovTbu_e2SoN?sp+~}UMpTKc zl`%g0e5M?knniXCc_HWJbIo?5FjLqGG019Qwda$qq`_U!vOVP^G)*i3N#N?@z?WSNL!q8FDWYVWBlUyw+Y*mX}a52Hqc-DsqNIN zXn8exnsMXUA zjr((X3#ENdew9cQr1<^uS(CP<`aE^~%L6VgF)%oA+n?g!uVuCN(yxjmXuM-^h~;|d zZs$Jfdh41Q8W;W~GAg=2=7BM)~doU-L^+f+!e{n5cFKfKi`ueAN&ZdSXZ%@b*pDX@M!qntm zX*WHyearlR1!Dcozuec(pHVNv+piX^;7+y5;NyfIBAIBVu zIhRSw;BdZ{OCyF&XLaQ=a%*WXo4{Jp9@cGN-juEJDSsNrZH+&Zm@{>SSJp0RHT;>< zyQGAsoJ@`LJP*9E5~S`yZ=4@O-aCsr<^<`sJL({Hx9wc8%e5@DSy-%lSx7=q1!V?l zj;x}Sc2Qf3Jm#_9OZ(|NnLaM%YU1hmn{lP$N#d#Gsc9b1K;JjtUf)meMsG#m2fwaU z(L)+-I}rTSIm%Ve-POIoT`V*`>|lnE8T({@5YsHCRHo$_{&AhOFOcU-5%NfRh7`s2 z(fsT#-Nq{h3Z>>xaK&kVuEmu~_?mPnP4Zp!t@4DXdXkzY_egbn&ilIQ(}h!M7nIwv zJ9v0dL3=@)q103Isp)nWQaQ9xSZ{Yh=P&yi9*JMlpBoz!XRzdgsjhrBtxk~hXT0Y3Bs&&uMJw?P#`D!W5M8-(5pCE<4? zf}^rVAIOw3vyy3Lq!jkR`8}v~&{TU*b+mMjNQBW2w3E21hkA>r$cbg*$HgVaT}#-O z9GP}L?eEl?$!C)ONv@HaJ^h5|rBBmtSnKIOawWB%ZGkPPElka%lvPsI>p@XY?)=AD z#Bni5weL{Qu~1Tp$C|&4llnTXuy!l(-oMMc1W|Bq(&D79$%9iarp`(q?iIc`|AfF? zt(Z~Nsw@i7S5iw`O^4g<4lkEsb0m#w5IHX5se6j^M(`|0gu7OFZ1}~{Db5&s5xF_p zXD-&NfUaU#BCQff+7PrJZlzkeU zY|);)qPOJ|_BFu+9ErghgWW-GZIOyYn#LALKV+h;lm^qk#cgYo@ix%X_s+B5V|ucB zi+WFcfBA&JM4**_yzi!OLExL-&blX-v+v4Vdus4d=f;qSu66DX?!KMUZLFuO?%RA%~a*FI$X3K@7I<&Yb zW-T!)>9e%$T8zF_-=cTW3+s*aS^5n<#5ioEn>Tnk9WA|9ir9TY)X~K8EqHnGzTg+Z zZ5_`YBc1I-sH?c^NJu~Dw&1GvfyyK%#2|hERf^5(Z#Fh8ZCT)wFV2%c{Yh%sR4XM{ z>Y>!rX(c_^y`Ow7{5||#{m1>Y1CO*Z#y{3+afuF){*@~$*Oe@4jGC-$R{AJYmGjCM z<*715c`pBywz91Bmbk{V^Hx1F*N3o?Uj!RLbV1huvoxAj-XOPgshDJ$%v6sr6eJYcOdf9MYanf=1s z*W2D()!WYd#kf-k{a1A1OROQN*k)LptbeSrRvnCXt(n`bZ`L*Mn;WcMygd0v+er20 z$8ts`zw(zdPf1kvtN+*r*<*s{1dR_$vv0NEu^mwRDP!a~sh$)gg-c)96n2k3COyS> zYooc*kc@u%7wxFFRePv~=ux_*J^tBvOXX z2O9B$KB0FJ5mHHhGF+tb(L5Tp$3<%qMmxbOWt}y>My!$BY-%1dGg}4ub6yBl{YBK) z(R4P=#qO}nY@f75ZmbkhhpBtii|ShSx*DObR~{;3m61wy<&S(scFV&g9~;jG&<^Ay zke@HSB!2^$l(Jr$3(Qt#oN>yyZu~Jc!!$~nE6g3{2~#tNSWeWoXShv-i^9N@4g<3o zC&r^vo+5Gx2Wt3g;uU|y*YMxgDC?Bf)v9Y%vkF>`tie{Sm5TeV))L-;f3Y_4?cz8v zqT{3v?GJQjGrh)su(m9T&6N_dX7{A3@)OA~^~3#mX^GTXN@wfYa@LY@YS8!iO(-ow zPKckt4jQ3`%n0<1h;95Br$7lV@Gbl>U&%+}79k?VPv8jsfwBCD8vP|c@;W?JJmST9 zQ@$FNdN99jmEwdCvU*{(2duAVnDx}k&qrEr%dkTDUc5J*_v3G^c6^Vx&mZu)WG(8{ z9by!jOk;u3bYkP!MtT#o_{g5nrPRw>u(Na@EzjQ32J|-_#TuY)d`7#|7C`SQ0qYKfp>Z3q}?LnW!yf z@tPOoGsJ#A$;!YlK;E*IjaT4Xtm4*cYXwiY`a{-k=+1xhsLUsdo)!=lu?on_P5e3& zFtw=vP$%-782k^}NgLw)cZiJ@2GUlGuBVxSUDcusXagWB>GTOI#UrEwJuSk?DEgUv z5#32CItKP+BOmE}GD@hV2fa?di#Svm3&|YOnV=3O8^u(dqp8rvHhL0h=4IdxeMt-P zOq@a$K3;f5PVxogKLmaH#4lQvu$n=90RP1+iIMy=?*O!N25-(!@}ImGj}dvrd|ru6 z;9a?oS0kw+6K^j*h~IW;olg2HQ-vixtE?I`>e5~g49o34&mjTCW^Lv8?567#_@~z$RMDWT_Mqdz>=LnD07gaz(CHBI>62=(23%$ zH4iI$!)i&k&`Y8xnaZ!T?cy1KCn%jpM_P5oJ9>?@v98cKtl>$q1Gs8OQ5m;|z^Z16 zA>=9S)Fw^?jXMOL8%UdrTUI`nlUTqomyo99GLI%N$aiZo*U3LX&`XKkw2FumMM)15 zLMQMyd>uaj2Y$*g$;fha0J)r5E=ob;jdyw*U zJ8|%_qPcWKjDnnclQtwEM({f%jTGVO(8bOqo8UOhbn6zG3~k)cFOoAfjS&7+^kgl? zb|B;HXleeI&!@+rbxC3kWM5cp5xeLEZt`7_)k%7VXQFPJ6`1KXv6;ZLl2)Q4RJe$o(A1JiW%dk`zlkEs@#8Fk?Bog1s>YCfCVGez z1BQQzq|lu39=DmBjNoP1HR#q@5-J)2^FCuO5LxI!GLC(Kri9W9q9uW+;$O&qF`rzb zIY?&Uzc%(SbMPRMk@X;3!Ch>jh3Qft%{DrP9uOyaHgS%QrP0uU3^bJ%VSn+4q(AeC z6+DKNhSZ(|dEY`mi5|2fR&ps_NH6fxqA?=jI(m>Q27+EinkNg*D}zLA09}e{q`TMIM!b&7{4>2~nFp690%}kV^?x zkq9f77)j@`8X{m#C*w$Uwuk2wgq9{t!Mc>Eok(|B+P}culi*MP7Olx5@|_2Zu5_w+ z2}^E9x{6ftjDCP#ohB7|1~P|6z&8bv4`d2?&&!gjEE3u`R$L?%=`0}d57<*gxfY@h z?@02I%c7}hMJc(;6X6*M738p342EkkDI{8nvgDn(LHxkuE7G>`i&aTI5dw=pZ#l_l z_Cvh3w$i<9IoZf-i=Xs6sm0SQmFA&UxGtu#bnzD6gTf0X@%vVHGM2UEH^gkyb(8eFVO@M@hcxof{+{K5sO4`nglK&oYo;1;8k|vnw`jXc9o1rG`Iq$q^GFP zk0aU+BEPH~m{lyV4Xx?Mmceop`9ksyY|U_D@eAS#tA+VB7RALfM1#{{yILYfPv*nL z5VnC-1J{s`uA>Zj%{@r|sQAV^z;`yJ8uy8Pq&HtC{=f#0;#GUP5^PZ{pDS8GAD;;! zwt(@NOs4Qmz^W6)2}I)lBp*%V#jI4Zfj*=YtO0x&Z4S+ygH?D;{w2NmLmrE<%@e=G zWHOK>f`u3ggu_jjklXwxnM8hzUZgqrw>|Kv)5S49obMoGpkI{`VPBK?;-@tVUU@so zC2j$;Zb&nTLc9P8gNN66OKTkBdrk0oFL@hLgxw|^_+_gWDNY~rtbC|=LH5CCWf8SV ze==5t@GW8r&5p4RfSjt3Bm59ogDh* zVqkRj$aS3Uy%;FY^KHB?9|oy@!e<4*of7glSg_W}s!cwKMD^=Q%!Ic*+f$$oJS{P91K zTm|^*E;wU|m?UmNa|^?+vw;7M#H`O@+>^u{#H?N7Gd>%LtEGyyq$DYbwLBy4lHSCQ zIQ$7-Vkr56QRV}~mx8E18WI=`HZ&tLp9Q2R*&rT3QZ>P0S44a}ia8_!ADo5k?j0D? zMc`~F!NziusbDo5kRixo39U(4$WQwq~8U*CZrQ) zl^r~4e?;5v7_E$(2RR%;R+A0PY%h#%3a+#ZpC=#>DTw#_ke9fXBRBu!jgh%DhJu-(Olp$Tg%!a~~mIe#0lFi7OYl5w%$ZTq2=4E0S7~=+*r3WjUf|b2P4njJaQQg#lG%I555<~?2{v@nj8T|Su zq;ne9_7fI08^><(99Hm0Py&QTG=T1}gtj+_Cw%gM{c>VO+F*tyF}FmlOc_Y0C~PYi zSp!)e2gl|nI;m8&P58W#TNoK9`X6sACq{bBC|F)u@ep(?uy z=`KPQ`w7yygfTi{ud9%+9>Hx9&c7AceE`OO3lNc9bRykKXCX2+rGw})x*XMW4D!;V zcs&s{5qez6KGSr0uUelFnE8;c6? z%{ftFWEW-eJd;R4v`U08AB1n7LF`BaS6>0rs)X4#MHj{y^aqru|IiCmV)fW$wvFup zU*C!KWoy}O_MN3OKZ}s+0%7VRRg@ChZq|=gU=Gy&YiL(mhDK2n^~rEp+zDVNIS_Y8 z@yz@KnDf&>7S;fD8E?(A!102KDQT6l+M~Z_ier`5 zqu#X9UZ`X00>PQYmN0Mv(jw`xlqO}Ad&xKCyvi7519<%IKz4(aH}Xu`CXbg?=^oq6 z#YA0z zqGoo}0wUGf2r~S7Z6nDz1O7k7>cd|nsw{%u|BK$BJ+PDd=rH*U_*qZ(mQ|5@Vnu38 zp;88^tkhMSAsv%^Qa3qS963BZxD71svvJq>WRy4mn5V3M{0Z_0B2v-6 z@)h+_LD<1ISnwp|SFdPA)(x|s$!4)0tOU!z%Axxr3pzcLrRwq)SyuW36Amar>T{)x z(o`NJ#RAK_$-kN~Wi2zi0F54mk<~GS&8Nl{KmPmLqPBDnhmTYR+^QW7sXMWmq#zlQfPcb3j_XSTQDG6(z8eErEZgBl2A1 zcdd-p2D6zN0ew1S>@#*7{~BG5vPO0zn^DNfW#q*3F~$?4ggM`QWky<+fDhIH^41gf zP!!o(B)rB`0j8RK0X7jy!x5P+qR>X@-KxQ^K-V`*Ho2#~PyQ%}D~*&+N{CWY4wEPw z2*l$Pq+AiUH=5R@L+K4jy$5YVe)7fEYSU$2Lq|+C07M`4SNdE1k$xV``A&T;koLLy zYW=FN8Xd8w70oT?XEW7|H&e`L^!dE8s_~=17-vDl+91xpLPygV@}0!umQ1qInKX*+ zW#u8SpHe}2iu_Hkt*lh0D_`Z4Qb*Q=CXw1S0oIzCEr1UziGH&Z(6Wm3oN(~H=1XG) zEG?gru3yj>>ZA2x`Y3&pzDQrDkJB6Lx%GVdcwql0jH=MRx#mLiIMDUp=r+2Co|+24 z$hx2}Vj=PZ9X(Ee(8=_Zq#{d_X?1#p)@Q%jSSd+rFJF~ol)s^0H-OxqlP62LSRRb) zZ}hV4VPn{5I*A^jjoE!RpEU*Qvj}}RcZ_?+BV)hO+F-_O{iPnOr{MUi-_}>^?e&s+ zYyG}H*Jx>eG(#;7^3u#k@M$@DJNUN)sHiQ}byMjB`Urk}G963@LbJAGeagZ^mY3d1 zQ{)hg<)@Ne?W1l|x2l!Ye9Bg-0P78Ju$~Q-MoWj-b9xx)={VrQZ`c#EnTJ?C%{FFp zGt}H*RE5l{8cmE6SkGA9qq_{Ben9V~Pt|J}qs@gDx6Xrw_-WR%!ueeuf&BL+&x_2l z1TD=jv9qizv*_ zaZ+1!KPAvw(%*6$c@g_AHdy1#Za~S0nxBmth72w5Z;UdE8y}!iZ}dd{tX>D@NGE-g zkzgiRG9O~SH-DLXtPL1XW@OB-_)77LxY;vCq^t0WrD0bU;2lb`CTth0Aia@#%kSj+ z$}^=0#*^1p$5z?)Sxr=`%RN~UT8B<#2c)~wQ8t3Mq&wLFIaIkW=_EU!V*X_gfV>VH ziTVcpr{2iuZ3G*W^!oY~{ffRvm-RMUdu^=V$^2bBB>)ILraYOg)p{Uym><8+-IST0?D}R#6|IU(*f-8lab~ zsZkgBdkZUzOhcDCb@1+wpxKx+Cay$#jFdu<4%I{=*;%Osv6K=bQ{Yr^+y!y zB)3uOsdv>2>KJv7dIqtlnGy@cW;-f@F07&SFMEwD`W;DS!{kd+0a}u`!rHwt`WT({ z5!zU-f*!!S3C*sZ2vpL1nhCDS7P#b32uQ{+bGj98I?Z_Fx%tPc!w*`o&1a?wzATFF zq9^HF+KSzUj{g8oF&MddIeG^gR-e6R?<84yq;Ra`YUPtM5VAV1%$9=b8sG$1X@vBE zm8U;NQ&NEakbX%X+C!|e63x}-S!0wwMcboI(;dcYy`|PSFfA}jE2GD2rvm@_XZoK6 zj_MiB{-$XxG!8)bOIiJ_80)qf49=whDtSg%fEn7!9znNG0C#c18Xr<8BP&4ODV2wR3@q4)iP>JnXySg_2$vPk@MuIm64tKX*nsUbRU@I2VQ_zv91~w^pW7V z&g#L&P`#cuJ#aiQQ2V2;*UkhCKM8ydGn-(k-K&S`{=naXYk@l25RGc%0#yP-1OC8Dt-W3h zfL@JYk7+~1k&PJ99(b|5thAIl^06G5fiJk6>JVmM4$06`Wiiw+1V*{nT{X}L`8l9t2h`Pb*+qp z`aP|()R0SE+cUm0RYJ-`VWF@3Q@(1~~oG6!8jw=yrPBl!eu69vB!PBpjBBe&)L~BYT zpplRo}z8xBf{Xl-m=M3eR%ngaxUeB zTvztV=agy6P-TVES8ZUMq3W_ro+?{%A$bRKo9@V^Tj5um*jILn1!z`k2f|+!eM!$v z#msC>*9YtO^g>1jbXATqo*PkEIoT|2PBjmkcTB%IAGz{MehaxW_(0?|Ur7|IhG2Mz z3alX5xaH`sO=5@O^LwxmNTZPyCGC?6$qVJ$@;o_AnXP;WtKCZNq{b_ml+|))xs!Za z%7L7@sFYpWha9Ym)Jz)7TF}YBCGv_!e5m!z9ASP%h8$uPFuEa1<}qwWxX~6F#ycY` z;=wKCHnkwNYZm1l`9&Uvp8kD6henfs(DgeC8OlvM4!P49WGLbAVV98=_*f0F$u$sT zBIIY%C8?%7S$-v-l^x0?WslNPSudBs$coEHq%i3YWHyV9MCLIb8QBVC77I~<{^E^z z2)NAk=5ON~)^3fl!We7JH6|O&kf}zSIgsZM0Gs>7{B82t z6&=&7P)`j6_K`^XQ+OuC;l=1HO+$=q4xigaY9|epdO)Xo$Rps@56BbcJ@O#1%2Dz+ zX&P#vB$ivUBlmfWIgMw>>9_xo7N9|8#UgN%8=y%Suy#Gok{HiN1Kp>_Q{?w=jY7y~ z+au4pXEwEFSxaC8t${d>Muf}muIk`77_*;V+`+VTlRg_)9Dj+JJi!@7vnKzfVJXab|KiQV946>x$<Lze17g7H% z0Gf9KSx*JgfjmK;_W`}ZBWMfeWEqhEbw$VBKDLFXqh@T!x=Jshi&vx|JZ3|T{|XD5 z!HS??;Xbn=qb@9QR+i=mi=0YbGzwV9DY6=Dwq3;Yo4g4(EgMg^82^Eu^ivqkab(c1 zt!N&Ki1f~y545^9u);Iow(9aMJR?wm052tGptpS~kfJDL)xXitoRd=e4cUxA)1Z;# zn2RNVEnLC+AQL--$EU2eRGXEQCbBndKSmM>`QAol)POYw*R8N?nB`UU)Z4))tN@x* z6IjDb$Z;OlIs}I-e)Fx!>bIi%d_KD3TZ2iz1VnZo(B{+p4o~O*@;tzGDq(EP!IBLD zYmo!E$O_bQkAN9eLY6&}76Zam7qZw(%CaP~1XW5au$(o19qH|ynPfwmA}Is>4uHsS(09ZVi0#|;r0Mp+p; zk{!0P4p`Pm?4Q_#wKxp~b|3JX`ap(;0W%jo9tg=jzERWzOI{4PWiB9bKJY2UfDDZW zhUW+KTnKe}C~(Yis4f264^t3qMrqm;XyZq$br;$h=O{*Z0q1)HracGU4^)2=GVCUJ zbfF_91@q5`&VroeDDsfmK>2O~ORP*HNCoteBw*CzkOA)%?MMOA0NwN5!R_xARm27` zPceK25WJhfv%GvBDGOfVk{ARg<(ha3f4$a3)J=kc8tBm;j>&jMfR4rb^g2&ORy3< zc{KeCvYLi*P6J{%Q^fJCK>f-96>JUmVG12Yu3$B?m=)mBvSC#V z0Nu8c9Y7^3@^#?l`tVagf^J}!(g?6fsP;rh{sNq0G+6AuK;2pcA)mqDz>iaC=oVb> zZ!lp0;o;Mu&7q0U6ecmEI9Pv;;fYqRxp}30M5Aq z);$C}!n%<|Rv7Ta2WA3LLJk%n%z~z4-8utFVyNs-@h9eVx?c3e9)KITUPZCcdInUz z5cD}GKL{e#bjTn9tHoT3oyx{CM&JUhvn1#68JVL%5<6B$;KgKXg)v6@xEloUm!SQB>il#CG; zuTCBS)9eA{_cGbX13ZL%!e=+ZAX(_NhzCLqNs@CwEQW)(eZ|`YZGR(jV_g=3-Terj zhl7Xv50%eG!PTHkqbhvxc3h)4vYoE0!PZPYd{?>&Rd30cBW4JiM|v4DGEB zoqGjsSVxNj@hwQd@?G>YKY}d2}ym;?9ET@c^3$!^C%?5va3e475;ItP>8h-zngrWNQ1jI`RQ`DWTLdH@PEaw{Z zuq;P!%4_}zyiYyM=O*~gndpWo3+w)YIh-bYp?AkbGVsZfJS*8ud%|l*V7*=g3poq3)9*QUqq0NZ9>n_C9#_{gmj*gHNfyciY#DBnn0gs(pC8EK5V!> z`1nfbXP6AHHcq6A=Jb@vDk{_Y@ZH(aE%2Ru0>d~OjKn5j>j|_T&kK*&9rG$I!e|K~ zTA1L-3+ z0(Y+`W`ocA4en#CCrTBndkmVuSLC}OC zjN?7N)sP&7Oxhu1`i*`n75!DOz{Z?{zwm)w3@6cG6^f#ZD;BFe3>LQtYq|k`eH(d< zXjKD|Bs;u$Fxa%L=+_ztzj;RVg>TFOc4;1B@(W}Rr;$Va4Y?jB2XLJQ;Ighm0$-sI z7Fdgb=t6!052}P~_X5|IiuqInmsAg{*%i2d5V(ZSINx_@-fF~%Yvc-au>-D_1<@)N z=jez1g0&H0azl5}8H_xj9vGJxoKL~oR)Nd1Lpr}iCS3Ck#u5phEfk+W1Gn=Q%;hn# zH*GQNF^E)S;iXoBDS3^Nf5q5jScwBXpay2O0i;z5>{%ypIgw!N%0U}PL7ysv`>F<( zqBQu#^00|w;2Eo7M2(Og4}~mt!RtPPpZJN)aT)N`tFY5I=v4TC9*R21k2=tCK;^mu zHL3}oq9g5#=YxToc13U82IN(PQJHi{1>78uErEYEN4^q)gA<$yZ?+8kKG)#AZ{S5Eum>=jmZl}Z<`kttKzK9Z z{8Q0^H3ZjgfKS`v`AOiHv$0RIDRxWN!+2W)(aeE8n|XlsL?D+tgR|Ym`Fub(2&S{XcC8Az!ezS9rgcnzRIWwG*&@lH?VS|d?Y^aks^ z03$sK?6exH$1$`wvdmW4C%F@tP7JGzI&u`70UYHTkd#=K!W1b)qS%9SflWnS*&0Ym z6YRaq%hJFVO~fd_0xO=2y|KBF5zi4d(0_Urd2fAgSa+;-=!Pw4`ORzK6Xuz7%q8Y3 zb1QJ?b>;!{5~{OA(`^;O{?b$EKpn!bAuDbKmg58b@*=F>3i=l?xjND<^vPb8ODa22 z^$$_6shV2I*334**2`AK=C)l|2dhEqawS}uB7c?|N(UHYjgbjog*{c{%PnF}LKQvV z$YdM<(j2cX)T(Mc@GI~I*v+>Q$;-R7R;8ZAWdn?cMFo?VoI=Z0ppz|6xjMe{>`dQyZw?fHXCf z+e$;(Y~+sd$OueS0hv&@47ci8A=Z2_2ddFVzoeDbo(Coe+6CGN76)W)CeWrA+6ir! zc28q^6Mdbo>r;%v=5uqT<=`W*PxlP)@L#+JEUPftO$yL!bTEsNu1W3WfV^5Mtln2! z+qT(`WAEl4b)GsJ`&+-ON$OFxA8?$v;H!4YH>BUF)*8|s=n*&zkH3z4u*0`FR%#aZ zAY9UI`Z08Io9OD+wVT>zZ8`9+L)sPXs`f-v^&UW3yMXbyV|+mGbQN@#e=}pODpn_J zmler3qRQTdIDDKm0dmre9RvHbPiiUWP=xYYU1a-YJ7LRf8=%H2Pn0xZOD)vqYA#+IV$N)UG2lhyG(+lbkwQazcK53Sg zs=d>`Y2nyMa2PvLCL13O*(5*;^O=*)uV#L$p*6<}MhETy@SaU!QAL4?cLrA3RC+EA zlm7w|p02*JHM3W-&#^58*0o1kb3`)hEgw`MmV#|GKpXes~zVoKmdX z76S%Z403B=_BRgU`jz!8`crMBRv(!2N=WR2wgLU|ue8$oDm@LFbrzgo5W3sbjN;}> z(*SP20M+dgJ{U}P2h>EF!3DHnuUS3mhSU{ohFAHazO;?7_p=|fEm2o1MU+86Rs(q7 zP#U1?eYu*hJdjsQOV~wJfQ?Zd6c-E7VP>M5?_qAl{{}QMmw^fTTkoQm*FR`If%fLr zS_Anyt-aA61Fs*b?>E{Td34Fjgncl*pqD*OVSF@iSWc@pa^p9A3-X~<)JAvFn>U1c z*-2@DoJmPlo~xy8RqUPY&ux`#o0NO<6S@5V@um7|eW1Q8u(IQoBC>%xzYA*GVDzvnBtfDZ^?0UFfOFyAk2Dalj+Caa`TNNPX zbNJm=AYsL^6TKYIg-)i0$hPXyOTbFcvDVTAsf~0{E~8vg`q*OZ!|Y4!e%o$f^*iM^ zauJ}0f2oy$l@nWtt-YEDf%PXk@G`uanChh6}-I~?4QVvaL! zK)>=^rLF8}uke^Z%$vXwiS^6MA!;Bx#sMYBOwZ6UER|&f=G{j6BfXNpDBsn~whH## z_J+3a7~3t$Bb}CqVn0l>(p*i%=%%Tf{Fn3^QR+UT^8zuQ53`1uPmIe(8SG%+Y>l?o zLF3#;q!D8*)kkSlfll|w{V+dx^7IqVl6+MA*J~NL251S0xRp4up zW9FwaeMZvBB5dG2;iq~u-@VO)MFS(!62P=C_ z74moNp6-HJ?L(gJwKkh`z`PtVx|^@9jlhi}trtdVV+v5}Fyn`|0Hf=zebCR~oprib z>#PqkV$BoQO^k0fP`dK4qm|}FvlwE?2tc0+BCPFX?Aj+IvX@9xO31b+dJ&iT0-K0>op_ktclvv7OKpo_4y9$+3b&+_%Cz2-|cyr zM!aLRL`x&NSvtAMhg*HEZ`jx1w%+KU10Msg^<|cde$0#3HuJXm)spd#3w?PO>nk|R zo4Tft0}?v|Xksg?5_U_>f~{)k?y3r=XeFX|UFnBZOgW-fwOFT^vT8svxe0g|059$ zMCT}~ro%KZy@i@DKWl)oMau)_s>*aV#a1LJG`N}Lt@EKPJamhDvvZ1CLEH}v#jeLw z{_S+OKGY0G_a(!+#;`~rzK>aFs^h<8 zUZ7Td3%0E&_ULp`qHX={6@n%QZ3(Iye9%E$-Q6!;g&ohOQAT!exAex|LxGyQ7C7wR zuH_@wZOa`4f_ktZ!}Jf(dYZkg-9~;btM<-Xr(AVzbnDKkwzlZW_wdiwQQhMmla@8j z@5!cxf~R?H+|fVkn~iSJx?}inf@H0VzSp>DdBjJWS-LK{WT!k;YQgTK?ybb%Tj|ym zK2xalDQh8*QPPz;@GcANG~5*O@!qp3?SdyL&`0YRSmCd$*P=^< z_J>>y-p;6A+}F#$L2qZK>c0b-bW6;!uW)Y-tLXY>i()%@W~+@+Cot2qG__GGpiF;j z<24wZN5*s`!B~n2^VnFczk^R)WfbB+NO@@^I%Af|lcc)r79paJ^&R{{8>4`^)oL!P z(0EoH`O{)$w0hDu5IantIOn)lg!T%{7U~W5(cghiX?n_|^y|Kw{zUI*?>hah^xElh zpAC*8J$E;q+ma*U5XmzpN=0d%UKGW#S_tRo>TSZm2OAo>It>Eq9QKOA;eX4d#t7-hP zIQa*=EXycWu`7_+X9q8H`a?FmDunI{+Z5K*wMhvvzNS7-e4iZcY3iHgZSGP0d&KVG z?qLz`2QvN(BbBF?3*_KU#SQbGUYL)ywF-}jUKXi2j*tafaA2y|P21uHs8!Z~n<6jw@TOLDM}^vhD`~s;B>lT+yz1-!Y4%(i&*bkDBlRYO^S*4CKgD4@7?W5OWW@4 zE@GTlBl3rZD))SYleeYj);H46>^{$AIC!kBdIlwCV8(ZjP5ia5jxR_Xt$94>61Cqm z;!dXS(>)|V_J?GL%&$MC}WPmzRf?92)^LS2cp>bI6sMQK& z3M>s=(OrB1jhBijOBJ_z%~mq_rgN~Xo%_2xTX;mIJL+rrBKuSQTvD??@$qJASMRd) z{3-U-Df+yiW|0%3?l@W-*;4)_Uk%idGTAn>enzlvQdelF7-yDI;kD%}+Be_3zzG0|5;TtJRH23So4mIRh=D0zdO%0-M&_|o2_IqkwGf_qxUijb_YYS)~Fzw$`Q0VZ-uO4HhMd}W*J_Wo|Utz z6XXe`zL`bOhP~ohjX_#v|3qJ%Ky?#+yizY@(~Z=BZ5x8tIQoV-+|@AuZkz9}=4@8O>h)`&i?<{77Dcy1f3JxEW|`m4WPG^CGYH!hkd z>{?W>tQj+2wC^@5`ho*CqqtTvty5f<*iUg8)0Z1nXq;3T7F^UeN-ZGIrD;Hw&Wbbq zxY-iSd>f+4Wo)w)8y#jg(1m^+Sx6hBdti(2u79I3fV7kcs-~L5cHcHK=!7G$Yij7Y z@TwW=MZS)DoY5Aw(%n*u@Xr5}_#+S-lN6V_CAC6IUe7J@$@MY%XOuUnfW9DYt-qUG z&^0HdrQ8{8vScrmF-vxmxsBto`Q97ppNC8`H2q}UuU~uPlGD#xAEo`uzu-Ripi5$; zTnHHPHuMY>=H)F1xUU_enq1b_OATWwW+JMayT(1@KfZy^D(^mLvrbQqn*duPF;t zYbN`XHUC0oR@jRS!(8D~O=G;dP>y#B*RP<@ESyx8OE{m0-3+ztHnGmX#SrPbOqk>Wa=@oe4n({vNP0$l(HuuudRblxeRzBv{ZgBytNv}q2U1MwW-DrolOtIoxVvTiq4mpXuZ{O__h;4b zTVDuAc3YZ1G8STYdKse^V)A@#Hvau-kv_?4LBLg_vup(V_GU{FwspZ$NJZCASC-JO z;b$YeMYo7L7P6KPPs;nF(6>CX?GoE3w@D05_?mLf6hQ^TbXPOg!F!k!=}!AR=cM4t zN>{d48g75?>=!a7XeL`_bnq2QpO`u`rEtoFl=^9>J*@+`&8yTYAANL0ZdFO z>x!{myX$Y@A0GInAGLM>d0Q?e}aL@w?=9Nw*VtawC18df2tw6=OHa zN#2s>vHx*+gR|JG%Pr(qwvE9J97pVJrOK8RXyR#?wlDQ!>dds4>ASpp0}D+`gO!@L zZnj&>NNFgo1{D3Ewb&S=^$6SwwA3e>ZFyV#PhTtk+H7q!(<`9&ZaI4K{zueVKu3{v z?YgWr2|9tL-Jch|w)ox$CrEmf!fr+U7-?y6OtPD}NvBl~Q5 z_q!MRSNYfLM?^|_JBXLV@+3T0iSixov?GOkV$iPOd@zlaPLU++b=b({+nmLHWqv&U z*zD7??4UTjq*@C3#$0G)=hmX*CC=64ewV`^qWKYU^eLj zzP?sMF?Xl@wQA zSk2T;QV7>ozxU^k&+k9)`ce1KiQhMVKL4dAMkrqQ=iqM6SMpFI9=mL_UDe$0969Z` z?7dx!gExnK40`Hluh@-&o+R;kW4pynim4rYF>Y*PJO4g&uzJO|8vd-Yc)yyf@8$f` z0C7cc=I`U{?yKp)t5+58z_{Ji3;WCZs`}pfmit4%(4N#^8{5p2a-@1vO`%?rH{svf zB(GHu+Xgz$Idi-72R}=)C)vRi4O6yAHpNXmD!%K_O5dyetox^G%-lb=KRaVzdQ-~N z(2P#m?`byj@DDliyBh{wc9(Yzc8s_8wlBi}l}n8gU40^9Y5auv)CunrJ|~Xyp7wtx zH){~KT?V4>x_U@$qUfKgLddGuW3+{*)lkTKGHrPT-{&;A`z~q1Q9YipE5Z z(#RX-%i!Uwkm+1fR+I>JgOgaK_DL@%tD1ae=rUJfDMy0uSMs0te`Siv z1+P$#KPBQr^c+fBTNw=`0IT2&>}J)S1)Le3Go6}iqN^Z0Cr$0)YGbpOKQ1wE!kGAZ zD5TF5nt0~>vKYa{b7x7{rSZhK3n?GulyWEXfW8s=9A!B18|W~fZSdFemGxfqeDw_W zX7OFcL$Fs*CZ?O~q+Z|$rt_Ue_}1H#$NWZh+tSSdRd-^I+kLid_U5)f z%0e@vehnO7JI`p(CXen}i^bgx4EX|mf#JsI^%YsA;@iFh8ZEaxgQ(GRBa`tNgpmgh zhZJ7LTh03!TPQ15d@v}BmgG}?B@=iSUgn;BH$CWx)>>v;kiDVft#g)J4t^KhKICmk zhu|L0Z}K)@`h;3>>*I>V=ZLErv*b^=*foh2^loMx`K+y#NOiTQ*{3;%Ik!2T&VBYb zwm92b`1VrS>XCV#*$BZxPmW*Dz>Ajz3v#?KFEgMKc;wUI6K;|rH4miOaCrqW!=>cY z=QI0>iiXGk8lO=vaKtx#ePD2?i~l78?uJjWApA|#dpWr8n({zpuv$=a*+TG%ZgswK zU3bq2QiHn(?+VK3(zT7|JbxPRyTmSu|Hhw;Eg3T=rbc{aZ%#vy(O*eX)Cg_AEt#XL z*1{N$>^MedL}$1<))=ph1UL+4fUG}3%&3#B%lnia z>I3bw?XLYF$0p}A*JO9Ipl3n%-1VGWw3B2>7uR#cc{DSzOML6NX7Rrgzx$>cnTc7| z1NTx=30IG+yVWx4NoA)pPra?Z1^Jc3ehD0TXYGO-r^L(Gh-YOL$MmJ(^;Y`J;LR$b zhvGFosZTQ=fu_%8JV0|h4I{)QaQ;`pJFf*rb(Z*WDg5*bS%m$?V%|~75s~6?^#Pj-zNq9?(l6&ws z5_YB85G$hb^#RtA0Z^u56cJ-BM@<)EPJ9%w~ zSYcC$!i_askSlqL_&`HrDP!=+f7>tV$@LuY@YF&}J*PXd2d*0NpoM=L6+juK;Vw>- zU6hLG_z|Kdo#lh9(%xWQ3nA6JYbx0MKg`!=_|~V{y|$xT3w59T7(_xka{%b<+(c(a zlFyj{I-o4sTUUtP>;*&hkFhJj!g35gkgetpX_9;!{9_emD0v*j8I;ptSL?_X@S9d7 zb~Xih+!&scGsNvO6SMsZF8(VVq1#~(9c>H%88{z~j@8C0QKe!E5#5(=CbGUM2q6j#z9?Gd+2k zd5L|LVqF%;YhI9evxj`t6G)wZ#AW1&1j3;$@#tO5%U{G$lM{u_1v;n{Z5=|i^f*}F zL@BLYj;P`Ycx~2$>EB2G#zk0AUdUg;n*EXEINpFm-XF+1sY+B)Vdwft_T)LDhtr7T zS$w%2iFuYJ-kOndaD#8K6Y&;|%va*7Z#Z6op}8B#wtG&b_#@|g4l5JwJ#Bl>=YE2& zws>#zlGRn4JM9AsY%dYRPsA!i@zs_h;@F%x=y0+Wrhu}YCod%1dl@_FdU|drJT9AH zc3Hx!Gvv|4Nb7P};Y2L$^!88M`bNt;guGB6jhY6h_{bmQnk>pO9DA&%UNU#`#Z z*9JdPgJ;co6&w~((`X{3d-(Qg;-#0^M{jVo$I??+6n>%$BuX+IwN7HB$>h{TP1DL5 zXme&c8=k_PuzZGdjl+NXjds1|cnt#V1&EAiWVb#hs{Dkkm?ykq zJ+oeY&GDW-{s@Nj8!_KM#GMn#E+ptkzsq>Bog6OO7WBU(8CpOJelsnISK!zJ%4G*p_pKb8iMMZN9qKA7DhjgTwg1+Iq{%dV$<{243YUUf}zzz1v_su7jDm3;8yWbo8qaEw=W0FH z;;75_>VXSv95|ZstO>u|jI$M(oL01}Et*c(z}bs7cjumZaCS%g=^Z$GbGO~O>p}dr zSl9>fY-pfYtezP`zYOEQA!vESI7jlz2>u_%<4AKXeK?Y19M8tkr(^j0SKydJjD9NU zmzjZM7IHrtB=ua*#jqPJ=2*&zSe)$3cxG|5FX3?of30&VV`e>D#pjm)?^pri%KGoW z_m?rEi}=nwUW?{R(dan~d9*Tp=X1{GoCmUK7Kkp39e!TmGxK@RYQX|dYerb#Sj=Cm zO;#(c(_)Ob+OvXVH8W>5t+3j&lEZpuDWA4JvxM(iSD4S$qB-XUe%bo{|Jo7#zu&id zz+$SmPS8N~%M^4Si@$y%bI|%bnP-zhdQRn>#%cAO^&gQV-m$puqxc)eF^ShE1im+h zIXsSI5`8|7&shIipB*1KCh+Q*z*t$MGnA1V$m|}(cw2KDTqS2e{_DZnGccnq*7^?2 z;nvI=t1dt*=F@*O*qT$;%&qso+4%+frT;js*R zc`?r7o! zCm<(y*$r4H63}|%(QxB9aqj=``1QZPG2)l_$@3TY`lM|eCwiO-y$`0oSHtXHi^ z>$NZB&c5S(kN*4yzOuLCU-3%3=J7TEzY;G4=L_+izt(BJXMOTr;C1VB|9$5FfAS@t zu+G1#VHD)fgVotSZes$#NinQq(kUf?JvSd&n=Au;tqDNqM{#X5BFmu+b{xA&5 zV;vSd!VqTgVC0S^t%nBYwIz$J89p*F+eagP#<4yu**=bS@ZV~f9C&7}u&Jz&={(M4 z70zI7&1S96V*O5IrOphj+j;+!2ulN!U^y#(IV*oHSm<@^BWpP~1$LEfWYp|nm)Xl+ zvxoCAzV<^Pkxzk?JVlKA9C+Gu>`WIpZ?Q|=!e+j~j&+~&Ay~VI;PPIgEB+hU+diT# zeg@I|iyhD6jfeq75eNFjs*K8}}XcNN&__tAL7ZcGir=V#}rMAHw zG>>^`n+wrKmV?GyjfS!b9mUdGwxhr7MvplFQt)^{v$6Ei)9@Dj6VRRRqC?$*`^?fy zVHiTcdqW2FTXev8Xm?-G@qVM>#i8L@@pTV+o{TncX|8q-Lav++bUinkS}2-qQgpUd z94R@|q1RuXtnvl2LAV00JLEd@N~t|)r*r?QHpvUW#uv)Zb_Ek|2QgP$zP6-&y+XkiIjSc1c9aY-z%qMR1eu>kj! zH}E%{zxmL-3*d3e#hI1=^KjJL~Bd}MB2Y$5_pQ*_I)%dR*ts(}(IF?58RiZ5w@FkSy zzDmIRH&7a)o4#lejyYf+SqZB9H;AE|$h01e#X$NfGoG<8+?Lg~ zL|Vf7TY`_~EE(9#$aYg0uYKlf=H>!gya;RcIIvj_rzX#QA6i5x zyWn+fn}Xo{a-fmrz^XqE?rI~vGj3%Uk74)&#lTQ^3Va@VJfBg#_AEw&m-*PGuZJP z;7zKD&Kt#kz8$oC0@mbaY{aYl&OPR`%xtZJ1w1-nE6-*p+R2qyV^#0MHeW|y_6u0T z79W?jKH{)WZ?cD9fl+NOzt9!7xx#2nCE)jQp-UAfInnWu$vTKf_b%)_j zI)p@fsZIf?vM zD#(77lG#3n8MzIHArN7dR$jDY@k+K2apBCgACsm%vbJ&j(k+%`jCB{`Uo59A| z&4|bIC3Gh!B zWtL=M79Ew`FgZV#TPc2Jwdzxs!L-vC)N332FZ(=4Dd%`+TGtfUYWEn|MO!ENtMQjv zM@gslkkg6?xQQF$eO+Yy^yTzMCti*p6E`D%xaR^dOIY`?5d3I0{dmDc4ZDX^XXVwr2LS&Xn$>?m|I7f;uJnm?TX|73W4W znjdOS?9a8M@?_S(Acyy^c}6$A`4Z>F7XRJm_sZD%o~^!b=RVx9Yn^fCfO8HCb$!*%CWAt zt|QK3j^DPmwnw&ewr;d|hFI-C?D-bICH82{*4QTTJredMtV>9f7*5Tj?TMQcmL{b3 ztnqEvH;S23O(mtaTC1srfz01Qe)2iB8+Do1Y2)GlDXJY-ddjch+g+#k_tygB?$-bI zkMYg$R`+)ChIs3F79{RZyy&UpyXgM`YOpeq%tEqVnWo;hb#Qie7Yhyp1)wJLgifNu zOd6M;xJL=s3TI(w1E=8#b9~TV%JD`kZ}a#^F*{>&$JI_amG~;LWnxfb&O~Yq#P5nL z9+x8iS3*zkaXq~hq;AoMX(iRA@;fp;YbuqAAI#A9+Ai2zJ09DAXn!dwrQe3vZ}XS; zHzE?fSsw&jMRjlr^AZOp^omc6yA!`TagVo-znfm2x^Y*Hr^XKIvt(2s*_Ju81bqrw zoitzaOUWWa%LGG6?fm8}>>A*D?E2+8LYrII8>kb_y8c%Qqhiy?Jc~IWw;*9uVw=P* z330I0{}opwc67|Am;-SUiNU^V@W@V5TBtphchY|3R3(t7)6|k$J=+s|MZ=kCq3nSrTy3aEy3LW1wMQ`=(aXm z6~}g0iQrpFdL+%AYdK}H=Bi;O}rZaf6DGTL)Iv07q<#F)fJ-c>}?Bdr=<#QQG6 zj8a;;tR-;_b43TqAzwn;g;WbJ9#qkt#qDxGbrp0^cYkoVa8Gj9v2RnqO1F(|zI>kZ z37Zqt#2SqKkc1=&Q{vagMZ{K$iHI2(+bh0o;#W^I-z~ouz=4&c$0mzynSD0(KJyuz64mB?MAA28oPI%6G zrg&l!|4p3lx$SwAI3z)hUmQ0&K6BzY?`(e$I8T}x*)SV*vc(0N!S$s!V556z?})s1 zx4i|a-IrYYwn}F*haRB4lmXwZ!j+rdxaBuM`kw|TTtq(%8oZ#M;7_J6)FX{J!v?== zw6a*MVSnmK56l^!1A9Zvm=A?6Ap=RW2R98X4nrR6<(N$c-#!4$SVGvQuuT1?~_K)^|>`TeVdT8rv3%6~9jV~SCS|4Cuc}(VCeVFFw zlJPhc-%&q2tF7@EMesN_;8RKmFYMpq7#w#m(2rw85>VQskpvyd|LzXndZsa*?32gF zC78FiGaCvU?_i%hu3zB3WA&s)7rya}CX`9X4z8o*~V zM@_EnQkTQokx`wcyi<$XhuDP{LtfoGrMPxlE1>mNZE7p{G#10-<=_rdFki|l9bp9d zAjQBn-AZ{&ym~v=T};kUIpr34oy(EFosexMq^ag~BI41sqcQxq8;z%Wr2gAKkQrYN zEPf1GKKsf0iO`#(;dsdrSw&u6JEJ&b6-ot#o+6t_kEgIDHt`|qBFu)JxE(o-GvQOa z4tjPlzZ^nDu`>3+Ir%&qYq{WgT){br(Y#+CVJadNvnkNm=zYs z#cE%53*1IasEe0MEy{h4Q$E3P@|LLS7I`_dc#LV)!}N(IXufUXnyyHFm(#|5;>SIV z7kW4`AG;D zvA-s!`T>7oHL?Wzm^-n}G+KNK4Y@PS1u3xLThW>^=)upVBXS!&7!%2mOr~t3b-7?p zJSgW;Dl1pu1aZL>^HCnER9Bkeb4iM=bQNsiZTT_QfrQ5+8yM*zIV()OYc1v(`LfiK zOpdnb=+)5ZR--Y0!vYyiu77Q6ZVbmd+KcY)Gt$Cv(Kn#M%O)8iXv~|$G(5VU%u>=3 z(HYI78h(nC;*Z|S_$1QFtEoScAo`Kp8wOi;1@!q^*qQ0b2UB^zhfMLt_(v|_1@wy| z@?<5C@_{V!x8`N(w(>$A3WG**>AkX9%|h1wa`T6@RT(0$fycQo@}(U4p;kSK^7z)q zqlFj3@B7&thTgMW4reyBG|!4_*rRD>qKf8j*to} z3?Q4ai(g_n7oZ}+CbO12oJ@9D#bIUm#@tD1b~KWj)5v5!DLo^{{;NnKb(RNWSHxls zFE?+A&Spn!h&g6Hd8X9HyhnC`ORlK~DKNXRV|YRFMaezom&~E7WcXi{6?(Cx6e3=d zn|?urSiV(hf->3c2AW}(nNp2bo{*dP3mF#*3)5Zk-gs?%6Wg)I!^|IsX3l{h%_mwj zz9Xa>N>6i;9z(^A22x&m7nY(Sz8Eu+Ccv@do1{zT3A#f`$F1)LxloUVy>JO&)(;SvG@N4MoT*PKm$Q4wrNvX)$~syF@M+ zMmoY4vqb7_B+(y9&y`1FKYX3%&5lYC5_~(D2frfV9QncS-I!SyjW6wikpqA7K>TC< zjLc#v>v}b4w<<<0a~Z5IEu>e-HVICOEJy_(w&_=64f8CWD1n!iI>79Q-C$6&5xpt| zI_9dB2gK?%m>E(r2X2E6Da^cTYrdz-Ocyhq7!T&DsMJZEHx9rhRFUXIefEPAXl0wh zCq4525EHdVIQK4N^S*`Uat0EvE_ZNPJT;z)I#MlpsPuqLKM--vGAt{p`rY z&6053ZZ#+CH;m&V28N1E<_O~@v84r6nfy+bfvr@UNMsGB5ucI61z3Z1^)S4P-;FwQ zKdkgyqLu$EV1TLAL&yQU$bBkG^aE|Y(#$O%F_ubAL{I%X>{OkQhFf9Et!<=)aiXgf zCKnW4g-a@fHkDTS$0QpOPm5b#VU9DB8duD}m43uh2KX*9p%>4Bni>pvk(5TgEKBmP^?QPm^zS%s4M?)E3DzVTbxhRAI!e3c1Op((*ZR zR?jDmguBsCEr`*0wf4jNl2tyc;%hfb8dI4Umz8L$9=sGwmA1&H>`J1ML|G(#Guo=j zl-qh8@zJcUdje`61>f= zk(RTOn}WzsVL0zV=NJ-G4dfPySNDJ-6SJasVw(4%P9|xeMVWenG}!a^#*p0Jj?{xgz+>M#P^;| z-iW3%K%6t<6aj)uG86FiJT>1+%Y+>skR|Arva-PV*XXTe04o$Hyj=&G164V5Q@IE8})_T!aASf)x=Xz2uU>8Uwg z&T9^$azsb!YSfUf8tu(Z@&IW!JpY@e;#7Y)C$ABE@%Nd=C-yL_ir7*k5-nvT41^Bu zXN72B9>Z^#+p3O3-I*ky6*|F_G8Mnr-(<+QgIVR9s14_cUp`b*B@rC12?@{E=&F6 z%*Iiv3|xqtqy)X8NP%X)2zHX^%!pa+PlH50_&y5byRzcsrPwEvfn{w;#g>Os7o)S8 z8IP+E@7`Uh5s0(5<}s0r{c|{XxZ4(=q<%s^Qux>9$~yi4o?#IiiS0OO7*h{rk{ zW}OYpfmC>T^HR5FGaNCo#vJpP(T6#^1OI8X(F$!}h+6UtbqaS8BdWE zr^&!-0HfOuvpRZI0{ovD3_CX07O8^KR|G3ADTCOd$LJlU9!gp>x2R8Sbg|h`q?cC7 zN7!4Ah-m+N@em1hT6YjXsY0Hrc6fQyH$5^K?X<*Uw-YHpg7h0JT|(aUmRgGbx}jH>MycnO#!`9xvcI_Gfx|OYN+C)Z zN$`b5D6N%Da~(-DCdbdc#YQN zS2V0l{V;tJERk$7yj%m(l~Xv`4_ryA2CfnAZ69vJ|CzYm%^<+gal!E13SjV-XZ|N+KKfDq0&~7&G;adgB5$6+!$;`J!V8DrMB`&ZY1?E4~fxK zYoey7+=*zzMtHOHQdeq-l2mRWe}@IkYxaeCcdOz+9<9eBS|YdB?n)`l)Y3Mw3xC#d zGf4fQ%tuG>XY4jJstxdrA7&T425aIydA;&QsV=9-$Nm)dnckLNLVu;jo|#NNo+Vg2 zMdilqBB5AJU1S5U&0ca7YNr2`5@bKQLkGF<=hVhI#*8cnru8-qe@kG0Jd7l*O^qeP zbSk~Et?>iG5S#>FH3!@_H@VUsDTkO1OIT+3Afu_*G#MVHcKFYoWDQovZd|~+-vQR} zFcq3kn+27sJs2Vo94S9WV+S3(TF$T=fHX>uEAL zR>7yaju{il|2v5KWFkseSAHdThZ(YzIgp6lQK>FD%d!$Fj_S9>RN1YLwWU$dz;GR} zA2kbVraD$VtlTt<;T1S)R#47s1-0EEWw08=KI&urBlm(qextlvxQ#YqxV(k@yz_E6 zvgw;h!>(3U2~%R=#A|J)px%?mTmzE(9t_#*sTEg^YGK#Zu4-pE(qyM8bk+o_nFTAU&FrETXmKai5#FH1 z9u@PEY>{NLX!0H;*?J_p#RuG+J#vLvohVRdDLZ=10uaJ$<*sOtUTnUZRI-eMgD@u^ zrX}R^cTs-fL#LvkxTwb)8(^Qj1&eA1n7HbJ-l>GIvanJH?vK3Ya$}F!A{8Rzr;YN{ z9B+7Z4bI3YycVtH5c01l3M!Ql2h1ivgS+ex$fF)GbFGy#T5P9iXJd?Y;+(V#rqkV2 zWvYt>-Ab}B*E`X3j)>l5=Q!o-?0(0H5gsIFUjfFuH=-U9qil+%EFmuW9fmho00pv` z$bK&(Sk*x?J>{NWN)EXm83(PX)|r~vTX{Ty;qn1#Hk!m+JOU@A3fTWynA;s-Y4nSD z*v(pkj_6OtyfLDR3Gx>7^nCL1FQHrH0GE=8RUAp|t|KV5zC;yE!jC;j2JH=ZolbwfCZ(=Y#=n6|ud&NQD+u8T%Wi`}=4bGw}nRBQ~2FyP7#i6!HwX z`Wbx7!Z8%&Xb)QWDlO;>ioPPT**j8SB$tT}GbiBdm`2nw2lr^zhdGPS;|VNbdC>+Y zVg)}$H=JgcAy&JQ^D{oI$5<;W@$m-eZ+*B%Df+V@THZc*La|Cgcvay}S0KX{vR4)Z zCAOY^8^|Z}!9%tHbXRjwS`U#}HR!`Quulo-qE;-o3ifIYHg{Vht#`qmCvab#>4Qz^ z+2im4y`i^jV+q~{1y%)Xa1+r}t4}Pn>S;9XhF~&M;}M%ji@TxMt)eE!eZKXR-NegS z+@WVyah+|n?h3yYLzHwQE4vE4(1kt0!hlAB|5}8uJ{TQ7KPW^iW?Kqx&or)-4!*7$ z*rCh7M%HEa>_tb+fe#`-n2nrZAL8jb3k_z~rFn$)RTf`g33T%F;L=K?^X8+?{rJ5u zoCAqT-{Mo}LAG_}TEmHQALo^~#923hCJw^_x3Gv_G`-&(Dd@)$w00%!ED!$iJ3fJ6 zum_a`cRH9}&kG7L5e#912*OX*lDW2t$f(7^+8m#*)|PW0u%I4D&O2ut|<@5zDojQ^(6I^ zno(<_wFJ;IQ+2~1)L1D96L>Nuf!vQKL<-8Icf^bGVh`-knh{H0pdXLB%lHJtO>JtI zT%cOXHJH8U>q&^{9EZdBC6C99h1A12N-gP?aDCn(&v7=AYCbc+2xtL62=$Wm&2Ooz zJQ>|&3l)Jbv%Bshr)0M*VE=NfGgKF?nxHMyR=|dHM^kLo;ko}orrlm!dU76Nnn&w6 z4>sfpQf>qOmKDt4g60JHm{#dt{}STf7RT5&qZPia42<$?*o>=U(Om%iIbI|Ki5PDd z!Si;X8CF(wh1+@%EFdbmg6+uudPrZ}#c0?HR=}FGM9)qwBuiQA2+c{7kbN3h7I zfq|%r7uhf0XP^G9mex9I-L#=_q+irp+nn|{WC`AJY<7IMpSJbZa;V9b@<`be%<7Mf z^h2?ks>b_`WX4o7qYl98v5fjTU-Um{(ChGQp2GI-!m8iPn#+z?pccrCGRT(-SO=tC8t^(D zkrqN4Cu=Z6|A3QjmikyNLT_iX9k(sBFLLa0E_8WZQLZA+?Dk9QN&JRi1Z-{aXxD<> z&LL8Z)1VK${^!0A-df(`u#Eic8=zMaf6P*hX9wab$pRe9ks#FW$)AezS=v zMoO@vi=^f9cqI=J{tfuvYr^JtRryFx;2JH)R@Kqg8SZ-S8s)y_?(IJ8oM)RZuQQGi z@xJN(?dweTXajU@s__{a^~Ju--lCr5o(-NS-hTc%#xN}Pg=l2`wW{hU*@NuZi`Mr| zd9BsAyB(+Pi)_u+b!7dQ_y2|uZL;?kQLXmA^*)EcEPV2%e6Kts6Wb=_O?aQs#xu~j zMBgulz!tq+zDiZd=3=$6hqzilhXJWfUL-zP}!SVKW)qTyT0>S>$ZF^;Hp3)@-czM0-g=O6A3^E^v%f+J|^k^H@k zdZGv13ne}00Tl)eF zSu%Lfsrp6bkrCp9*-FV`8wb?wtn0JO?oRF=>6YAk9QTxr{-C(4zh?b*C4?K@)t&b2 zj+r)D^_bc9;+}VL1!8~1R!V3LT4Dg6r7YSf#}fA_cP0mM3cUpkeJ^#i1yYX^U1QxT zTz71ZZ_@^yaFvP zpA;dI8Y_t!=7tZhrasmfZ0?Zfs3)~NwvU=ut*Mm5W4qdj!ncr43vwiNy>$I{?Q_j> zO>+Gt*E6&8liE%%954Mz5i`!yO4?!n>OAV0ul1LIi|l$?@92bQaUCXr2T zuIa1v1QCW#CEF5DYTyU zk&Z%+V$|CCth&@lrJD@%t$D?W&<(12obv_iZ;e4{GE+oNJ&*6MXP76gw}1~WApN|t zh_yTx4a9&gcrS?MUc^!k3XhR!OhdEXgVsL+t8N2`$hoY<5`6Xz8c$#BnVC`^d9+eN zbJ(jo#yED`OW7_`#q6`z+Zi34K6GM|{;qGza#)&f;#W_vWR<@Fq1~!KFzyorJ!sUS zcGE{HGoA4%-khGo-kbVu`K#?8$3^=CZ2)XpW3Rgcmgn)t-k6zcOdscgjAoCi7x%$^v%-@SD2=$2bjxx58C3;!ULzR;2dUx#MSMn!n0{Do$ zUjZ4sh@GP$n!^V6kO(x@3dFiAzyvp&wX{ylpgmIk>S^t#t*v8}v%af{>yu-S?JItR z=J?|$;qmIF7ErRlw%!>JqMNMM7iKmin%3;{jPb7VWz}C9l2luXQ3q<1n9u9Un!cmv zw{5dMWbauae~^k(4JEz02+1iFpS%aoU--7*F&zpzEEGPLD!!%O#oh+K{{Gq2a+nIj ztT4VMLobBAl3)yH4=vB0bQFwTCp4mF*zbE_fX$7x@5}DghCStnR6_1XU8;#N@s(4O zsv+RX*1`q&udTi9rIx`q+up)C)Ro#9rLC4yrNhu2WYjD!90oa9U)y*9V31j{0w{l@O%^R@PCeI?SQBiQ0uo zzlp&w7t@R>Iu*L$Whv|Z=50u|u*_6id#Kycw~nDh4JNmD1zN#qqQghfQzZ1Z(q#WG zq~%)_I~*?=*sr(ZLtRc)(WP<~Dn3-xTG$$cEV$%o?cD5KatY?Tp{=aK3_fm5C1;oSa~AH z&GC{}G3-V**c4-pX2gn3>}RTpOJ&fj(kgxEsoGS4NFp!8vYCiaHZ3u+;l{)Tj!5lt&CB8L92xQKu1koX0!oA5s%-dV+H zK!vvUdW2{JJA#+}Zks$s`KmsrHr_BY+pAF<u8hRM&-OoQ-pN3tX4#V$Ka7D=2q zYfRAR_=Ehfd=sf{wak0L8|7Q*p9c?R4OmWfyt!quB=9_o>uB|D(dJKKn>1yfOHj`$Uq&o(PoYjjusBx-on058>?iO_ETe{7;>j0 z67(Sv_Wt;C%HW}YLoKaf*iYg4(sxrG=9+&TJH#ug3jU>Eay3~yO~%U-hlLkP9OJqeYn0a8QGczL?;vxpruVDY@TCCpQklw;Nx_ts z48-Jqq3gFIs?|_*fJrw6m{u9TS|8XKcatful3AZX)%0x2E9Oxs8RQpX7%8UNzzIIE z<)%VoFUK}VKYJ>6pyODbKd_x{!y{1|OsK~whgJ3kjOa!q49wSdFawqK2_Qiu(S5_H z1&lYrTqI9XPr$3>)6QsXsLgjuU5%G)3|RCS`nZMpN~}hA?u7lf5yWXdv73nGF8w(= zpU=CT`bT!SkH-6s`^x##>1B*O*dr3W6T)qMZBflJnAHKSN)BhKU ztntQsJsfQ}Ef|mE=*|t%+JliB9#MceeHk=dtY$e4*3L(zCReSgm4s<-KOU0>%(`R5 z5kC_lxn#UyUcCYL;x^uba(Sfp)))Fu!6#D*M&-i3Tj+K>ywzdt2muGw49$OmC<9jc z9oFm-EW$*%%u9k9j>Z2pABk~`dRSAHuT;PLtY*s@1FDqOqUMf`UAdR0 zsI}$PSo3eWs}2&F4*YgU@C|>2HT1QAxjzF~p>o(&?e(hgjyA$Od>iYw5uSxL(l*#T z>L~ePv!1Iw0((*ujLH}&C}yaVkqihh3~mIy`EF*fS#wP@xW{rUa(;dKqBPGM)N zwWZ-`LA{<3y*~U4xAB!WS8wK6(zfYQOW`vZVg+wnt?hb->JQkMtUo{8SB_9gs~7GOSJJ_|4F@_V-_oj zeu;{NjrFde^h*jC_=gH$08-%FOhz@LY3g^Z(1GfDYS_%=_lx1FGO)gF@WfWcQhE+b z?IzY_0p{KdxOJ=gVqvyE>K#ozu5#X;-j;B@+<;N%rQR1yXIKD>{ZpjHBEE+FE6lz$ znfUb>?x`jIzz<4Ebvkvg-ceuCP93SS+7hjy_7n{TlSE z74}&LuVgoQF@BuB=rIl~H${vw(it}6B_r`3wCg+SS{lATzWr!TX?>-AlY9@DeSN4= z+JO7&4mv0edsU2>kFRYJ=>JEIRvGrG=dx4DO+B%-$~QR2s)9n=qvpV3e#(k&sX4Wx z)S$Xc8p;z*Q|E%*sE;SGkMtTFxiZ%J3vg4rK&5r2=G7J~_H=Lr57s~FbBKgx0Ymk` z$PS)#3;g(t@F?yf=JE$@%Y3B*(!3LuzkZQHH5a+}7~fAJbFK&$o57D&H6Ag8*ZU*k zz;XKD`sVp6`@($rs0Fqap6xvTiGI7j0O>x$xItC2t?auO;YS}rkH3%#gLD7L`mIg% z)VoS%wXHf#oz3hE(uQce*&_#PDK%9ap;fcpr@mYV?I%e%x8*M|Wz;4@p9$?Tl+~To z7(ykl=kSYfMuSNLn^AFm5hdxtZj4kz>KPTn%Y&vwR@4ag#?DF&V+` zYv^eonL*!RKC$G*P^#_jW?kuUgdg{x^hZ;ZF*7*NQ0hgtquSaTzeDc;x4FiAlO-{!w+>L+r8UCq)R8uH!UKH&`GSDe)&?QmcvaJ?fJ}GW%G*lYeo)UisrI*;Tm73A(F{E&5BzS4;0aR+AA4eEkzCNB zMlz)OV0Obj;@bc6gP)NZoRpl0<;dUccsZw_(|p7NXbFxjkNktx@HhFDapTHxY71_gR9_)d&Vv`2%m5s zur+u)&?2I!Y4;HgHbU*8E`?e11$AN{sCSVaw|Vwe^-!1iE&s0p@t6xV=1k=C6(s00 zyv`4qJIT=l{sv`P74%nm>IZZZQ}E+GK`%>#*Do7+E(O5d6vtnh-An@-SKzhuvo9+= zvsi#$GopvkPUoYawn1iQKx!nwG!sXw{f3hjnw#r3#ap--Cis_JHx;{X!GQ0$Bl^Jl zK&DPO{;EyjD+&JDS}OYsoP^0B(V;UzqdlpMskW+MK4 z5v_NaTobDkS%@5Mgq;7v9ykHtXb3ybF8q>1xU;tCpY6EUelREv!4oi8^yQVBMAK6W zCuklO*^p7>qrElIb;iP}e~Hl*A{{+Yk;qC7{KoA#I^gf>iEnp0>t_qQ(kCivqyqI9 zj<>uaXn_&zw>$8`Ucw{mC$FLj*2e}g&WUnX_Q$?dbeKX$*HUE~vBDZkF3`?CG^z*k z8FtE%%$G>=G;QR1oj{|XKsHWkPe#1PYPAo{kC=u}J)1LdJhz3Dj6$ypJ;&?G=p15%j=w+}SpsU1e`Q zj^FhHUhUkBO;3J*75Qqj@aoQ^HKVcS+Ysq!M=WJBc$>AX+6|1`7S`%S&aw1)0;4%!{X1F$7{Q}`jooBy@D%K^&)Dq1z3_0!t>)4iAT@JnN3GvE$%*kf>m<#Z|;v5#YO-^Du zVMKch@mpmX-L#BuP5Q3_IgNGruRfm*;dPbXvJ)q;a^#y4%UA{)XA-j_Cl+-EWKS}l zZ)D~~Aq%Fl6Rjj?;26=#uC&8qya^(cel1ZpE9bEQ^DK%P*pKJ-oJ#{EE)14I{&)Avx%w z#9im|&NiZr|B(H#8BdB;@nW|5o1jc`ZTG$n{jJOwr59t3n66J79cmB8WY^zQ2JpeS1N?B zehd*(n{3cG;qq^O;TFGUMdCcv?#jvYgY;k&tx&;^6vl#IORVt#*I3E8S3pLW=Widr zy_0*`K|fhh>%+{EzZtPC%$=T$L2o2(5s;5D=qTeDnQz?PawOLP+B}kRN=GC#DSh9b z9)F9?=%ANYGdHqROYsJX^WkXp*|-}k7kDUru#s!d2PyXlo9qR9#ZzYVN#Yw_i7K8p zYtf!D%#QK&dlp*K2>Z_@s(FiJGcm^^%!korGk4+srqc@#z+ziD+3)Z|ePRt&XO6ey z+o@<@0eZF*_0&Abjt$s<`EeM8|Xzr&o*G5bY?W0qS>rRK2+m345ZCk zR(?3G&BvY3WhTXw5mG|#ZfVxU#)7F>F&0TThaKh|xWOo{a)h>|L&{{}+=@=OoN?Gs zCTM%SY?HY66x_ulqSDLhn@32eIL5OiYwslNV6#|-R>WST{|2zLWurYMsbqHX)Eu7qTSVJGVqIJNR1YHR``LBEs3F;cejdY^S3~afV%|hyBkaM7j^@5+ z5*^>qyKeT}f6#HpGs6uSXO1w}7o$mBU=NPQvidBNp>_D#IX5$JR}#~Iz_-^17_sJ% zG1VJy-)-j4eD;V+?7N#pbuz^+fzK=i%IY!bqEhU3nV2(giHy$^i_PQczXjQqE659w z9nS--fpgH;8}OS0xQ~>~>7V@iHT;slnDd>PlU?EMxgh59x*$jCiU|^!-!8@H?{d#M z$y|Al99cojDs%Tc1NUE=STS%HHuU#{fKr^tuccMKEM-GCMV2i9y^Gn#AELK^1)$#a98l37*;4p$Bp-N;xe!#eEA4mpb1REb%(0zLgH zBlwRw3pC|MW>7zB98N`#$STdpuau2($}9Jy>MblcNP=FVop#Yii|2C`nB?WilIx%| z>VZ%Jsf(PCrMFVhmg}ssob07D@d4dtWyH|;J&@tk%$9h(Qs6=Eg^aGjwH|`a+R2_j zirpxROp}!KXbJS6V(bTB>4kjkMyWxwALP4fkR9ET+A6Z66Mxr9>EK;R$Lwl`yt3uK~qi1 zN}bN`w;rhi1BYCk@i`{-;}`ew?JDekmB>11iM(9OT_&RAMIq-3(uQ>Goc+1OCG=W1 zG|D1qU{CPBSb9weyIC!eR)^Uy3ri8`kH^`;&e1xrxfIEI9{!OKvbTpIz1Faj_kg7; zh%A0!-T-kno1JnreYl4Ek7a)EWh6%+Q?}Cw`RV0B$f@tF4DwsJk4b2i@0jOJ(O+t@ zUt2g(FRxBOBNyjW3z^+>(K%MKD{FE?cGHbu!hPZ-_dE>SVlq4DeXiS39!K9>+#|W! z4MN%XzO&D_W~7F($B$wb3?NTuKCNrbE}afN`A4Lq9Id{Uv2K>}6CxF(<7|JQqHbUr3;&)I`m|3Q8`=BBASmpg6~Bn1CE_Osk@q ztp(&WXjxs^o9gn7hG?+2Sq1SToL-p;dsI*E?jrSVSCQqC4{7|-9KzfVkxzkG%*}NK zGwuS?q!v5$S!CmAa{QCYZP4qe!prUw#P1o*zLsc36QmjPCPt<*IOB<2X$NxQ0qwJD z!bYNpG(zGIWp<_lLwXDRy)&R;EJT_`kZrS^yo;5LhX)JqC(rEiPU@dF;@uZy=e$P; zYRo9NLe_Uj`we9UbY^XgM5n1|W<2T521dGw8y zB%@F4WfeYSu4~BWhUng8lOW$tum|5Y7cj?;Ac-_|!`@tf7rSvhbNB&0aDm_Y%3eR7 zm1x86*~YI9BEFNH74SFrQXkZw$?E-!S!rqX4_F}=_&b(gt`DLq3D+#nCk^K1Z)|Oy zL#7w&v&!utJI^33P9e*#FsDXQht|c&pGKp~jAmFHjqEj2`aKn2cX1A4y-wzC4r0x} zWM6Xxw22bPtt|BKD(3wMdOHpGHVQkVEr{~0%-&qs;2qEevhXZ||1Zo0RCgovn4tT0V(9%FM6F zpcUq0K9^vYOh6kSh<4JHZ~4%HCNaC~GA<7Gth~(a%B;Lvtg0wxp;alg3|iZITJ)V6 zE;D9Bk(`}aQPt3IZ_*$2kZK9cI43%GUE1@Mb-15bhtWfi*jLG^K@wQ9?+H*zKCqy3@$1eZ1d;*}4nP+)Y@72bdk(uoiZ3oaY`!@!nka)Wfu(99IQ9 z!+lz8(=YG}HD@GxGhRda&92j(*dUFCjyOXh9%isM{4m@ZbkV`XYZz%U$g?n0r4t)Yy+Z5kgLHROw+?aM$LIU?h zRwjZb?to9aI@zZ8@m;i`cN#KJmT~Qge0B-etRGS~a?oh&2ztIP_ipVHeHp9Xw09d9IwAF!@uW^Fdcx-8$QOdnNe zK3H|mn+7=DEPJsYeOH@SPeEpFWyEF$?8bJy-;hsr;GGfN+a&IPBr>E4S4ziPe1>d% zLA!pSX?$kpzClYli|k*Gm0@|ZEWX=m=t36X?FL@`kJEM&d*db6%3E4$d4n?tbXaSI zI@0TN`2C}JWGsfe%gCk6cv?P#Yz!mb(FPv%L7Xk%3Xdd9x*0QL6dvPgVE#woUoA+M z(<#QT6Fr_C?d3Z8b?vYp4Uh(_#28oxijqkcN5tj0vB}tAY^5^F1)?u+!NQ~xML_8G z#rJ-dxLg)6$`fhlYxI}=w5&gR(;?=m4R1?NeCW4`0A*215K(PF9Dl5`7f*XWb%=UO zbr2<9M3kf=)lOW>E_A97;C+Rd#C|y+4Dnay&n|O_2r*vybApoi%U?^+XT*^C`G#y{ zasa?&9Dz!2oh*?B#up+! z4fJ069%AV2jK$zOPjQ`j4eA9^AzU#T9IkujEfZo%{ zB*qch$VMFerQVa;fSHUcSPfgqR(dI?Bo8J?c@MY5Xjlls<&t>9XHwmII+@`=nB%Wu z3+o47+%^##dt?$nb7R(hf+F z)-K zsgOMbkAG76B>kF_y?-u9`u9W|(!-i^hI#@^i7#j3-nxS#d*EN;AK>pv9U;GevYs5a zj+@{SJ{jMjGtEsDZyWg&f1?$>k_J#U_AwbshhZ*wN&LPWEE`GH!PIEYKyy8IvH`YT{ zcAHs9mbS>Hg6zi@pIma}Oc=e}jT%cX5J8`m10e3ofeiD=AxcAK6>+YU#3^sWFt`j2 z`Y=8jD?$*Ag;j-^$ai9>CB%0aP`0tsHxn7WOZy^WcbiJoZZv-ij(5*&cn@mREyT*FSgM^%b#1F#9JC>f>N(AZ;)xQVAlk$o>EKq8x zuaq&$E%_UJR68QPu_7gwW?#ngG!pU+mT)d}DUm}Pj5K|Tg%-h|62x8K!@lsc$1k84 zmb3S!Vr`USg(TGq?=U@r3$pt*l z08jnRSi;I!0D3bDZ_@yx4L{Mpv0c#ZV(@LOr$uSmIcBgIu4RA!C!iVhX0OV?-jJDb zv^1ib;IoOMKc}@KIpxm_Ku|H$i%We8!{Oi~@{dA|YDMQ4r zJHD$A3skmqdtJ8#d$1AY)18+8#=d`M^7ZMXuf=$p%2Es7)&bEh>g{H(7s$goEC=dP zK9XEJdogV`6b<~HEzgH>B`o_s9bFb~zXrB@0j`+|t8|1k+QK_$kjCp(05*s{B8I$~ zq@3*4{jwFGk>$`<{a#8;Hbd)e7N6~Hrr~6Ylgu|@#T%i(YH<5IR=A$1V_AJ4Bf0&! zewIk*_5tc9GjmhlDp+SH8x7CA*kb)3B_RAE+Kbjg-CvT2 zJ8Mh#i6j?S`E~=&dCngGt*1|Gh^r@B zZ^+76i=S4T=^s#Ar`%Ed^p#4xMs|82sW^_8XrmFoh{hYZ#yFq;kQeo85zY~KrY=18 zEZQp$i!4#=np9<9E7nt%+$#B9AIMyIz^*+n=XJinK8Sr0`^Mvac`-#}^P>ah8P|y( zgFWve{|~{G6G?)`cI|j}J6`#STpURj-YF}&mt26J*&E4-&Zw$ zKSttSqVDW%nDj4vRS|u4g5n;Bv1R9xmvz-U5BZF2bjijJT` z4@57DRgHa*{@#=+RL;A@bEZ1@q3Tc$bjpJAj z4Wj3x#J5R@g=`)k0cZ>5^3K8sh=_oQ(W4au)2`NA~TDcJ64jtZeisI^N-R4u5Ud*LDz`dkuWs zL^h1vXZZ~|6uOf~C!EJK1kK)wZ>zIy*F&h2A?$~Z@qc(PWCpYmJ8$pztzrvLle2Fb z>m)N6C2kyQ`exwLY2Ke~&laMxG%v@^?4oxe?jz!J)zE$~y>wTjs%EmMUXN{azVu`9 zSK_zEf0xH~$hlu_Vo##4^I|(yqE&@}Yso{t9QKachr00GgT^KnMQ?4gavjyS9Mnl- zB(1ZP*7%MlsbdCBX!*GAZ%PQQi!>Sj9cfE}6!8;{a+ zrU3*Jm&9H)TUyRc9lEb22{=q1#{wsSO_8O#EE}O`?xiDM!FxyfOgfUreM#Nt$-Oyf z^HDb6i+1BGTIM5_Y9Hx&b_BAhXg|)84|iv5bZo4A?Yq2sOjnn2vQZARGp-V6y+zhW z65GCaEuoE@bf~VWbYE&v`stH_!$X`G+3mnRpJuSf(<$ zezZ70Mt6I1UEb<_(5xc7Am5WVf7rJvw03V2Xfg`A1Jc?@YKIddzOe2cRX5gUJvB9- zo^aGq_+%l}{5?$3fVK6Hz9o;^l?n9p6Y%YCus}Q~*&I4K2VN^LhxZ3qr6oKNe4Z^m zZnRS?(PJ?y-3AtTJ~~FeQ$y0cD(N*$ox);gSnZLqRz&T1Qs&|gyt7e$)7xyC%T!A4 zg#$y zYJ*Aj7tADNf^YS<<&`Cehm#*$@&-4G-c%SExo(;&hFKD6}w5%%de7;$M-S{j2 z_}i;&rYwnf4gGw#9hy!Oo{EldgEC(w8`j~+zhIYyah2c+U1dyAf~|Ky8w*avmcUSf>s2_Wa!kJg$b~c&5ONi)ByGRx|77=6$Ok9mCM&Y}F&H)#V;jGh9R5 zq?2_InK#SGqd!T=QhbU{?QvgTt#MFes69Uxso*?Htqj2)noqVlp^4K9|%rlKOAws)b6mN@+y9d$VBV^aVI$tFr+T!T5pIz+8V#>fq zXNz2(i|0O64 zPPdJOs~6e*UA*YkNw!Bw%Jm{M^ZC<1tDYA|n=nW10peNyIoU{+k zh1{icMZ0EO?VDWl9z4^KuG+}kQ3tg?WAwM#hmbQ>LNseWTkcH}vSldl8JzG4{qhto z)j@vR-t3ol@N;r+5}Dq>{EBCfSnDyF+cMXwhP^ct`o&$8xkR3J_sl?9ztdzeuVt}B zq2U%NaTxwvs~)DZtk*{T2e&~f<4M~QI3|ql5z=cpN&H5GBzw;@-Ye+!0r_0EsYbJa=kjNoCTTa@Lu^CHx!3rBm$HEm!@1{^ zmG?vWpQxlN%N}`DR_Nn8GaYrF;Uw=r9qkEul;VRqBU&u_Gre{-#CWbpMc8kd{kqmY zPsdN&?a>=h!V;d1aK_Lda&xP}8R6`lJL$%5kYPAqXCQ4i)>^&m@p7IUFEQ?OjK8F6 zo8_+Y6#J#Sy}N+EuZ#QY+xPl3NI&-T2dK4>R=?J@rjT(bVD@`xqn(gpOEfx~2Vt%A z7t822Tpr3;CS&(}naS73`^BfoM_$Lz^9@u#7Y?fy>kVs7mnHftx%&f+`Kooki~j9m z%nfwjdIm1~DchKh_%@u^(+sAe?|43%aDx6QL9cYk9iw+Uvwz?Aoww<@D!8i~YQ2T^ zcp+caNl;5hg+L#6TMabVHs2wuIU0C?B~=o>D2&vG&a=@+`DCU!J9U~`z`D+By*F`A zd@a=aaBRGqs{fMP;`O5Up|=)J0n|%LF2zz&ky+MmvX$v8-gIWPV)S0o@;l*pXF@oa z`jJ@Y=yKN&ePs@*Nw`Fp{9kFxrDX1&$VY1R3doq3ax>_%2rjIQJ{#vcW#3gp^m(Qx zDLS6i9|Q{?;O|+T>1_2E&{+?YnYHa;4IcU*#f*L?(~d_@hp%sE^9?7}daLm01Z9sA zH@Y8no`D9wv0pdQvm@Y?wYweJhI;tgu5LlOmxfc;%sR*wx*Y$A{4w`62-|&#GWAWe4-ujxXsA)&XvEL`Lwg;fs`>lMP=no{x zH+KCboo;K28uYUpyXb*O#9p%O#jVg=D;mC|v4(SIpTqwn-FpzJ`7d9~LMK9u<;Cnr z(!Bs#v}J*0Rq%9?RXz!Rn4puvO8Pt}zp5!q>{T4UUktYviQ1lax)ySnBGWRQym?TJ zY(MPKlUM5yukOXHtXpK~e~T_IfXP=xE5+-@M@3uY5~NhK$Yc1bu`YX$%3J?FTM=*1 zC%IqY8EDNvxsgSBh{g1mh~s=!0UO0>j^niNWZ$f3kImt+z6Wk7P7l9GU#^9|Q~*Li z_28pnc>NM}Bbv2m{}UBiE?%%XQ;AHN;NDM?>219JSe3||?(sD#cp9u;nGUIpA}j-?R9XmPtIgj=qu6hm6(jRd6dU$Q3US|bH{9$HI zCPE6gh81q)59^$_23KUuk-ovo?*+lFk1`Kh3SoewssFMqDVq-_9v<1MK+& zyz1-G{4=!WB$oAk=&%?xbeYKZx3E!pcJV6S{h##|J5T1%>GHbnGcSEc(Z@_SaQ)nL zmd1bCo4sE>x15~67=|s0;_8@hH(r_Gk7@69Gn{i4D`g*vI)Zk4A5xl@y_`g8&x)&P zR3Fo+4S5|JTFXxQXnji_gfq%!MHsBtHZe`anmi~S?0 ztsZN21*^N-0hXZy;OF;^&yl<31PSeiR;< z!2eN{WO>~wLO-#i*6VD2quR;e_#d8QJ&lOIrV?;otZsA-YJDhjC#e7p$O0@zhF`%` zayCws1t5QLclH)Iw~=gtFw)?yZN|omkx}Q^u?f7RUCEp|_-lkUEJmJIXE)c*eFgCx z$X=v>Y7Nz}1L^$p;n}Wir7qf8IakfTFG_(edt@r#QtgwuAcRn6J3r4vTI=es(@tn+15t>D?=jC}? zsxN|YKB+bmUsYCj&>znEv-#{4yO)`%qjHDBM} zHl|*m{3#U~8$Bt0Y5e`@ow<6X)-91yXw50VA~ch%)Zf`ANUjaW@efEUwd8ZIp5rM3yz4b&%Z6s*oy?Q-@e>Z^1Gj^0M5@>e(vZ zRG&Os$liHgZ}|lN>0%99LO7LR(|JCBh+RAe-ag4p-y$2j@|yL5h^z9+-$i2SA4`TV z6q_0%Cq7MjMP%Fjrw@BG-i2n-ZSoynB+);mQ?61U|DHW7K&fByH;*AVR`E~hr;J~A zv5NMH3|y>M^C6o0RodY;vbzc0z7uye6d|1D@fFX|Z(@b7h^yU^xyL*{XRp>mT`kR~ zl3lwc`$@JenSUvI-lv+ZA*t~*>nE1?Z620^TbDE&#v}DI-$}@DD20k<>)J3Ex76aj zx-i<4To@PK7W*UCBYID6c(%D*$pP?a4|!pK(zj2mR@)}Cda5|{30{N3?E7+BFLsZA z@N|q1Wii?RA=J{CybO8eU-CNEkT2a`-e0IPe>nF8FG?eNay`CZ%EsORpWTqTi*2_% zQ$$aLiT=7))bJa%foHJ=Ti}@fblVHOc5!m_epc0!w6D_vWa{;^4^iA0&P*SMns0zY zLI<-q*kC`xAT#o4R~8`|_6tegOeEq~=zj^X>#h7^WBI~9;sf3md6Krdh^|>i%8i5` zUg3>7$d~z>Xu(mt*^77VdY0iPlIbk@*mtnu^lcJR+Q>rftt&!fw)CA&DEUCXK#u36 z5{cY_e`qm_y`7Qvr)|0$|1?^ru2F={rBF|Ek(kivye#_^-k3<9eGCa)hx5OS{;qnj zG=Jkmu{0VgjP#E#k3Aj#BUU3ifqnF7=IM_SSl6$fb`DcdUor26I zA{PhwYQ~%M<0^SKs#kxDj-AEEX^#4O!Z(*g(I1OihVzc!M-P{h{@=KNH5$4U*;J0F zF8JwgVyXPACVrsU+0}4aM(yg${9W(K)#xIA+FLJ?kD^84xPeeun8(xdCQ8tPVFy3f}>(EaEBnSH!%2I0ZtFvY&DOP7>_` zKD(JL@zw0{S$O7HwhaqDL?&W1;yNhuPM*OtNzM#@ooO#hk)S`Q5Is{@f!{^i8?r6a z{MmD5hkoNsql&Sjy7WeJFFBzh=X^SWEpS6Ef z&dg2xRS)qs?+{rmDjpTIe^Y*kZ+H{B@*vINHS4J#6j6-qJ@t9Zt)B+W(fY*H_Asdy%x9&TAWiVT(XfRn6*2$YeI3 z>G8} zcDeH}t5kT9-)#-MxgWl5OXD@*|7k*B59Om<%+}gWi>|bW!80(JJbJ^7hS3!%v^Cid zEfObsl~(|8cx-FEi#wppDxd1sIHgw=r8q? zsQ1puS-D2A$cgMB65|1PI8~O)YTlw_tbq6E=+zM411ym;q}#`4woj(&xqJe(bFaYn zWBKZ{{2))VX?p5=y49HkJLT?Nn0-#=|7hJbUeENDH}oy4tPOK^C+~iNu;!67ome<8 zM#|&weL9+)Di5VTugvXYkKgKdQrn3z$6^IeN%$dtS>ma9v)BOgc3%8QqGRIvSg5N0 zF_MkGADgb8y?nHU4(8%lkzuZ+x);|E7NuCGg1a2l8CMne3Pkh(8G6)X8w|aSrs|5P zLZ`~_$fG_Y@g4OoxST)x_w2bW|8-t3Ahr9ED#1zISx-$fF$bRrl(m( zHN@m+;>_=9{MIzYYb4fka-ok*liyVNZ;GW!gQ@ZNbvk)T)p(P{3RG5(cV=;XxU-VV z#^%$#W1_v)E7y-bDmK%W)@lRQJ%<91kYh9P{wrv=Xmqhm$N4CErWw3$MxWTHkJ!;) z`uiGE`Xx5#3Z9#WGF#8jje!CN8vRb5nZc^Yr$Y!2@E$frUoEpg%OQG;ZXMb>GL=^78(dbehllLZl-O6X67kOQtM#la6Yr%8enkBF z*eY?b4*ERZ7N3u{uG5$2IJ#R#&OZ-pe1g-*@v}A|ANTS~ekj9wj*&l0n%qVLwIzeD zBm26tsIT;QYyQP8cCseh@p|atORs;Iw@_3Lz@zx+GpO?n=&e7rb|}-8gd1pv55saF zWjf-pPhqzt4b_H+>jv^*E>zIQl|pULulxar?AQb{t2>YHRyg@pXmq9+*tI-7J>lZY zI!b&&<4+XNd|Gt;fUcQad1x+!h;q@P_Vw3TqgaFJaJcUQk|m9d0 z_8toPn|Ee4RI{1Ab;!>~_~;(kC2Z`HzWDE7iG(^JqM9z;f(Jg#u~G$ z4?)hMCi(#qw0J&p@Gbr?h+d{k#c#3T`y0pKcRkJ6D?THBZ|p7E{irhx#L|OF+9U$#X(7~@B zJ>^PO&23|0HY0IIiFSOf_v0cqbVGP8D>~QT`tQk{iqf{Qp{wLd%TI7xYPJcRZap9E z4`jWM){+(>2coS{ zTu67Uw-R&ti3Z7}n8;oZ86R(;oBeda@BCgHWZBiGbDsCDu`Jo6vOPjP{E%G=ry>8r z$5Gj%7QfmZVx--539lzM@gBd>VwP0}_+g5?s(BFM6tws?DO{fq>&CoqzY0_lB1PkQ zc-HaQ{%q&oCfVQh*9o!zQZko9#?rm^a4=dQWhR^OaDp}pnc_LVsZc4|lhyE$eI4X6 z1V67bs`IRUV<_S>_;Qgh(fwnydn&xW5#Q<|?uTx|aAL%75 zVHT69?ONLVA(DSO3+WZQXTJRoC#=3s6OE*g=8@zTAk1BC%ce$J-#RXV3dY&tg)sGS z*7r`R>3e#vc=mh!fl5IY!*NuJ?2qKf@l1_;4Ek2ygqCch;V2+Pr4D$`uyaEP+Hb`B zHu1t`)DiV%8Bc^YZ=s7bVnnsr8`F&bVp$b&y#5xn6Doi|uvhoUr5&)Z`jdKpaP>59)=L>mF&dEpOC_mAyGf3~= zu=a~^-X{4dpYxM{j2=_)${+B^VHoAN$T`OI41W2YMNltxCErdPIVY{? z(^F%|Nb`5`_+{*if9&43zVQqyyIOQs-*{GgSdGvHej^$66$}@sXe9J@H#r=#9?s#- z-75d|0Iv7}mxtI@$OauEjiS)g z{H`JYv1&f*c(QjIvcd1*E4+nwrx)qATql|;^jUpg$`1Hynb>C|bbVi5rdDJrzs?`{ zN1h8_mMyk8^x20?Ys!Tw8e4=?PN1bRWLqCR+!D$P)%n{*CQf2=7Uj3ug6>!0gMHQ~ zWU01sx7zM@jx2ybQTa18WmjXm-pF@k-iKGd;P2Q+>&z4*eLDa5Mpoxi$oYAH{S434 zGLH-8>;@ZqHYpG?2|_ozp5)<9)uJlL?En18F4kNtnJ)|3Fj*0`vvj;}7;BGmO2@X! z2N?-{_4kQ*^fNZ)RQlyD5}+SRa3&p<XZCqC7i>xPnWC3qPL^O*SqMUePQNO{*3=Ym_2wlu9j=x*L-G@uG@U}M5c`IJ&uFd z;p#8pwi{c{63)s~-d z56W2xp&sCuTF3h8!Mkvp(f>t4e?>3c!ADXIzFG>EbmH~sqWl+@xXzFaw4fxn^v6I8d ztM5pr7W}=d#DHqRdUw<8OY#{z=j4$`3-Q=9Aor>&CYpFI#3_@l|EEU3lAgcK$WNiW zin7Y5lQ32I2KT_PAMzcwhxmR$A+uyGyl7;@c_jMkZPqVyFXVf-o?Q23MrNi$h|6RU zEM_q;HP2P3=~p?X2dw)(83adpm`dY~deG*Bc?wEpPliG($fPKZEwz6(UlxY7HK;l$r*>&Ho?X5>&wa`ZcT>_rmrX&%z? zWKl;v9pd-Rao|9I-$$-?Bk9l19YEht>J-)AJf4@U@i}cBI`*6-SNRJw`jqyHSnu|< z$#_@&Adh0k(exAZ8sP`LaEuNM9cM1edzm|u>rarblk@ie92Uj`{)X>qvZH)L=gk_l8QVXGo$O=&_8p{ychG1@c(-#R9U+gbHcw*6m02mrsTjH_ z%?EVMnl3Tg=b+sGLHtA5GUJTuEpz+|;tQ6=uRc@ABC8K)wDi*$jh)V;w88v8^X;uX zVG-BLp;Ct$;<5rbejm>IgAD(L4Y`8XdZ|x;Zts@kw!n#ne5D2QUsFD}(cLB(!R=3sz^I@oz&df4Rz9QfIbRn{4e~H0sILZVF8>0d+Wa z)7{p)Lx?nn^C@du`4UE%fyu)dk3z6(jc2hrY=G3$D6p*0oI);yD98#jd6iFWmU|px z%;jPK2JDd9u2eye{662EMp}%tW)t!>J;`5BSjF4m-#d5$I8Yo6YlyJhx4=o|BC$^j90~dZE5E++}c{rhA*^0Po#}wz|4wfA@RB zW0<>+_N^EE?uUDNp|gHQbqlLHbPDLrMhG20nz7eHN4rOH^fSIS1V#3<3)lPK)qW4O z4v(8h=;n464@oa~f7bfGZUrWr*D&w2C;jTsDSAwh!8QEV-t2?VYk(`?>U%9n$VPUx zv%ep*YX4vP+nDXtm>ud&bK@W2TA{Pk<7RNbI|hrqiyiFhe_>Y7*s*aq`AIVm*X?2a z9caJ{+_ARngdEH}aMc4wI50n|#zxWlRLzuAgk zK^C1yj>JU*4?rdna;cikvU)xlD*sd7+eh-`MEyFt%QIFt?04{ubTi7(W9}Yf?c<8Q zUGqA3s^w1S8C`QW{59rtPhJ8I_3fViZiCWWdv0dkPGy;wfQdqFa}#4}@6p<x1dNv?ceUQe|) z*MZjgC3HT@I^ICSo+iS#h4t|(`zCn9FLvK1R-=x2$K*lmVVfL>`GOt@mA;iBP*DS) z|0bW?_Z!XeFMVFhc*;RW1@3teK0U>3F3dkJum-2oTYtbCpF;pkcym`m0XummPIj$O z-M!m*KWA%A=4}hzn7-f<_{F$FzRorl@%J>?3eOwOslYBI@?%>MlaA-Le+cq@7LuIJ zv$ufd_696Gj1C;cD?T=#0U6F^dCI3k-@75+zek49lX(sKI$ZLitGw)6Q@k_M|A*6x zFVLW)>DXuaHDCAsINu#WZ|bq`)!6+19&?qu_+^5fSq*lmCZnMNG+UoPq#ay4)VC(U z3u9d85w>+-c3EfEa7#G7F{|J*p8M8nA6j^)tw%@p=M6k$_Z#8u{$D!x7gQ2~R+_R; z?=!Y>u6Y}ee?^hCCUp5oW1E&=gC4GSE?c9jD~GJf5gtQ~udgfJEb8&Cx%#sutH&PCYbpMv$@MvFE{VdL$r~r zb}^c8QtFhv)jZEG%*gNNc-MZ^^+M;RNAmHKLGZ_eR(~@4>laj464g~h*Z-i3@ji8v z$DPJC6HZ@?%0Bbzc{3em6jSZ*xA4yXeWhw3VkYka;JCZ+WQf9=OtH~v?<=Ue}IC*?au zeWpBa3SPcYnNr+488~XY&jn1i&0~vk9?J6%@JZH%?m;EY?oabsW2`^IimUSLdq|CA zg}nT#NA_Gz7GF=2UzwL#t;x3YpzWjP{)d&{o5x}ia|?NMp+93IQZ~qu#-3}Fwr7he zhdv(>KGKkLe}rc)M989k%K1+npKnC>wI;8FXXP&PJv{Cr+j_fqTUV+>o}TYfo8D+i z-t{E)ZuE^7eu9^-2RYm!FEvB{Y*^z$t5w7}gA9%E*G5D(f*f;lD2l5}bDU#j6|7k~ z)EBx-p6>6O?hvGQRpUL)Y8LhR(~Z9lDSRP`8%9xEB&w>rl=H378TM@ZRgUy6n}0sV zybfCJU(nZjJG|9n7n(Xsj)tB4#g6=8bYYi5Z-|7^{hRmFh%NcXCjb8loh^p)KO`?h zRBwy#7s9%w-J!hul=Z!n%<+gThn}8)pxV&kb+5+(kHhvkybclbUH-ezW4}lE-+_GZ zgYZuH#2%jsXgYlEPqgK1L-IB4C(u#x{Q8x&4&hP6J29gTbQwCal(ecT*Er#O|9TaA zU+y-`zw`RDl=+?FGevzT>5+5oK(QGV7)~N9;{B-iqgJ4#l|0!eOIYi$ilyBx>{Hm2 z%66-y&jpGO?}nWU`QB%EgdGT@3%hWJpDI3Ek>yaux6a6~R4Jc1&6Ub~gm(i62A&R_ zR3yKKx_am~8r};lUD8!c8C%?y!bpOS4_q9s z7k`mGSo4bB z36YeOeX6+6l{L0geWHy2g|P*mEbo(*-LE{$#JBMp-PsCMWw;Nv{O%tXD}_ z3v?FfJ5(bb!5{zSWnQ3?liV}zIZ#A#uS=NCX;%JZk08UtK2|c8ti1`V80b3mi#*Bi z64s$;ey`6k+w*+7xX+z%r2>?3rtg+9pR(5VEdMJ;ub%0*9Q1HScPwLk#ogt!{Jkp}OTsnFnpqjM4r`w_n}kmm^QpM^vUop3>J;(c zu$F~p{kKo;Lv?|A!s{S63avxHh$$2nJnDb>&Jj}Mu%FO#@~CedCX<4W+wXgIta><4 zFnoHmKA)klzzGs7p<{(J-+i`j(hC$$z7g<9^c_g+o090dFj56-VB`% ze)b$B>7Kld4HEYtNx#o54(9KZjEK^9_cB=`)pe#j&(AFN3jxKfB6B{aLk>dC zFT*R_!~)hpC5Pa}x7;CP6$bhKUYKX0wYboy|B;U!Y@DEvOGZA|XL7UI{%pTmt0|}p ze|*pSdDp!vJAL#sn5-$x7S6vdDr0K`8qT_JhBqnj_I<`*T{L(*ulEJAhEk%bj~Ll5 z(ce(bbG8{IDch&0@$7-pU(to_0MFESx|uZ4gCLHQW?B8A zt}#a!l@rkA^RC%LC)wZhp!*yG-DJdzt>09!zEdI>nc)EV;_K|Wto9Ms_uE_-_gE!Q z@&=>&#Q(=cT3Lk>W}lXAb9ZDk8VcRwE|kOG(G}jc*Mr1lKDP2*?QN(V&+0N3`rh6~ zlIG~>71nP=WG*CJB$|UL*Z6BesA){ex?_*9;>8reRu)`vRpZpzL=cRjqyTEB-QC*j|ZWE)?gm*c&5@n>sz zNH@Ew`fIkcr*k7u%HP|9V>hGjPN?=1-7-_zCvz7?x9cO)M{MR3QHmk@)4tAXYR!LF z!@ACjuFNjVy%c>?_T2rsInkSQTeDM0od+Q6kIbu^-b6>;r#dV4{p>T&XZ_C(4fXSS zj)%&g&$e1`2Vi zF?P>2(B#ds^X`%dHON>m;TfADUq#+J+Ngsv`bPf;o7LbuA0_+uCsaF={B0 z;^TP5wq;kMi9}?P{)FoL&1^4U=xy0|a*^m6k(#(H7VVMyDSMCkT;Z?jF!%!8zXYd# zl3S~;;%w->ULFnn;F@RiMwQW_=X(Cp&sm*yjfKY?O|R1n>;e0G0UFwAcQ4~t8A&hY zjBqQSnI)6G3g1dQ7DgGI^Os7`o4nRzURTfs_Gz$e!JF6wGtu>bs7;?3J2k}ItMUl7 z^7(3H$|IsC$JlA-sp87w@ZDraz&}OhFw|s4j8NsUlwA4K?^5*P-A3>Wi86@yW@+w8 zzLq8sRvEs+O(en>d?|m?c>m;nA{&p`xj%4Yd48%Kjdi|`h-Dyv8Z^vc^7;y+dtKaU zM(%AR+{yDkKT?;>-`k zj#^r~(1Csn?L3`-s<@fHBa`}=E`McQy&|d78lNB4y;3hgx~#ReeLF8>i$*4snhU%; z4_^g)VF53|Z11I>OacA zdzq&p*g`*$s#SO#7s@nkCA#^VY^=Z4$k&cNtDDPiJzZk)oqAhE;-}?Lm%Bvf;8s;p z#bZCq1o%lDbgC%Cb5?O8Z%+qfDwbO%qLvX$`&g~t2(iKkW%)fVc04mP!Fyw5(T@|s zY#{zUL>#o3`i6^SjTDGQ4;J%(UVhIS+HxR_Hk}rn#R@wS=^ec^`m3FLn3Nysl$}a) zFdvmQw^lA}GZ}Ns<;!(<%GHk|{NL%a^1d9p$He$2=>bPHp`Qz`zOD9y~ zK4{H86X#20zZ8G{L8@&*zgI^-Q4D--<~I9WQtnMt`s{iX8*;;6<8%BCYJ5jlZCzfk z^|@YT|0&VgsQ*d+F_|)vM(FNi+*Onw9Vr&j8C|@r#-)SE_qWlWdT-nt9kE zQvW!|^U3rFPP1-n)t5WJtfWp&LgC+4`76duYyceb@2uX?n4be)y%sJ`WRbcoe0hlb1PIQNEJ-g)}mB%S)# zFFrqhPyEx^0vR`l@Y-YM{T`lLm|f|F$4AqH(r;Pgc1{lK=8UkZskc+RQ@PaO^pfZwTTSYb0~Xst+9a{AOS=>q2mwi4~W z$e0^j&qK83jkHWN9;i1&p~|Z>TSx~~B<(}J*cswpXWIXbWX~~_)0mH}xOto!y-`=+ z`}Esupjz^b*d6&!8h=I~QDwVKhVl6Lbis)9<7@r>;-+NL^*z#hhu@Al1$pW-C(-(;r#s!Ez-IWZJ2t zuZEiU$}*iof{qc(xtla!C;F$)GEGzhRV*>mbnXrDs(+xFXrwcaTm&zKT)qa8r$}wd z6#BDFS8p1!(2M9OGb@&jHJ0hUBz~1Hk)@M+oh|;idfe^K`km-Rt;+H0u_x72>=ng{ zMy}31Bnx|Y`co7#I{kfWZ(;Mo?uB~_`#PhnSE^E~h?4+Iq~A`TPr5A9*W*0di)G=U zlcBG0R^DJJqlSE`cdhk(W-t-8+(Tn6qcJLwbh~7SwL>Y_(~L`tiT#NU&O2?8*dPBp-XL*R;sZVUlCkUT;0-F3 zd&pYr?2O89aN&Z~IOpkB)zNWd;qk&`>OkRth4q{=-zNQqv-g_mj_|t*hsD_>`{x6g zvcGuBQ0qNfTr^}!O%=VHK)W7;gr0(Mm+0L5hB$1{s-t0@SvcV<2zw!%v6mkIO;yHX z5<3@d=S-|svGqE0j)|8^+@IK#D4J}WJTLiuqC=uo;>^UQiR%*4#O?7(vF}L07g%cZ zXuH{}l&WOzODC=ObIw^jSh%BbUE%4enyG4jZcTlWs)bTYkdVvdmt0Sy?8sh0`c@@B zX3J`9h&tLql&i!e`*|HsIaq)e9;d6ng4F&eTDFTG5AW?~1@-r>kuYRENNrc-c^$Eu z#%{)ylknvgvdzmTo=n7(MLo78eow4$_T*KG4C(TGyj%Qty|dazr|}>EFZY`Y<&-WO z+i>QCsY_FhQf)jRDg3%{x^wNWOZ}|lWOGvdgLFkzB`4G5A%pTSnV$EM9M_Q)H^W9n zp}VtDPF)mqm_I#aFAk)O*5odR8i(M^7-TgMa=J_W_K1}a`K}uy&1GI!q78r2CGW3T z)%Z>EZBFZ4ok%C@C(ln-NdA_XpBR;Bl{h)^k<&u!>Xvw3?8fNbx}9Gm$MZ$~oO)+g zrO!{loVwOn?@4uZhW1Z|-xZchjZGD#E>GW{9-3Z|{wF;wQ$jZD3R!x~jlZsu93}gX z^BL@9(}#FO=*sn`2*<0UFXP$Pf5BOIk?gm@#Al+Sl2-f&(&Y!SyAmHJ2IwT3B^e)&S90E6F>>f5-lJPl`C$F*#;Ka< zn=a$*?h(d*d8%>hBInIEOx>i{XEfc?NqpndtDGem&fM#wSHpCfV1@FpdqYU!yowNy z`+r~c2gV)ZJ0W8B21@@2_Po}k3@um0?sUZ&pNaPGke5(_e4It5^g!ch#;=VJCbjp+ zn8WrMYV`ew+z&B{jYjxt^}{_yxr%;$5q z(=xMJY%XQt$X7+=!|7mGxL-@!`VjqDU1miEnfcRYOJ1tBdR?qi{7z?me;m&^HTiR= zTV9=PmAojKNUn5}Vq@niAB=x#Zp(F#eJHvgru|yP?KODm4Ltg#vo5!#2B$iuYT?X= zsY{%^SvS>!7X8rqjlIdmwdv#O>Z)4isugQ3C+;JeJ%w^R>qAdH$c~4sbBMEruFC6i z=V3O$)8zZNq|TH0u|MhmG1+pmT#g3N@?&ySv(YXw-M8Y`Iq7*hTB(tEFtIgJFWEcU zD|w|g|2i=^aY^Eo#6SLfb9}86jQgt??V>CD&FsEQAeB0qFZAg>kb2$7E2q-VkWM9py>Aq`oV38hHo)Yfo?fq*HMNbp2(bRI+8VgA*Ndi60X$ zByJ!Vf1!`sI^+4`*z<7K-RzvZb8A)FeUfRD*_yu9si}ie`01$vJ$ero9wN_=6s8ML zrB&Zb9ZA(o-|Y0sk70l|G78sb%FAMV3B~Wt#za)>L#mIkz1ER^1+0~mf~G_%2U#hH zHeNug zzm!^+I@PX>POs1@_gwh=L)n{6R1%F=-LU~)Y$V3}Og;WYc6hx_`dYLc2gMf5qR8 zw~;S=Y3x1uL(i~2Uyy12xH`u{nL{LBStEZW)ruC)6&@}8x9|i_TF3=l)tWzr+IOa^ z>n=Pqy~hc**XyVBi~5b`#vZy^r?NFg)gPjvR>S50qVR5c-rOi-X`5Bw551IzOnX7# zAIbyX6D=XN2O* zFK~)yU9(*-GcxpU4b*-td!A_fC@AZD;|{rVeIb}nXqzoYeHhLYQIv?f)aq zSw7Z5&(gPJzsG7h#dE5YOAj~)xIGQ}X<~iix5Vbe0)0+8kt;jnA33AAQv9>n6|vQF z2$%7it%o!VWnvz%`$e6kdy}(MSEeQ!eP^1q4jFR}E^SV)c1;aQO-ubsKHij`-~`fB z*>s~I$A2<)`LM=&?8wCRh$Z{+jQ|QiZMA0lIoRU#pVU`SSc& zSN7z`^k>ER4e{sVi{RK~{v_GniSADSZJwy5oAqDurD(iKd>?$!#5u%+qyLgq$N2=B z$@_g;rs*6x$T{ozQ2NSr`E)#eEVVJUEcH(6jnw?qcd5Opc=}wnM(_0F~tf$$vNOt;@`xNTKV&xom~kgKcE8eBXx6k$1jW*#Fpxqer9Z;YT&)3?78Xy zpOF8%%F30_E`zHs&J=UT@3-mqXwJUrYiZD?=_a0UNe@fEo?e~)1Dzj87tNe!rq9X( z-km8YL-tPjvs2{t|HmhKqpS$0O~C{)TEDYgsTWA}#n%2P*>Wxq$Z-8ezb0F9da$&1 zKI)53ncqq_o*J(e@931`$vTD4i_d}v?qh#8jaPJb`#Kdo{biSL!zt}$2nGwLMC5Jq zxIhKLO==_mk%9a<&%@1`TA7nFf3e?orGH9)ZOp%=xj3gnk>9H) zoNp$Rb+#K|HrH9%%ZxO1vFWS2W{~GT-hW(<`Lp`ie$49lP`35*>~^_9hh?;fb9@`~ zxeUO$uZe3c73bQHgX8>wwdHQyYZs4?lZ6@6P}1P+73n>i?>GB?}!?W5KkA%|gZUXck6@Rc(ii z!&!m#p!Y{`@>?Du7xW*UVJg^(>r~8*K~K}jqAB`CyrP0~nm!3Lc)xn+33IWG@srd~ z{3K6w66>rHJlcY^47ox(AilSGOoy4SyWAx-N|yR>f}mzPUMogED9-< zfwjf>H?cd`>-ezGxPLS9xKG98wHnqwN;86(=-oIfJ; z{`(T^}l>Z;|DCnH|>0X9)lTdkR{~NzdXtBlC9x< z&9Css+bUY7vi-x!Fwcp0E%E-td2f9W-v8jQEUKGzIR9fSFUjX*#614rr^F>k+4q)s zS(L5fpD<)!Im1JZZNJ*FZ8E99GMW!Phsx#cUWGGN2I9eMWoq7H#--`Gf5fz!vL0*j zfPShI%v`>R`L2BkC#^;!MfLKU!7h8rn6I(dqe=e1vX!m$!`8T=nBhn22aaUl6VDSB zMNNh1{9(4xemn7-PxW($Vpg=QxIl5)pS`SYFC!g}E>_BbJ~vtrDa8l+yfxkcAta+a zaBMGQoyc3Zg>Uvjyj;W1{N!}AI?(MwI<9~f*`BX|y|_lmbJOg*q zNJ7YiD#=gK8A`ttCk-cyf0enti0v`R^DNy<%JGjrE$TS|9feL9-{ZR$=pbb6^!3f_ zQCA&4%PeFRv+g0|xf(yf4*rmwxX%^5M_-xCZoah>^hj~BxdU{{0FMXh_2JgH4?1Zg z1F}8ZITu|Qg%jWP?nZR*u)N0lD7!B%KU37=1*<=nx2u*B)^Mk4sIeo;d_m^!RQo>> zJ&fe9abBEVnCtmHx!jNVdjNcLqwzce1BDE+zxhDo@`+m*`w85#SLFD7JaUN)iEl{6 z-}QcKYxKvgeQ9Gqn~n_;oejnj>M!>2q|D^YT;O{n#YWnaH4V*b2HJj#EWL*X9X|1a z8Qw#VEVA}*iOD`KUh;<3XhRNOMfQf8)#_G#rm?w&)CSpG$SR&6V<|3U9((MEmzRi3sBJg#B4 z?m%0Q!x`bMn~wa1U9C?Q_vi=rh0}k|^vTlrrjxzPk;WmbBSo|SBEHub)>?p`7QN~#06?EQoTvrL*hANcM0XRpRb+q2y$jsY$ zQ#;s!SIL{nB+wB1a*w%<_s&yzW`?+6sN=gARo%*V{Dwc|eY+Jnq8~~;fLpG%BO(8A zD$Wn9QV&nBVQpUQQCuYVIkJBZIuE^sI?KLoi3cX}(A}@oX?uUyIN;mRdtXZD*-;vi_km^(neO)RZj}6*^fh*0sE` z-PA1&g?*Ysq2V;6yYk(lu7M09saTWRc zCaJdxZ`E*@PN?96NL&p}1HS6Vb*)^a1KeJH)~TQ}r!={wRxSfLHo(ss*K&)hE- z)`xYsH`5*R9nTu6i-J4L?DeS@)qnUfuJ6@oh!ph8qY*U)6 z2W-7Y=l!M8yHrvih@H!Fe4o#1cH&@SvADw<*7enRiA06O?D&ybQ+-w5j>UP!e{?@hPJl*t@TKajpKU74Ta*UZ{%4L<9O#O)(?NDNlL-QK$P3}U@M5bu_F zGx2@m#zg7F>xo42n&hg)hw+B->O4HN;uYfEV-H5ZfLcQ4Y6m}qp~{O@ukOzdC!2bz zQ$Gc&E6-mTGRgt`I)ycSw>s~1`r*`*g~JPP zPt8n^6;*yey^Bw%ZF*$p?raGXHk|h{k!~nT8a0aTi9HmLCC(BjyE5?rzx;tjqvU(Z zRmqOYmlM;)wEvEk<+&6ojwPa5brC)2ouTS+C-W#ZVEv6D181`Lijn$tVV7|3;z0T# z9(_x^|0sXV{m|+wR(=KA9hL2nX^`&6AKonWU+Pf$_Ds3VkD2lERt?1PPj0W z;*)5b-k*Ln`+F`_+&1J7Se+!q8-M-m{&FV<@3v;?{VF z*i@ZsALGM0Em~4$P0j3EtnF_z1NZ_S$gZ`U^E2;cyTD@~(XppMRQHMG9ua97L2rzb z7qMErd^^TBJ`()hXOraBJb*R7#ZU=hV*yyH7k@FgK+podmumGd%qw z!}P1%y}Eoo!KRxlVtilhQ2eVz@#Kue{}LM$ZzgleW65sG!=fOE618|(XC>B(U%$ii zFvqI(N2fREI_tgMHq(elqoPXkF`1LHry0eyJnT=v`&Y4KuGLrVLZ17b(G8?aefE8k z*tOB8WL%sfu33gZ`J7DS^sj|)6kJnqMZxK!ntjt}XJ(}u7nUvfykK?eaOO)n5AAa0 zoD6qawqK-|PD?%G7stNh3+NsHF!5aSv*d%xn#up6fU}E~E^>GBK%%M$(*=pKiP?!< zyi2TA)h%MH@24+M-=BKEaB0Ds6QfUbKQZ&fZw0ATKegy* zq`MS0DjbxWmi;8sJhmbFkVt#?+)2^)@g*psAih7g!g*IeB_<}nOb!-(ted<$IX~Go zxjoS=F^^pMOG18uK41T!!0tI`tMP$8Oec=Wl+4_oK9bs>+Ls!h-k1I;b1e__V!3Lo zczy=yY|xFTaFV#rd-08;Bjah4b^0`%89ki)N_6C(^mXZF;y67EKQ5Sk;{FpYPqaGm zVnKt{k<2gIHJSTU{}fbDt_8pphWbBcwaG$%83&^V4uVnCWa>aBpW4X zCiaN1e~|o^xBZ?(_4re=cP_NfW8*br{UYDV$~i3?g-2>+K1x?ew@HuWb9_zo^g^fQ zm1gTd1P^`6c5J|_vnbY+#riN!od3*-9| zMT$I`9FA`9NxYG)UZi@F-HA=2jV*ZhD)FA)Z`73|{jeQjAujrP`eh#7 ze&V}x(@$nUcH-i%@Z-_Q<$AKd9_tz(>d_J&+A8+5UFCdF8Gv`Ns>h1{J)Rz)dbO}) zVX?x83latOPEqP%To^O&fm+6z*QaCd$zc)5N{z-gg^iv+gEz$Y$%ahHLHzu0I zC&m`V2PEc;?=DL;5xsjL`CoE#a)apf54!R-MIR05>AysIPtN^=7T2Vg$KjUQ&79$=@b1=%q%!HeOmv~=y`@v4Ph_{Pv(~quKb|dVxH{DpA z@vl@nE8SC;&K;?1QYF(@WoBewm#N(ujohq{--CF3s57qq-y^cP za+%-Lcc)esHb~u-u0rcBPR%cTxo{;t(41f3IPdtv+_RDS(M7TPPLOOUezRLPP!uZZ z6jKKu=_Gcxfv2=EQ$BM;`Xy12P3U`CLFa-?3RV{^N_ETJ4oi&7Y)VZ}-Jf|XS0gr0 zM52y7io!@EYdAYTD1KJ_O8v6uC8{MaO#UsuWqP80^5h~TinJ9hYXNwN#B>yjI&zuYW2F&AZ4rvDYyKEH5X!O?kO~tG`|QINFCzTsFEP_96{%roM%Pi}C#k(Y6hjdo_d5860oF0;?svp``6VsFJK$Z+{awC<zCYUvx?{RaJD4rEf9itNs} zovd>UD}9r@oee#oD(dkQ{5+qhr*x9M+=22sYU!u=7`YXZf12jI zJ|+wAN63Gam2Z*vkj~cYX=JQ0_I$j6wpo+7BDp$wW|0ev98S(k-j%#3IU@O-Y=)xA zrxV{fBjTdiFy8-9`CLzz?Oh}{AiJ5SZ(_vf$`37>DGp0rX8o?V6O~xV)2;jk`tH;c zmFvSgeuZ58@#V9nPp-F zB}vZ*p@#~*_NR&So-Au5%U1a--qW~iCLe=NeoJmoewAF2T$lVMIVX7wgwmTG*oVz} zBRlX$821YOyX)sZ*XM4tZbFTmhI|B~+LQi@z8~z=tyulzt8bI%(<}nz5GH`ar`Ix6OAI<$64e5c#j(LDYX@K86{Wy9R9W+tn~od!yRIC zW5uoHYO-*0;_t+z$pOhx$)}S2lQ$*X!LW6cSy_P96R*XK$6sbguGEhwK?B4i59W^A z>s##ZM)?R2h%xtP;n&R^7Y&`0em}jEq&z)SPYkzOriQbT`szV7MsD$w(9LV?p6}U) zn{-A0)2Ur&pqQ?Do!%ote7n`YSGS=zd6m}VkE;CY4~i3g$+J6wm#qogXa~R0m$B+H zqPE04BtA&|nMlegDk*oNHR|ZZV%SR;HA(y?l72?~JqT+HK0YhDkazVnnL)>N-+PP{ zE+-RuzO3O(Gi5W!WaIpn{@u@B`A`+iqKOe%+W?>UkQ@ zHtEFf>%*=Nos7HD1uOOPxr`j#60HmStc%r%_jV%n^eI)_Oc&=XfKZ%*qc@>D8>!ob|3}?AP-^%oZOxnKT&2 zM^lUkb`TVCG}=HP^3P&BV*m0UoF8v)b=&dNbx?~?hkx$3*h2C1AyCFCvCrk2)YT1X zRo)5?bu-0S-_O~}o2|{|By$G3Si{4xMAxfDqS4{Ri_m9!gq0jYM%+Sf^s}zv4C*`S z^WJ_Bx7tttA4_)vXLI%c4}9*Ov1QMaEm3f1dmMKkj43GI#FhocDRJulI7ibFq{A)+@}F{(`6V^w?AOw3K( zuHWKy72ky-m*g4WkrTKVrXP_T+6&EJ%nF7KVn3Rug`Cx$Q0~$!dbl^Z5|q1gVvg4$ zbs+(34igfWqwRFHMN{o>yWI3q^-)Jzm~bcTZ|)Gt0`aTqv)Wnt5V?$>Wc}(uvfqQg z?i97gRo1t+`n6^epA*5x&D)Gd(|qNWInN6(SSG996&lk>-CYVid>^l4vJ6C+hj6!C zOz6c8UE^VnWavxXEDsc>OwRE&jU>!utHWJbJw>#YO%Vx`T(kSirE?_78C`~drL!%C zSo31O7jdslF8S&baJPW#w-(iPg^#b5>&OR#Z7xSL5VE&HUNprFwHmVG?a_3Yy!j+` zFys6=*uUFBnH!*VRTU0#wHkZHG|T0jKZG*9Z2gbR)`ZCi^}Oy<`BDU$mRH^-OkoLK z%%|wzjh?y%-u;8j&sR{|9w_;!Okz{~Ra17Qj&muA({i}iC#PDg^8Q``+MXoCUJSq7 z=e;lZtxnLmb>hsNsMWyJyUUt9>2x4?Ktx;qJv^_`Z#wJgQfyk{r_AJkSb!g97mXB#3I5C=#a3EOZiG zt-c07vkW@t#7R*ZluQ{Jrf}x4i=r84VEoywzL43RjmYUc@=34a$8e9#&uXgk$WxV< z2k(yhi&)dOssk&kM!1*u_(Oi^tk-$*z;ZEfZ{N4enzpjC?p9h^p02kXSydUx7QC0< zq;|2Dhsj9manvvvdvl-qjL+$72P5S2E;)&auB|LdQ)Q{2!aE_m`vUB88gzextnb^T z;~LBJ1AOwLo}F{*8di~l8u0EeerEKd#6%G(xV#}5X##UT z1#LU#uj|`u=dAU^$hdu|cW0(GJ#PK& ze`8$HTGsG_uEP!_w~jL@p7_*K;+0!rDs}DZs;=gWG-&TcKKM!^tGKUtI>?-y03z zr8O#8-=k>ww(R13l@$|lL`S>m?Bsj;9(MSWcV)4w8j+)Lq?mC;#^h z%{0Vs-|+h>dTkeynfW|{BN_GNxI)!e0a@G`R(=eZRFbh;LxX&U4@SzlboYIPpRFgQ zgY2rS$aN5^zfZFqmU{|QuEHFaFs~-3+};WrvNBvc#9h_YttXS@s|gAH8FU$?hCNwZamd@w;_akY8#*w}+`@;oh;R&(CVN z740z0rr2Xox8tiYLAA8A|B96#E~;HGhd4~l#zg$zTi$;P`tLyd(KPxdc5R^@U-7pi zPV}ggIKwlz84d69>^rTriAsbpFTEX}8Ehy0t@SOm@5b*LCNuex?EY(Hxgo96OSZoZ zo@nG>&ye5S{kyqO%BrI}m-8z|Vk#%r7;ZW);W_2KKTK*GFJCqX^^)v&8kub<>S>WU z{g>1g%|q?bud~AHC!z3oytvU%!;Qbo<=mp4e+T+xL7z%=+BkCA-6~t*|Dkm9uROHL zDBTOE57mLSp8n}>Wo@0pd$di+?nm%d7ZnOkt^Ibkr#fBpHZH4yH}+d?*h>>nIE9x^ z%J+mjhA-jHJvch_rY=#3@dS(6f}Riezdp}zNl~@?veho3!B^PF`#9)5za3_LT=Y2` zNNpP{>VhkZdG4z?ImqQuD_x8`Rwpdc78bKL-Lo2^bdq*|fyY&Y#BQK9@26i%+vOwn z(88$!T1B@NtS+~#THu^;<8V4lv;+q%MzPCm;Ws$rDYj>_6)f}jS8>A(ygkr+rqMN@ zkd>iy|2Q&{uE(V`iQY?7CGj`zcCtfo!=vaoKxR42Slno>)7ZmVYPlA&pr=fbTv{VlNb3g9b#;aSMs07(V>XvxY57xRRF)3^rd76Pc zrm%#MvzUYZ{cAq#n|S+iT4hP1?r|i1f4dly}VIv;H zp(WiPl+76zqJx94{vA(yn>vI1{wJ=E;y=jX<#ttR?t-?gUt(V1|-8T$4nME6Gu(%B*#*^h8j!&nxzHP7-^KH5(8v}466J!Ie~^Y+92mv?zpc8gpiS(AzIo^sHh z-tKN*CG*(8H0e>0hFcRzNfoOcYKOOr7r!R)naJrk*4R+S@>`YqWvuO5w6^SWL*24% zy-YyTeLG4CZ=UiRk8*~9@jt-5SQvWuX&(ND~T1A)N!&`ZUMJ<^zQsn5Fr=?VZdbX9WxELn z1x*>&*+!1Bg@|fVbeVYS5=`|de0`Pd(SYPP)rZ%YQMmyH9wDABl$h?GKQfG0Q;b%7 zj4c`kJ^w3Ski}ohQjRCP&*G;iNI=hoj;{~x&BFtmAp^Aleo#HVaQePjQhL7hypZX! zdL#eT{d2!O<+D(cDNet+6Zl4SwF9a&nr3`W<;82bt(Us*Dba~~2adX*=D+0IQwpXG zmwCBg7r_Fx2Dc@TQeB-Ct?0&y!+MyelIn0z?5!#qm($z#l8a}pa0{7PNFy$$?T*r( zN7&FHIS=q7%d3h=jyF#~ADac=z7fl+hT~iLr>e1r@;TRI&%zPrn#uh%o+zU-c|N_f zO3l)D(C&EBTs1_Mqc^K?X%_u3db_h;p(nbjtWK^zjsbtI1kC$7d)*;t%pw{^c?K4iz1)a^q8dz8CchWQpa} zok$g#|B|@wG_)b)I2ETJ2YBKdM7=o(k$#Dy! z;()>#3sfU)jqS6_GHHeN2;QM9{f}6#SdY|Zde?TwvcTsqy1T5M6*que+`)HErS(1} zxz9QMd!wCk=zq~h$rTNCsl9Tw5#AcU|%JcGn-SjSPtV%a-+4JyR^HgD#Tw=>^kk#Wtqhlr}we zxo)YPvC4W#>ZdhNZJAmn?R=~xX+H(KJ04q=zSrH9$H-9!-tW4k4w2`f;yoeE15Ks4 zGkJ)dYgenSr~~B54XUT zVK!p9*%56aG{vIp*@TX4!aBdVRvp(z@w4e?V}GQ*mbNT)Vd`V4jZ*WZT}W#lyEXP@ zYLnD?si)Hpr8kEcJ`%s4z{K5pB+rv0IzF<84gWQ%XSBV(=9$TZ+?m@X`g(H3l+G#N z=?N~bbMI5Pp|(zbF*-2vG`}+oPh%xMucFGNJ2dsf_#@(^Exd~LW=Lg$J?@CCR!wpv z@+1jeLF>H@gIvVx`lXrcOz% zm6qMvkAxESriJyEhPtYxCHiXysS(Vtj&QLl9}SXIpli+0^iDNpuZAx8o@*8dSCKsHHpVO zr%R+{WC@K?D`R|on+~oAW1Z6$q~4)kXZ(#tH(n<>pNkDhzZSnk z?*2{KeNT6i*O$#1ffA>oYF)|ycd8$b%8lNt>-Q(z`iLCtv!+s%w~M1JeoeC=&O7J7 zp);-dUkB*=*-+}o*rZsz1l{^((zEUY9R^{!$*qmcqb+3Ocj?qAMH4>~y{z)*kh6Ix z<8>(K$MQ^VVtG|Jyn18ujlDNMN-drCV_If>`(EnHsg2W$rnh3FcEbjumwyE+er=kZG`WsG5qR>QEsV-+A7KQ)t%j*(kl5~`s!%3h;IGgooKPhUjA2SXFf|bJX0pFuPUGt z{K?;}c`1FkF)1LR*Wie|VTdE-z>g=NO^zlPiFQ=k7KMGbi(KN#G?4en6Mw|8y~C*o zZd6XaGj(!m^|Z?LZBt!{6ZEb=FZWYKg-^K4cM5E(p9pS#Qajk*-!Q!fvXi}{Z{UJ) z$z_uZs*p@kBUdTqS^a~bsGR###lm6LH*ph~zK8$z6{%i@97W?};O}>!^jKKdWnS9g zr1I{RdtFb{yV1J(n)XIR6=SpH7Vh#5XjYEMB5{A)jN0+L)2GB*#q!5irA5xbaZB&-r$Tb)>b_3)Phe&{ri%A(o(<>bP5V zWNo(7A9-ePx*O)4%)@hR#9KNgM#4B+s+C);A3IMhMfKrJX}6}WV#7wn>c$$$zc);4 z6KkbzwvbNT_v9?q6o_kD@c3(rHg=i)m7)eQHCl;(HCdnFZ*oKKe^e& zfU409*wH=gXLqs4ING6>inlv?eBrj9fIE+3Aq$h>j&eDrom~UE|%){_$PuYhy)X%hQgfmFL;)G#lZ!v;_C_xty z%56!#^tZgre;+Fgnni^1t$vJu;TYBIzZT1|Rd#a)s{Wy8?2KH{ViiWkB5Qe*zw?1g ztHl_`yLmDFn2JN14KLiKGGk@q{48@sVb7vEE`waz#$SKn|;Cd6b|23yGSRUO7FN8o|bq+vDF8YvqjS-VkMW6y3kiS6B!2I;0vsKWiLrL3|7T zF&#&jREM7;rq~2i>nJKI#1sBYgjzFWqd2Sv8n2N-eTLUPPmIt=-*~H7Q>*<_7w6L0 zH~invRF0g9HIS>%B8$H?o{aPU)k}29%#@R~Tm;XKafjdjOV0>xs2(u=B31nDXPGCyH#nL={3)#7cv3l8*7{%TN68te@m!QJ*^M5jx1jVndDDo z3OcdQ&!O;>`WEYp6JsK=cW_1il)CQbe30Z7OZiK-x`TY!8TYF`7`-HJYbMhe?iCKV z4wQ%8gjuj*KJeZCRSkmC5T(PkwlHnBqP16IZ%&H#dy3b}i@J~TJDQ1a&s*&(dB=Gw zsUKCL{4ZbfFZuVAv7@n{6Z!0_W>58_FLKHDeu^hX!X;mqmA+SBVqv=T0^Qa>d1mr= zI;3l%a7&f8`{{|^D4a8S3te(Y^ba=RO?PqZ7e$17=AQPwgZxWgh|6DOBD+=Qv(^mg z?r-?y2PoSZ9(*^v{xCdXzYIBhBR)%2|FI5rL)XF+BDG@j?f>eW&QRTwmk)oF`s)|` z{8)M^IqG$MpDsSJ5j?0An|Xr&H#3?MZKErCgSiXmX^VWSRL_~o@iseMP+oK;KCck{ zU1e_@b&ywKyi0gGond6*W{0Y%S{6Ea0gs&Y^^2H)IzMR;Uau#wk}C7|El=rxsuJ?@ z!*|N)PEU014i@V)B7ON#xlno;vF}5w8=A<*PnPd3tvco-dB(cr^@eF^)AdHbrM~z9 zxx-!2Tr#1(lIJGx5%(UI&mEIIP{+d4EZ{LVAavzMbf$G&OplfF6{T5l}?qn?;Ua$?mvB89nvFe?l6G65RLsmo43ZnI9 zZmD`+Ez3Obx{@CJl*MdQH9HTQXhl*nSBUT%-is1&GUwFj}XuDoAfyE(4 z$K7_dL{|AlmbS1-H5b$-tV*Ac!o#es8)`l;tKJwdKjs#&J~~7`$1mXy&8O}2RJ;tN zW(mZzwJ0ki=`s59O5{NqcwUCwLxS$CLA zG+2)LXL!g}S}tz3#Zf-ydj8K?5#A%wBGHr3ozZ3kv~%BV9acCiO}kz+_&&ZEDgy4w zo|Q%A^YHiWs4^AO{XB%BI6Q2R4xCY-@qVR+u^X zPkLV7%9A3E`A+8oU-M-;@pML*5W7g!nO~QCf7$)Bkz%-`mALJ7H;#8y-`U>ZpJ$sZ z$~d1;nKMWBwm-h_MD`l%zA7pUdJV>SRJZVQG3*E{4>!LwrsZ<7#hdWPNQg-tyqw0{ z+b$nI3wONgw!Qk0vFfZ|Mcpp9n=@BdWm%{ms=-g|h=1OK_AP~qUDhko8rrsqp2$kR z+sW#$g=!Xd&Ye;3ODjJdIVH}yCMLT8UHnDO;9m8DYup<@kzRX6$4-4!0ENX?x%mnC z@y0pbt>40K$D?y!*5XMzxGWugh1S>)Yn_4CuhDKzfFtN!a+WiPt?im<)Kyh=kC}|L# zYmkYPXtYW;VUAb05wRc6YzvdB%AypMKhCb&;JU811M#D7y*ntcxe_9{Rc+8=xNg{G zQ3!uwUPT>w(SYSIqmeEoav5dh3fl1|2ham+VEE^~VmgNJ#08D$h?-VgHnG~0u!#b= z;w+oFoD?sXO&JC=ZHq7J!J!)POz&i~%Xw9W^%ub5S7qDELE27;x3WWt=0Vn0z@tOQ zV91tq#r2h;Z|7AOp5_sziSLTS5vtnTW3uG=yr&|ZxVnzN#}i0&1D~)S;(Isbu7T`S zDcRS1A-^5vmG{Di&iI}xiaX+a8Xxt9Nb3^n&VX=irk^L_jWOc+0ep%fX#6CP=t&fK zm~?hR`(|<&wbU~gVBfQ{=au+V>ts69#fE>%$nAsoH_#l`qk+5N}h?O}J#Rg*kv2XE3%4@0Xv%M2c2YyOt`9Y&VI6yVOhvlr-@1yrf0c#` z*2O{L{_AjyPY$_-eexm)Xq;xw|0^Cz8!LR9Evl(nuM1w98=q*!+u{$=o?D$jQ(t-H zigMY(BYeqWtU)(+tf6U>TkPUdr=6R}^EBIWzdbZj*)juXG=m;jh&%^tn}VCB+RgLq zRQ^Z{ygq?s_QLD?S(~mR_)Iux42?J(m#mjpY+$!XO*(#D7IlS8Mwsbz#^h<;*)oP7 z!W2I@r=SJ^DRy5N_jmXFL z`cKlZj9VhLea3R9^03;E#DQt+$J8Nvqk*JLHU7ilFai=jXyIuF<*^NqQtw&WzQG9B-KUZ zFL3t(m9M{xctQ;q1j*m-5TpGpKUG@x^O7}}ppBN}k_Y4)Dx35@3ax4mz{3*Cn|Xy^ZRlTG?x1yAHRN zJ-{=(Q{?b3O?Vk5zE<_gMp@ZiJn^TkbO;Z+z11|7uWwG<_3`~C9Q%Y7H}tnwyvgd; z`3;@o`TC!&~6eAcSgyq|is1Zkt20quQykEhsn3ki^HDrzG3ch{g}6MK}F|Um4@YMymqwOR`xvH z{oEYwCiB`#@;e(r1io^IcV&KFzvvu(YnUnYhB)g((e9g~mzI3%RG!oa;+Jqso^w)#E#$SD1t-b`HklXX5J zy4{pMRaN!Fyp6n2o`3Y)zd{FO=x}OC+85)#yHMsco@XZcuDQ`;=1wlvJ2Nl3Q=iw} zBCM8jIv-o}Km5L{Zo)o_+AGM*4YK(eKd2`DU&g;5%|Gj@dn2=|`RnPq{XI~rofz;j zKJ+#-uG5|IT=idb>|>++_8?WMi}^=`SjCGpZ-#hv6)$Nid@?zyxs1e*DmcSE6TN7g zFvEEPi}y!TN0qXZo$L|5Xw~R2t6nFjohqYzC7KnIe@w+`F&O4>s9U-o#O#rF<|t3_ zgtoATyXlyrZcqL%J-=K=yZB|Z^*-Ru-=~uM18aT&-5SVgFJ;Zz@)z=nUgp6me&nAX z5)VvMVYeiK<_zZ%t=CuZg1v;B!#nFz+9iu#%QVt>QlrRnNJ*2VJZjg^KolP{i8Mn` z`-bR^XszV8D8gCG(k{sOQP09=XM5OWt_G^?D>>l> zBIcXYzl&v&-T246>rpC<_gLxP#9DXamS1V2)x4uUB<2&^;6C2a&wT50^w4-7=2el+ zEV%u>stay$x5G@9>wn~Lq1y_osSoJKD>xAu2CscMdMYB9oBX1#;qmef@5+cTm$&~` zywzS@I~JaJN`>PuyzY(}L&QHFNJ{p27Z}y^v4XL45Z9TpE;91@(i_L_i5-dUkvCj~ zhEdV??|iNcBzZ6iX&yO8+jJn!2l<3|n5#0EUlG@#Q4B_TCcYAay^6i4sN=G|*gGEI znlS=LT#FxJ=l)A-EY{lsC%h+eU$j$lGCXb{^e8>KL&`HcaL+=@)8!6!o9UGXg?%J? z&~%G!`h#8-2h~*{FkGKVm)JjPz0;mEy)c_zhR0%+Vio1Rv&kax%Bn)O7`1< z#gE75z&AQYip%8}P0pG;0HXD0^r@8Pna*XZoav-YU%8b3Qp#oOoAP0Df#ea+p<-k= zP5XpBPR+;>FDJA4x!DEJ>6cuXcBjnunY3JKn^L=_b&cgqOnOU!QFND2`9keZxZksj z>XmDe>vAY3A?>%r&Wgg%56e?ccK^Wjcnwyh4{Nr^e1?l@x4@Ve^FViI^n>u$(lhp2 zddZ9&q;4CZ>#vL^k$Chjvko%gF=OFJi&J`Lx-E0H%+oRrg*H!4u5Hrp^pqyalOlW_ zUP-u9zED#2jO6&o(Cy#S=EpvSUA+;js@MKR>ThXRV_%E=2be|ol9`2LzwlIgBp%<23yyU5t&;!}3k6ZQ8Wd{AvATgXrK9dSGg!cU5-a2{!K` z9?+var1FtEB;||f?8s8u{FTH!iy!rGkKk$CMH~MoLL4kc|4-dYQ!{Tr(tkQU?Md?} z%`J`9=lhq-7(r5p#Jgs6O&T6KNa_y4yly9lr&D@l{v~tW%-J*5hL0Z7>pDjl>{BUy z<*@FEo@H(7MY<%_%y>Zld3X8?@#o|w@}-a0jn+9fQOE1W*h6N|&xRMh2Q}WMZn%$l z{6DDi3OxRmuSSt|bk-J;eOX+0m4y$pzj~oavy8qFoc-ity;Cfqr?Fe?C39HYn2K0R z$6n7^oAlPCCERn`LtSANzf~qOTkr2))fIir?;NiK=AKNOQeLK63Yo`vBY8K?(#xd! zucH5|8Ct*tN|R+cm5BdC7e(#(I(;LLLWxeA91>yI9%FGzSxZyVQazgPyxNY{5Qj}- z-?i*|66;npa)Xr{nV|M7^#3g0&jj`;On0e7$7hOnN)Oc|-O{tgAJ)0CSzk&{J8I1u z+zl1^)%`+e?E5%BZ3tPfrvK;wjOH4pM?aEPyYiZ*%GaScko;KOYtJ4 z|0Ml70RHiX9)|BDFPH(+M$JP3`PjP2Kgz_7HNE1=;C3M5iKe0O!=2i&5}{XWKgv{#pR?ASysYDX#nS0ry9SOIPz&$V53g`2n)JP zR&^#X?opW2GqT}3AbicO_8)oHM`eY-!uMb3(HLU}byo=6PrRDt`0s62`Ch0&W7tL$ z^xQ1A+s!j?pg!U}oFgD#&*QZ)eL37xwTkcDTg1`SPl{UcWgf^GsM=iq)HwA7KRJct ze8p<|fGV&cpRj0i=!d1OS_3h89~voCao*2!c#k(2ZWY_bFP!C6ZxLtDOmS^kdlnOnipl2xr-EjO$rR!N_uP}V_kzqax*7kJa9QNE-V zmg6&J!QaoSwY*m!z&h)kY&`)(`^Fii!^yVsT>jyo27MBG1a8D{hAKSZUuCR#C~9nm z1C`T>lEv5G-v0^Db^<@=Tj+LGXBe_Po9!eYd}&?M?d;HCGqb132`*7PmyK1;6D_Fo zr%|+)Gr2dK$8Od^-R?Hy=pOYx79QRr!W@)d!?pA)*3%DpZzyDs5=}?^aX}qm`Q-2kcxna!zL-~Hrc!%1BNqQ>R zrB`tx;eM}lzC$sbnM1S}Zkfz4`g)VsTbvE-p~m7hnlgVR8u=Ycn?r?3chj8S5<~oM z&hrk4+Y0%E+M=(Qcq%iYR!jM3d*BZ}_~u(+8a+L`qlh+wHr=2KW#pk!Rg^CGf1wsO z+z`AFUhunW{B@+EwD>C%i@S?f>&D9*r-SJ%OMQcPzZ~yBC2#Vk$Y-zE=N_?J$d}gl z%ITh>q4+X%QhX^VP!P`d0fc7(oM(h+_I14Vs%Yq(NcSDl&n}fdi|wwV-k%#H^i@vo zgs8G^q>o*fjC_I)zgp!s^Px&Z*TQW=Ax{##vSng~VyYGk;p1MO`VOz=138|dB=Qa& z4w>1m;{MhM%_oaKx7yiC`Ko#L`@Fb0%#Cg1CksRx`<-Q|X&=g3*UGpJzh1?YN${+G zM@&dDH0~%xEd^(kC+2IozerVhB8NjkP`Pb3cSeJ`Q~x zY8AiW>(69M3exD4Wcy3vwD(YQE_s~k&+niOp?k1~_vTD=P)yZ7UXLW#BUS&ZahVQT z=nFlq$NM^A-z)9*Hz>mEEc^zRz9&TOn$KA-;`-7#Cgb%f_OX{w|E)EI2~a!Oq91t% zjYTDOta+IjwHA5qj{k?yAGJx|w|?^}JUCj!`wrA%7c8?5&3Qni@vZOq=#XA4z$6s? z()Tdyv7;S_9+@osgBEx(ubaZ!+6H_r&-DEYRl)M%_0-rABRNjfKgpxHNwp&D*}S(^Y6ts!*SW_#Lk!Co*{66Dn9oR z-uVn=_CY>wr71$C`d5(2?{UC3Ts%CX|H7oe9dMpbu(vRFGB;az~XAy&ty3H?=0*0I#J5OaznP`45T#1+i9#*CroAQhRT1C=~oh3 zp((5=g1>swqF30}7ASQ}P1`Lh#rBfdS6GkyY~dAN?oBvsJPx^{cP8{K?3NWfWOdoe@YD&=%mdav#%hYewI|u_I8u1TUSGDGSvaaYGaaAihWz#;Z}oBQ zLiE0yJuBcv04)5l^^URL^(v)GiJPCIUpuqD#mRWMk37{)9-(eI++CbC%iy1)*)EcqEb0(C*lT~^!wt6i=$y5v^8qUCGcPpsTK*^du-~(uV;QoE3~Q<0 z*y#U4=U>*uohL)=r#g(T1&Qc}pLY4#tq_9H0}&VJ&-e3kytF*%@vIa5!(Q5>eGm5b z5!r_$o3P!*+~_)kFq4bDSjLUEB;!Zt-mZsT{|l2 z#8deS;oj)Xw0|k@%#LS2#_w(UbFJjOU>vNbdETXnDB3H2pk zIp5aSUdCyb@aGuPlfue{+*}LXw}DRhfUcTI8=OM*`>dyl6;`pnqo@*U-gl6m;?~uU zrtWDU!RssFJi^W5Vb1M4kdgze`Ekhh7$@C={VoXIKCL(8PM*p;bWC>?DCwkj(n>9$ zKy5v9J)SHp?;pBvUvb{~X}TF!6ME8mIIG3hdej*nVkZ~Ms(pn2KO@WI(P5QSJH)E| z;2cBd>}48#nAZ?q+$2&qoz-1#PZJXy8t%|44e4%aXN~EsU_JApZMbpy9=zLxC)ik& z_b?vqEkpCMGrj~fOOp#ZMt{$gMY&>KikzfTotBM z-{X5{Inh48hIqy%W^9T7g#0gyKKYO5q;ja0A-Cq^2({d_Hex0Z(iK zjs7+dhA=nubrH;9r||`wGsmADy}CQs_gLcL^k7%||7EtT zIe+M0+}xAS3-|X`cM=`wuSpP!{y66uCsNY$DzlA4c$VvU1Dn~OsZMBUVx0}VKXhen zhixap!IIUP9=4}DwA?I16K-t3gz`)1 z-`8odR@T>-&6`CRh8gY?XvF*I#H+B?9WnyT#m7g?Gr7(ay1`%0MXU6sW%kn_x6%%y z$nz$&AB7JZkdZcI=R+&_IZ=I4K_qp1?k6*E>cS0>c|KCT8MPy9F>;xZ(nk61X&y3TO%DqPRZ^vs**sj)SRf}~hOiE6X;9=*%WGx&xBcW42UH(p}|6 zD_;I(xuTG#eiYYr=hw_&_kS0k6jq~?;r&ZV$xQqBQNBG)(QmG{s;79MEswgS9Pnwj zdOc}cBr*td@fLf3m<8E{t~o&ujUvrY^Fl)<&rzLq%S6V54u6%AX!O7GL9ltv2rIq}f9Qpnyqim?XDKy+81vy!Gd zN%E%B-e=6P4PD+dtuD>FlOw<2sgJC2D+Db`Wm*xduM4N>;WO%~#Qzt)(!>do$Z1@+ zK`e3534X=u3nCohM1HbJlUCnXKVP|oa40P z>v(ty&!7d}5Pa~@P~->E?+?yuj6H2iNKTRp@0($S6?|omoD)0j636}QeFwzJoA{@5 zd4Y30D^#`BNz|iMAW7NmEp$xJr;V$#_rHsSKN1f=PDXB1*A_J~W~)7oRWsO072f@- z@G7X=yb`ZxgGI}4%fWx_6GreSp2D$pXy%&MTg%^` zv9}rCJ&z_UnjCsp~=Guc@#d8@`cM)$~+pGta5SI-U|Gj++uWS{o{JJS|je}ah(Q*FMO zWQ1L17fB4q>o?0-R_9q%!9)9<)^FCefpkoDS}oAJIGaBRhXqfrvDK9ogXk&4U3noH zVTMBYM3lGyPiB#`xm9giGCcdb{KCy{;%X%tqZ*`8J#(&LN3K_ct;&mH2IE&sl%ULbwZ!S*nTTOy3SlRTpUl z>ys=dkFlWD_(Fr_>Oz+Jdlu|2dFBUo0@w1rzD}>|R&tY^edwe9*ryfM88(|w(~Jxp zg$<68S?daIy2li;R2_Z$>BNuIU(w-tzd3NVO~cNgek!(1P4VyPCDa*wt_I*5J(a=M zOu#u|PW5H?C=YReRB@P1{>TlH=x&mfLw=*In6ef><&2uDk#zVCbr=oh>9flq&GA1| zoZ)=u-a%wl#VgGBzbxAQM=bCFIcmWwy+-c@%W*yFHuC){efgwMxL-E?A)i&sO3u^D zbMa+a6X~Zpx!Y9qtcOSUq8Y2esc*xV1=N~umz|kJlZ;YH|E9X$x18P6&aOv#UEI}- zB;U!xBt_1Uv&*1JU!|EQAs83)c~J&%h7!fc;6_-vJF zyW^%im8V^+sh&-BzAO1z3%n+ixMoi62N=mQk#IpWo6K7;Pol^Aqz`ez$8ebHYBIW^ zPkrb1f?CXebn^XpWdqLcjKVqbN! zR*hY|Zfe433ArqUcdO6}={{*|!dF>nWv{SP$>P5=BE8>438(4gO6)){nj%yze#izy za7Gg+_9RU`m<^gJNAz^01liw4kB+2UkJDj|cv3mxTAO)LJ!CkZ<44^a&%p+q*X?~X z9?gYAe<6JvScFi=J=AQy@6-FoUsjQI#17KYst->y!aw_lXFL`9TNRJRXq591t@-@G z$3=Q`oY^z{{{H-s+sNg?q)a&J7O(ttUGB(v8h#+{)635~h&#_iZCCKkqol7IE707o zhmq|Wq;n45ykM4ECpn6~&abCeH=SqMBj0;+fAX2%eJ+EcCp94tal89OuCAX~dzqeTFSaPFhD-mSy7>scIRj%drPt z`JhVbqVhi3oL`cgH~=NjK6qlDs|1<2;9!@B4)7^kf^jL{=7VH=p=J zxrQzzuBm)#OFMj0t$TSpylmRxcJ=M6S?<#+O)mSYD*M$2_51P4r?Ecu$Zj^;a+}zH zyhykhWC<@wYyf zg?xvUy`~1Ow)wU>c#}!;y-{DuEJ1nF_W}!XIo?6sP>1jEI>}B$|F7u9Wmdk#j;?1^ zBN2V^U=49!#3`PK4Gn`4b+PO7yscLgzVHO+8RFz=_A{B58Ol%WVqJ}31ULJw+)nC2 zT)r5;-o^qpW~XbCt_(itW!bbUEOiV2Q&#-6(rMR$Cmz5PZ_3H!m96|o=Iemi{v#Iq z1s?J%_SG2|J|Z(-+nJR%H~Ea)L@wyAs7-GUbf)q6GxqT>>s*YFcN4uA?q)ien9p{V z-mC0&r&D~&N_#k&TH?V7q~UibRTQUmwVyDTZ6B#Q%$M0syMAJCZ=gm;r#PHW`obBl zV-I%uJ`aD+_5QzU(-31H#=Q&C^-*3&7U*aUXU9ZoNBKi5;1@0M-zIfr-R$E&JGj$s zy79H%r9oQIAEoqP+#_rMG7oHxZpKIH?ZEe^c^t=m&P7<%1~gj9_T<5D4|}z=k5}xb zlbl~IXVwxQ&8G2|3K1bi@ z=b7ZBL-N4Qek7?OBls~y^EG~36%lkP81p}JO>^X5KQ|w9m(2NOT_){uVK+LlhgWBR zer)PYlJ~cRq%G$MB-6p6A2OFX=x>O39QPF_-4A(cK)u%Bm@jcm*jaD- zJQDh6*0Yt%eXSzCt;`mO9^v;rc^Xe(p;K8z=dU9%fAM;9iBYnt6wH<2qcl5O zL1MnP%lWu&6ghv_&imuhmz;HJ>kc==1-$Ih1lHZyUP3qCT_{D0C{jukt*{r*T`jd-_vY!Yp|9bSFJI zkxkm^gn}*`mxyb>^8E`u@)@q*ME?K6XO&r#M!u@CB{AOWue_$96;3&$eK>A3?z~ye z^=YX1-;m3KBqT)4RoJUr=$&n3We6|teYPUZT3o~?9K-D)N;`y!>E&UzOWVHxx3l7~!*&%WRxL)+&&bXiC{3tIT4YtPS?9ANh@^blPyi4%> zijdA5PVX1%-^I%BwUeK$YNGeIfzLK~5|7YV&xi?Td&dSxl?AwRl?)#)K#3VdIV zMG76s&FS;r_V)%!uSlP4#lz#QY(5J9UsmicpBMa^pJd9yX)Hk1c?s_R(q4{ORh%8X z)h^>!w#+%c%KkJYC&5$h#P3>(cXE=yinusr>w+f!zr58?;?XeYHK$$Vl=Hfg_}Ra9 znGFT9`^>=U^{uy*zZVes-bWUm<4+7APg8N%WE!s{9xUzMarDUH8QJY4bmHCS{e=?t z@jR=xi>BFa7rXtr!1{-aVOIFxAXTATbRAy#+WNosJxoLlH~-Z`&0P9*$H)sjNxQkt z3UW~mshj?9JS4cR^>^Zvjw2Nx;IB7W%5ZPx4tVaT&LPwUR8IH>FOZrR__m@wp7WFUam*xM z;wJmrN=CjTH-S%1kbp3;>PNH>vnu;1UT@P96YVsDo}KB)5p-iMoO6Vo?8+O;p%(3G z{7&+yl^x;pwMPCrIll zCwsT^dX+97#V>!!KCAnmLbUv4^!N$Y0yQf6-*5+E3IBh=?`_6bba9V5ac^f-4Lu}I?w++B=VFKeG{~xH4g43&bLS-Jyy}lR4$&2Mi zOVi#J?YSi+Hr$%on9UAV8H*A!avyJM54|&1{58yzg11_c2NSTA2S{_2HvWTN&tz?B zV)K*!l|~*fi(@n4(|XP@bbxN7>3fsI57MbS|`8Lt{QYnr}*?EK9-zvh`?)Mhq+2y$ZAdb&X2BUbgC=FkiMrh-E zurX!OFie=vZ@1SHG8*KoqW`Ny3Lf)%m{v(v;W(5XneFqopvhISWKN&=pHo|fn(xzh zUA-e@J-6YLfAGrz=ez=+CX<3Hc%TlRszo{?Nn0Q?q0;OH+EL9qo&S~7Y)wigLkQj? zG4<(-Ev(09PIM>k_!DQ}gi^)%cR3T5d?l$kO1oF38^fIDLe3`OvOynQv4i?}vKQ+$ zkd1oK8Kk(uIn;3dfOmJJ{4dV*W;&o5Dr7k0L!R>k%{>`cggKP!P;V|fGmVA$#nS`z zqE7M;_BT{`o+9%nJZpc#GKDO1hypg?)>$%bE1YA7>@c%4s|u3e?V&L5yt#9&;54eT0inh)OfkPJD5quP~9W(h` z6L8H~nqixjuW?pq{YLOr_tGjW{rgwGYYsH1M;A4=%4(>!(W)CkHY+76kG9x%m_B+x z{(m!p`rK~^KYHpOzkAB(1RMCJCuH#J>hm1);kgR-QCfy)lNF4xk7Ky2t|zp%gO|u- zE-}C{zw?E&|B^hM^^9`pR~g@jo3W3h;TW0Z2)52f#*%Y*Js^WH28eH-jX zsImHuWM{?YC;Tll3A~%c)N_W-?Bh}U%mHOx?KxYle-wUchj%{r?96!fW_!N}4YIi5 zI`F~AD6r0Mw|ecxeN|91Sx@S4dTTU2FwP!E;HTAozW^#U#8G$P&YP?#_+zIa1fhm9 zSmJYjKev<4jAz0G!SXo&Bsn-k+g`H6^<*$;%fCp)O@5ZeD+fL<>vJFRxuN1J#wMS{ zAz|v|FnqGpN&e##F4DqvXs@N`{6KoO~(0^ zR@vc{MyX-wLR-(W%bPs0PQo|o%toZBYn?|&O!b;W*3MgNP8O&oo~w#uQtd1F1OKAH zc69jO-|i$)&(Kt5tT^l<%)bB5O8O_N@TWPW0;m*Tl@gwJJ#zfJPw$U^CtKaO^zLHs z%0aeDzm5%A5F1en(<9+NwsKPmwaX+tbIuG_mI^$RL9?z?JgT4*E=s63 zlpfmU#-7dl=1d*JS@Oq0weTK2E5!D=7 zSpMFqp38X*fxN7OvIK0XHtAW&YYlxdf5lTo=r7n=xCMQX2xk`GU?hpCK!QHz-6m(G zi=-ao@h`?P+r0u>Qao{b`J7E|S~k?BU7xlsahoORT}U?pBB0>!x@BX!s+zGZakfnL^Q>{PqR@s(IK1Yrt$Ef z8Kh?#&RB~2nMnPeUg4hgt4{42`MN^KZFJ`AeBbPIk2}*)9kT%+{>Ubu<8l7uj4vd7 zy7TnKdc5ApCoe>`{r-KzDTEstF8I7qZ@0$3Hu~9ygl_wrWTn!fwaHcw*19u&a=~vM zwdx>Qtx)1E_TY?MaT`AE(=e|-yrB~G+(CPrN^)j-)?zy3C^-*Ooj;M?xn@V-d)`5N zh$gh+_f`=ws|+4csP4+J<5V)c)3c8G^H-MnJL_8IH#2*}%_`g$q0MKW@SZc7$btu? z>o=CZe8Qi33cZW_oC|jOy`3#W%|WDm41KhdUd_qQ))0vVytgPxD}rmjcfuce$4Aa~ zoi$&wy0Rp&1>JV1wZ~9(0s4)zxA{JC9nBJ#LFvdd9>>Z&Mc)RWZ5GYif=BmYBA?#J z8Ks+=G(rsb5ijLU+|bn1Q{+7cvD_WV{!p*?ocn6JFe}|q)z@vfA&tiS0Y`l1i67Co zVe<1d|NdL#Qj7kmmq2$KCZvBlT^_nO`qAin_;rj(;51#oKb$6k{Cq8E`9y#Xc z)GU4s=zq*P2gD|LC}H|WK&c!0?ZTcPX6A1}**SPJ++MMPjt&+6MV;g=2~VuB&%aKS zPT~2!nBa&*G;9T{3n!A%TfEHbL-lBo$C6&*c9Arkum&x{?D(%yEuSZasik*&Pi~)c3fF~RflysI z%kz#ngOc_YZdwVRPX`wL1w8wZRixwoJ#=5FIPOa~bw|TlJ~NMXy#~>p%pV_(x7*-` z%lL3H4`2;$+3jrhT5qxymG(Jhyu#`Nrgx5Xh6wHwn{WchedmA5(yKjiKsTp$pWP4zwORzeSi2=`|zjG$vnwa2OFa{kYvPA?HqsDWur1`3hizwAA9goK*V;2#Bl z>UlJrC@Px{&+JM&-A*TNMTKC;-@>Ec%&G@3F8CvA56D8J~DpUQ~EPrlTXumf+uCO-RNLT(*s5 z9YnRfR@~az_Vcbzkk9+kwh&Df?!UQCPh{m`*7x&}7ai#xElEhw=hy6ie!`3ICbnNj z7uQU@YbLH6X%`QZvxB5R+=5k9?R`x$9&onp&h!HAit}3Xidg@#%GEd_^g#{5Q!~-+ z4E_D6b&sT3x~;WVpBJfR+5@=~H+tVNQONgzwW8rql`lv~~8Q zSmfQ-UfQQUM;~^x&sMlKlc;AE+J~C~N7%t~v^@hys(>SU$w5r9k3OWQwrAJ!y*611 zh~7&y>t^1@37*0yKCL#cjQmoBiO~J_82JfV@E@ICs32d=7kC@@ zylkbdcp;V4z+X`>dfv2zD5^I}WL=liB1QNlD`D=1Vb?#SQaxI91AS3OC0#%L{-N8y zIBC8akDf@XDl0rtjoef6=SN`R%~5L_jo;Ff#?nex>4Zmc{8WC@akSrRe^1b=XY6}D zX*|sLxE;UGb~@>@^#3<)JIvW0fE)gX#;?`^nPJl1U}yQKcVEK&xwjak9UW$SAtf)E;#|+vR6U(XGqqwG#B&D5%9!=hK1J_z@QlNkj=V*@Y-h8Et=; z09{I=)cOG>Ue&A$A-eQF4%%W+<`=N``33<*E z9>Oe|xr(@82K4GTxAJ{Fnk6!tY%jt61!aDg z=rLX$|08~nm7ju{wdb>RWeu{bG5Ey!_hiY3u)2S#R3E_aKCRj|T@LCq=WvB2JxY2` zn9H#ef1ZrzqD2Rz-+%nXXK~+5-2O4#eHBjlRVHXDjW`%$I*P>n&ncSFB*rPKBdQ#^ z8)UUd`3g_c&KL1xWi=?9ozhK7oA5+KeEx&e9xtPKI$jAsF2_5mcrM~>vLz75T`WR> zR%xIJb1R>=AYOUIN?t&lXX)&V{=YT7nr05t^>{WJiB{}fz`G05GWB>vF>&K_IHeHW z_%>WyALq=XcYBCUT06O;?#lQ|O;QonLrZXDWfAma>NO9>liA9}R&*KXTvO58L%bAf zL3?`7W!%2kQ?ByTe?_0r^|*-i9wvE9QNEMi-{)y{5_zc^yv<*HMhTP{o5;2v4@M(WC#=a&Xcoq^`Ho!!s;(AOX}ZZyU(TX0fAnmkmGzKaw3cvtY> zOY-veInjAeDD=OL!$%XG;bpe;y8Vs8Y2ikXhzfweq29%;V=PZA$(e<^{fAkugMO!$ zSTGx&EFea$i$~V-U`Hn9rUmmv+eJG?%SPvrfVc3=408Fgim|ooPvhxR$Xo%sqX-F^ zqF-^fPU?=4f2?geeR)c)(smMEfaI@cXJ3M|hkCX#ynsm35Hrl4Fu(Rs)n%i+=NmP0 zN0WM(GgHlt5cz1{#*yVb+Gk05Ve^{9OxRw!72DA!)mh`SDpszMk>_|1AK8l=~k=*l0=T`=0i_?x#UDo|n@l=ldCNt51 zgwCO<;<_Nl=?48dUc;GXMy@{>|Nf z{;G=pZ>;%o6AwDdtQK}YIaujEY-3|}DECGFbKVVD^bz>@imL1-dN{Yy2se|TorzAx z!7OrnC!a5YKfJ4cJ4TZwleK4@YUy6z(R3()aNaXeRAYIUe33?sVZdPcXUmovEL;B&1r+G>g?WE zpZ_qcy_Q_vW|qYhJcdwRH-=u$OVT&Z3c53M@E6w@%<;x_w8i@z<_TbV?*BUQ}LTBojbwr7r#o0*|9_N)oWx45}A79YWX znx+?~kv$fPEaDB%#j884zGl3b$*9k%{&;`}3b#_ElY>0!d}i^l*RdIU)!II$hNly+ z>}0$lDK3NiHzX<{E|RL3MYYTEeGc_myGcO_r~V}gsKHXNq^rZ+%dct1TU3KTuOH|P zj{JuY)|t(yMQW2s+yhp%Pkuh!mp6h>buIp*&x*yLq%9xCf%ll-(J#_eR(v!sa}r(u z9V)d|=P`ihj(b5R!$p>+K+*?lJ-$)D-V5(sjX%o{-O3UyWA8)9$p#Xi z+x*5S*~Y6ZXdYZPliyRB1z5*ECedQwSyAz%MRf1asQrw}m=93kAyze+wDv~PjdI&t zaG2+N*LiwkzIk4I;_30)Vuf&@f$0iqu%C950Oh=X6-B^a%>cLuA`>uEsocuEVbwAnZhf_;NHi*YoIhnJn!9MU>70=C1<`MD# zjYq4gu|G~OvL=mC=XG&5NBH>}30=N4(E(M;`>QYi<%H z^b~s?Mu!%xaRzE;cH%SH|M4nnRekK2NT4s5C!p}MgB^Zhaqo&zRfo}xNel2b& z%TCWk^PH;9aEz7L z^hIktLZ;iI{ct|~|M-WecwRwPxi{=^HL~<6G_jUlt~sA`yreB9}WZtd%I-u3vc;-b-F-+#$`Qxtj$C96BZ&3yR#$^9=R`4A2* zMbC^xnN(hNGk*N@c30bqa>_x5Ui8ps8@HY?BR9T*C>buBOh<+=uTPG4WBL}_@9U#Rc zr}%2&-qb$4)*XpB{xcCyBc9OhIOD&h2UMK5gevUBwI_Jr*G;kN%O1a>3ON_AB^m!8 z6EB5n&jsj|7eodFRK6TG!*O%`I6d8xbtyngA4n=l*Hu*=xtR};C+QnM$&zuDXL#Bt zwZJc7%0mYd@R`?SI{2YPMDzk5xP%z30j)9{En=*8Bb?ifjS74G8I}Hr3(JV8YWmFX zqR^eZoiffnphtJ%jeppsFHp1(E7lo>Kf?cE9z{!9Vx-u9t|!jnQFn2++3j-!?`%GP zndWPrzi)!PorgZ%%+oIc2fSNs7bg3JJE+4=s;NAagHVr^V)BXBGfIT=iAetkG1(1B zLvE*5z$eG)*V8=8fRK;#zKPzy$-j^L^nXe1e~F*(OFVxT4f7G-V+p_bg3lzUnMK%p;nqY!{(dJs@XcPQV40E!4Pd^116tPpAM2^$!K; zg^Fx=d0yKsR+nmRyXl#wqKK7v{*dQJ{G_7K3$=S8|6a_Q9QB?sO?&`Z83GrdE1S5T z2KWoMPYv0}ANYMey3G_D%<^fWvf`qjh90;3V%v#4B^eyIXK`m~(Tll{NCJ?PRi%q#a~t3h8;*>OMlD zA4PIE+ePRZsgK+5M7?Wh@Hwp#`kn`pySYy5C(<11W{RRmetPvXOTNk4rjf>B{G3qf zwh2xXvSbffU3+??m1kE#yYqIuRwO*jK0;mBKM3)JP&rZ_wH^~MKj#dZ`ovsLY#$C<=&U9>`I*jkhxcYf(}%3;DO&VNf2-;m z4{|fzr-Z73tu$x48ns%9_q|{b4eUFIGuVZq({a*dpSaYSUKDqR%E^$IuY=c;`F@){ zZ#cX2s@MOk>fCy>sG=~;siue^BpStNlsj%ryfPtP8GnFR#`tgi173N%FR&P?a9_By<4tv!48?3vHG45#Y}kiYQ$ zDIPyUr0gXkFJt95ocEh;UxVAR(r=75Xb*9Hh+J^od!2lnK$|{BX6~{cz^3JD`QJrv z=Ow>gwyqFeGh|5%SuNsm8a|FbzfR=5gHN4bxsN{JQ?T?~R(|Ws*>6{g(TCJs1(!YO zwGR(SHAY@JQ|2sJ&C_Jn_45C7Bs(`^FGYOKX|v%w^5H0+KMws(>N<1lePZn+Jo^DU zY=BdXGk#<1Pvo8_uf~X`eOT}zezmn+I4#{yuvx>b9vpM{(xJ4DIC@Xj}ItHG80I;T>mc%%k-)TJHIx zoT3$;2Ge<3yIsVtzmwC=x*P8}mk4>CW|dVWvaa*CY1LUav5Ev@l% zfYQx%=?OfBu+v#_m&p+44d@RM>(q75?}a!X`4~+epih2txCfdcaGosrFYxlqqH<0_ ztGv^TMCUg3;-hi$dx7(Q1;<&>&nQo*YX&*@)Be^Vc#k7Rugp?9oOfQWv*omLk!1;A zdEM>ci)5c(zFRVeD-L}6wStY?!AXtg!Ai<|=;E{4o2Anrxiwz8&%s+XNASkIinXi6 z_f@RCUwHW*hTnHthLdt1S5z34J@C2()c~5C*Gtfd)k0w<2J*WGr<+iXVu^FQ?0p`< z+kPLui^%Z%NFyNRo35gNf%OgOA40zXT0X1V&UM`1k(feGANKlP7_;FPoNq(h!oqf; z^b`#rl{Nh({T5U`&^;)7o%InEoqs+9T?-pUexR~y82*o;(Gs&{p?oAZgUHoO{Y5v9 z6m>IOo!RBw@5#d0Ir$f8C$4VFbY}Y+x#jyG8~FVR5(eOO8&n!!_zf_#(>Z`M$Txll zk=0xB{1qs5S}Y%^w|Mn&_BwuYe%uvg*(Y1Z=Ur&jjjVO_cP^;&m7TwG5n8{c(+9WA zyzinV+QD-==giFFojz!t!+#AbBi1&h1Lk^jp^%uWm^1F27d!-aBW{#9a({#)!yId| z?f0w9O>@*`oFaz(_`$YIO6!dmT^pK7Y;=ybb^Mm2TN8zaM?99dh1<2&=FC(fX1(@i zy@}%KJZ<0NahBjTXQRbfX{Owviq}E5)ZMt5#YcV-%-C`6+9>DrwprzK1KV4vAK{%D zsBA-}pXEp!+ntk`7Wp5fOdxfxA}H-fJf@{)gk=yN%ya#%)Thvkv2P!#^(d5?-_wU| zHEk8W5u#L!Gbu*FxmDECYnyLUi&`5#@|v!-t&IK(?s`WorSY0E1C5>?rMBJ+F1D+- zJLZB{i>GXhX|Ch_@KU#7_Gi#)fvkT~Et{>H^W@dPuUfV{tcTDbs62<#*2H#6ZnioP zsznlIV`2&pws8wAb8Oq<=o6#Iw{vYHr;xg(o6nJ)AEO_PO6@b6Y&}IRS4C%TS;WP2 zi75Rq7V_)V+8i_vY=w<7&nAi*)WlbLIW8(Ldd}k>bvv=IEVpvb+;+)Xb7KrWQbV*m z{}p|b7#_uTy{ZJUd4dJTjT(EUHy`DpKP}eO?};vF78_wA7C%cp0!CxhizUi!;yWRZc5S?b5rU1BeHy3AbpI*IubOs(l|`JAiYxbjc+k7Rz#SAP0osu zNCt`7vch;Z5@U;6=uekvwtc(Pd{U1fwB>PaP`}>GP0VZM=7`alm=vo}#N*Nj*(L_0u(mwpB&HU9>DqJQ zphZSSv=PBvN7UC$vWCTLnXOFtm|Y$*D`K1dK&5NF-tZb|o?4_8S4)GMa@1wMc-lt@ zD)thTEha{A2MWEMo@c{PZS9Zh1?Bo*OXTSpZAhk?2SLeWTA*&WBKk{vlG%y+M85L$ zm2{3%JcBMa#0s$vPKl62O|Eqfvi77Cb$Y>w*6*&hL9TAOmUn!j)L_~vn-eKP((l`? zQg3CmXo0gKU5X+mc$vCX}LGPE*9TN9hg6q_dY^50k!1ASnP#7LsVC{Rn~Y-ysVksyAF z>e%Dacv(*3;=X>;DtTy!sJdrd$Up1Y5|p)6++($r*G(=JUwfza9?9@ z8)yA*9jW;#kEKt;U)I1p^<28XG_~H`4cUBt~<-J?W}p6 zR=6~0a$c*OBOCAu;?@&AX+)*rDXt!AYBgI*^7t;+k>YgVLX?0V#e*1 zCbpaX;2rF@eAHDICBBS}^Ng(c?(Gk(|6t_lB6oX^+}RVecQVACsqa5rs{8Q2>dl*z zjP3Z{++)=FjU&G^@^*%MO5aa#?7=ajON_9bfaXUi&vA5k!@He3+|xTk{|h5=r=js) z{Ra1|=b-(W@yu@--#yA`_AzLDtL<-UI7(>$)c(j4I@zP1qNQFf?0l=p9>=$_jv=v)q(I@6Xca1UU{bk=!w|Abq oqDeGariMI}_#PvxA5%V|9N{_E=ZtiJ$(Zd|tiRwX-e=qX1ysnLCjbBd literal 0 HcmV?d00001 diff --git a/ernie-sat/wavs/task_edit_pred.wav b/ernie-sat/wavs/task_edit_pred.wav new file mode 100644 index 0000000000000000000000000000000000000000..6bfda0fa42584cce5690ac86da095384200647d4 GIT binary patch literal 188276 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 aN44fx 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 literal 0 HcmV?d00001 diff --git a/ernie-sat/wavs/task_synthesize_pred.wav b/ernie-sat/wavs/task_synthesize_pred.wav new file mode 100644 index 0000000000000000000000000000000000000000..ce1379919274e5a8cc113d09e73ed869db0bdd56 GIT binary patch literal 177910 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 ZlvWxOp+M6=x9l{)uua#HP6rla>c( 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 TEuaOwK

    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