Repository: ku-nlp/kwja (language: python)

Issue:
Support Python 3.11
Python 3.11.0 is now released: https://www.python.org/downloads/release/python-3110/
Fix the senter module writer

Support Python 3.11
Python 3.11.0 is now released: https://www.python.org/downloads/release/python-3110/
Improve inference speed
## How

- [x] Load model parameters only once
- [x] Perform padding to the max sequence length in a batch
- [x] Use `torch.inference_mode` (https://pytorch.org/docs/stable/generated/torch.inference_mode.html)
- [x] Use `torch.compile` (https://pytorch.org/docs/stable/generated/torch.compile.html)

Introduce the configuration file
## What

It would be nice to have a configuration file that can specify the location of the model files, the default device to use, etc.

## How to implement

Put this kind of file in `$XDG_CONFIG_HOME/kwja`.

```yaml
# $XDG_CONFIG_HOME/kwja/config.yaml
model_path: null
model_size: base
device: gpu
typo_batch_size: 1
char_batch_size: 1
word_batch_size: 1
discourse: true
```

Document ids are not assigned correctly
## What

When using the `--filename` option to perform batch processing, document ids are not assigned correctly.

- Version: 2.0.0

## To reproduce

```
$ cat input.txt
今日は晴れ。
明日は雨。
EOD
隣町のレストラン。
EOD

$ kwja --model-size tiny --tasks "char,word" --filename input.txt
# S-ID:202303271616-0-0 kwja:2.0.0
* 1D
+ 1D <NE:DATE:今日><体言><時間>
今日 きょう 今日 名詞 6 時相名詞 10 * 0 * 0 "代表表記:今日/きょう カテゴリ:時間" <基本句-主辞>
は は は 助詞 9 副助詞 2 * 0 * 0 "代表表記:は/は"
* -1D
+ -1D <体言><時制:非過去><レベル:C><状態述語><節-区切><節-主辞>
晴れ 晴れ 晴れ 動詞 2 * 0 子音動詞ラ行 10 * 0 <基本句-主辞><用言表記先頭><用言表記末尾>
。 。 。 特殊 1 句点 1 * 0 * 0 "代表表記:。/。"
EOS
# S-ID:202303271616-1-0 kwja:2.0.0
* 1D
+ 1D <NE:DATE:明日><体言><時間>
明日 あす 明日 名詞 6 時相名詞 10 * 0 * 0 "代表表記:明日/あす カテゴリ:時間" <基本句-主辞>
は は は 助詞 9 副助詞 2 * 0 * 0 "代表表記:は/は"
* -1D
+ -1D <用言:判><体言><時制:非過去><レベル:C><状態述語><節-区切><節-主辞>
雨 あめ 雨 名詞 6 普通名詞 1 * 0 * 0 "代表表記:雨/あめ 漢字読み:訓 カテゴリ:抽象物" <基本句-主辞><用言表記先頭><用言表記末尾>
。 。 。 特殊 1 句点 1 * 0 * 0 "代表表記:。/。"
EOS
# S-ID:202303271616-2-0 kwja:2.0.0
* 1D
+ 1D <体言><係:ノ格>
隣町 よこまち 隣町 名詞 6 普通名詞 1 * 0 * 0 <基本句-主辞>
の の の 助詞 9 接続助詞 3 * 0 * 0 "代表表記:の/の"
* -1D
+ -1D <rel type="ガ" target="不特定:物"/><rel type="=" target="不特定:物"/><用言:判><体言><時制:非過去><SM-主体><レベル:C><状態述語><節-区切><節-主辞>
レストラン れすとらん レストラン 名詞 6 普通名詞 1 * 0 * 0 "代表表記:レストラン/れすとらん カテゴリ:場所-施設 ドメイン:料理・食事;ビジネス" <基本句-主辞><用言表記先頭><用言表記末尾>
。 。 。 特殊 1 句点 1 * 0 * 0 "代表表記:。/。"
EOS
```

KWJA recognizes that there are three documents (i.e., 202303271616-{0, 1, 2}), but actually, there are two.


Discussion / hints:
Waiting for PyTorch to support Python 3.11.
https://github.com/pytorch/pytorch/issues/86566

Waiting for PyTorch to support Python 3.11.
https://github.com/pytorch/pytorch/issues/86566




Fix patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..64e49567
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,56 @@
+name: Build
+
+on: [ push, pull_request ]
+
+jobs:
+  build:
+    name: Build the project
+    runs-on: ${{ matrix.os }}
+    strategy:
+      max-parallel: 4
+      fail-fast: false
+      matrix:
+        os: [ ubuntu-latest, macos-latest, windows-latest ]
+        python-version: [ "3.8", "3.9", "3.10", "3.11" ]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install Poetry
+        run: |
+            pipx install poetry
+            echo "$HOME/.local/bin" >> $GITHUB_PATH
+      - name: Install dependencies
+        run: |
+          poetry install --no-interaction --without dev,test
+      - name: Build KWJA
+        run: |
+          poetry build
+      - name: Install KWJA from wheel (non-Windows)
+        if: ${{ matrix.os != 'windows-latest' }}
+        run: |
+          pip3 install dist/*.whl
+      - name: Install KWJA from wheel (Windows)
+        if: ${{ matrix.os == 'windows-latest' }}
+        run: |
+          $whlFile = (Get-ChildItem -Path dist -Filter *.whl).FullName
+          pip3 install $whlFile
+        shell: pwsh
+      - name: Run KWJA (non-Windows)
+        if: ${{ matrix.os != 'windows-latest' }}
+        run: |
+          kwja --version
+          kwja --help
+          kwja --tasks typo,senter,char,word --model-size tiny --text "自然言語処理"
+          kwja --tasks typo,senter,seq2seq,word --model-size tiny --text "自然言語処理"
+      - name: Run KWJA (Windows)
+        if: ${{ matrix.os == 'windows-latest' }}
+        run: |
+          $env:PYTHONUTF8 = "1"
+          kwja --version
+          kwja --help
+          kwja --tasks typo,senter,char,word --model-size tiny --text "自然言語処理"
+          kwja --tasks typo,senter,seq2seq,word --model-size tiny --text "自然言語処理"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a22144da..acc7a9c9 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -6,24 +6,18 @@ jobs:
   lint:
     runs-on: ubuntu-latest
     strategy:
-      max-parallel: 3
+      max-parallel: 4
       fail-fast: false
       matrix:
-        python-version: [ "3.8", "3.9", "3.10" ]
+        python-version: [ "3.8", "3.9", "3.10", "3.11" ]
     steps:
       - name: Checkout repository
         uses: actions/checkout@v3
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v3
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
-      - name: Install pre-commit
-        run: |
-          python3 -m pip install --user pipx
-          python3 -m pipx ensurepath
-          python3 -m pipx install pre-commit
-      - name: Add path for Python packages
-        run: echo "$HOME/.local/bin" >> $GITHUB_PATH
-      - name: Run linters
+      - name: Install pre-commit and run linters
         run: |
+          pipx install pre-commit
           pre-commit run --all-files
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a612ccc8..d4473cda 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,11 +14,11 @@ jobs:
         uses: actions/checkout@v3
       - name: Create Release
         id: create_release
-        uses: actions/create-release@v1
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
+        uses: softprops/action-gh-release@v1
+        if: startsWith(github.ref, 'refs/tags/')
         with:
+          body: |
+            [Changelog](https://github.com/ku-nlp/kwja/blob/main/CHANGELOG.md)
           tag_name: ${{ github.ref }}
-          release_name: Release ${{ github.ref }}
           draft: false
           prerelease: false
diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml
index d8148f46..a5341dd7 100644
--- a/.github/workflows/typos.yml
+++ b/.github/workflows/typos.yml
@@ -9,9 +9,10 @@ on:
       - reopened
 
 jobs:
-  build:
+  typos:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - name: typos-action
-        uses: crate-ci/typos@v1.12.7
+      - name: Checkout repository
+        uses: actions/checkout@v3
+      - name: Check spelling
+        uses: crate-ci/typos@master
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 67f01955..73beb09b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
       - id: trailing-whitespace
       - id: check-yaml
   - repo: https://github.com/psf/black
-    rev: 23.1.0
+    rev: 23.3.0
     hooks:
       - id: black
   - repo: https://github.com/PyCQA/flake8
@@ -20,17 +20,19 @@ repos:
     hooks:
       - id: isort
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.1.1
+    rev: v1.3.0
     hooks:
       - id: mypy
         additional_dependencies:
-          - rhoknp==1.2.1
+          - rhoknp==1.3.0
           - hydra-core==1.3.2
-          - torch==1.13.1
+          - torch==2.0.0
           - torchmetrics==0.11.4
-          - transformers==4.25.1
-          - tokenizers==0.13.2
-          - wandb==0.13.11
+          - transformers==4.28.1
+          - tokenizers==0.13.3
+          - wandb==0.15.1
+          - typer==0.9.0
+          - types-PyYAML==6.0.12.9
   - repo: https://github.com/jumanjihouse/pre-commit-hooks
     rev: 3.0.0
     hooks:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b26c835..0232aa80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## [v2.1.0] - 2023-06-02
+### Added
+- Support Python 3.11.
+- Support NN-based sentence segmentation.
+  ```shell
+  kwja --tasks senter --text "モーニング娘。は日本のアイドルグループです。"
+  ```
+- Support multiple files as input.
+  ```shell
+  kwja --filename file1.txt --filename file2.txt
+  ```
+- Introduce a config file. You can specify some options in `XDG_CONFIG_HOME/kwja/config.yaml`.
+  ```yaml
+  model_size: base
+  device: cpu
+  num_workers: 0
+  torch_compile: false
+  typo_batch_size: 1
+  senter_batch_size: 1
+  seq2seq_batch_size: 1
+  char_batch_size: 1
+  word_batch_size: 1
+  ```
+- Implement padding truncation of word module to accelerate inference.
+- Support Windows.
+
+### Changed
+- Support CUDA 11.7 by default instead of CUDA 10.x.
+- Skip typo correction by default.
+- Optimize package requirements for faster loading.
+- Optimize model initialization for faster loading.
+- Replace mt5 models with t5 models pre-trained on Japanese corpora in seq2seq module.
+- Use partially annotated data for word normalization to train seq2seq module.
+
+### Removed
+- Remove the discourse module.
+
+### Fixed
+- Fix a bug that warning messages are shown when Juman++ and/or KNP are not installed.
+- Fix a bug that document IDs are not assigned properly when a text file is given as input.
+
 ## [2.0.0] - 2023-03-14
 
 ### Added
@@ -118,7 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Removed
 - Remove an unnecessary dependency, `fugashi`.
 
-[Unreleased]: https://github.com/ku-nlp/kwja/compare/v2.0.0...HEAD
+[Unreleased]: https://github.com/ku-nlp/kwja/compare/v2.1.0...HEAD
+[2.1.0]: https://github.com/ku-nlp/kwja/compare/v2.0.0...v2.1.0
 [2.0.0]: https://github.com/ku-nlp/kwja/compare/v1.4.2...v2.0.0
 [1.4.2]: https://github.com/ku-nlp/kwja/compare/v1.4.1...v1.4.2
 [1.4.1]: https://github.com/ku-nlp/kwja/compare/v1.4.0...v1.4.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 005ea4a1..c0d51b65 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -64,21 +64,15 @@ Options:
 "build_datasets.sh" performs formatting KWDLC and annotated FKC corpus.
 
 ```shell
-./scripts/build_datasets.sh
-  -a $(poetry run echo $VIRTUAL_ENV)/bin/activate
-  -w /path/to/work_dir
-  -s $(realpath ./scripts)
-  -j 2
-  -o /path/to/output_dir
+./scripts/build_datasets.sh \
+  --jobs 2 \
+  --out-dir /path/to/output_dir
 ```
 
 Options:
 
-- `-a`: path to activator
-- `-w`: path to working directory
-- `-s`: path to scripts
-- `-j`: number of jobs
-- `-o`: path to output directory
+- `--jobs`: number of jobs
+- `--out-dir`: path to output directory
 
 NOTE:
 To train word module on Kyoto University Text Corpus, you must have access to it and IREX CRL named entity data.
@@ -86,10 +80,10 @@ If you have both access, you can format the corpus with the following commands.
 (You may need preprocessing to format IREX CRL named entity data.)
 
 ```shell
-poetry run python scripts/add_features_to_raw_corpus.py
-  KyotoCorpus/knp
-  kyoto/knp
-  --ne-tags IREX_CRL_NE_data.jmn
+poetry run python scripts/build_dataset.py \
+  ./KyotoCorpus/knp \
+  ./kyoto/knp \
+  --ne-tags ./IREX_CRL_NE_data.jmn \
   -j 2
 poetry run kyoto idsplit \
   --corpus-dir kyoto/knp \
@@ -97,6 +91,11 @@ poetry run kyoto idsplit \
   --train KyotoCorpus/id/full/train.id \
   --valid KyotoCorpus/id/full/dev.id \
   --test KyotoCorpus/id/full/test.id
+poetry run python scripts/build_dataset.py \
+  ./KyotoCorpus/knp \
+  ./kyoto_ed \
+  --id ./KyotoCorpus/id/syntax-only \
+  -j 32
 ```
 
 ## Training and evaluation
@@ -117,16 +116,20 @@ poetry run python scripts/test.py module=char checkpoint_path="/path/to/checkpoi
 
 ## Debugging
 
-You can do debugging on local and server environments:
 
-Local environment (using CPU):
+```shell
+# For debugging word segmenter
+poetry run python scripts/train.py -cn char_module.debug
+```
+
+If you are on a machine with MPS devices (e.g. Apple M1), specify `trainer=cpu.debug` to use CPU.
 
 ```shell
 # For debugging word segmenter
-poetry run python scripts/train.py -cn char_module.debug devices=1
+poetry run python scripts/train.py -cn char_module.debug trainer=cpu.debug
 ```
 
-Server environment (using GPU):
+If you are on a machine with GPUs, you can specify the GPUs to use with the `devices` option.
 
 ```shell
 # For debugging word segmenter
diff --git a/README.md b/README.md
index 07a7d424..579e2e7f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# KWJA: Kyoto-Waseda Japanese Analyzer
+# KWJA: Kyoto-Waseda Japanese Analyzer[^1]
+
+[^1]: Pronunciation: [/kuʒa/](http://ipa-reader.xyz/?text=%20ku%CA%92a)
 
 [![test](https://github.com/ku-nlp/kwja/actions/workflows/test.yml/badge.svg)](https://github.com/ku-nlp/kwja/actions/workflows/test.yml)
 [![codecov](https://codecov.io/gh/ku-nlp/kwja/branch/main/graph/badge.svg?token=A9FWWPLITO)](https://codecov.io/gh/ku-nlp/kwja)
@@ -9,25 +11,27 @@
 [[Paper]](https://ipsj.ixsq.nii.ac.jp/ej/?action=pages_view_main&active_action=repository_view_main_item_detail&item_id=220232&item_no=1&page_id=13&block_id=8)
 [[Slides]](https://speakerdeck.com/nobug/kyoto-waseda-japanese-analyzer)
 
-KWJA is a Japanese language analyzer based on pre-trained language models.
-KWJA performs many language analysis tasks, including:
+KWJA is an integrated Japanese text analyzer based on foundation models.
+KWJA performs many text analysis tasks, including:
 - Typo correction
+- Sentence segmentation
 - Word segmentation
 - Word normalization
 - Morphological analysis
 - Word feature tagging
-- NER (Named Entity Recognition)
 - Base phrase feature tagging
+- NER (Named Entity Recognition)
 - Dependency parsing
-- PAS analysis
+- Predicate-argument structure (PAS) analysis
 - Bridging reference resolution
 - Coreference resolution
 - Discourse relation analysis
 
 ## Requirements
 
-- Python: 3.8, 3.9, 3.10
+- Python: 3.8+
 - Dependencies: See [pyproject.toml](./pyproject.toml).
+- GPUs with CUDA 11.7 (optional)
 
 ## Getting Started
 
@@ -43,8 +47,8 @@ Perform language analysis with the `kwja` command (the result is in the KNP form
 # Analyze a text
 $ kwja --text "KWJAは日本語の統合解析ツールです。汎用言語モデルを利用し、様々な言語解析を統一的な方法で解いています。"
 
-# Analyze a text file and write the result to a file
-$ kwja --file path/to/file.txt > path/to/analyzed.knp
+# Analyze text files and write the result to a file
+$ kwja --filename path/to/file1.txt --filename path/to/file2.txt > path/to/analyzed.knp
 
 # Analyze texts interactively
 $ kwja
@@ -53,7 +57,14 @@ KWJAは日本語の統合解析ツールです。汎用言語モデルを利用
 EOD
 ```
 
-The output is in the KNP format, like the following:
+If you use Windows and PowerShell, you need to set `PYTHONUTF8` environment variable to `1`:
+
+```shell
+> $env:PYTHONUTF8 = "1"
+> kwja ...
+````
+
+The output is in the KNP format, which looks like the following:
 
 ```
 # S-ID:202210010000-0-0 kwja:1.0.2
@@ -70,27 +81,34 @@ KWJA ＫWＪＡ KWJA 名詞 6 固有名詞 3 * 0 * 0 <基本句-主辞>
 ...
 ```
 
-Here are some other options for `kwja` command:
+Here are options for `kwja` command:
+
+- `--text`: Text to be analyzed.
+
+- `--filename`: Path to a text file to be analyzed. You can specify this option multiple times.
 
-`--model-size`: Model size to be used. Please specify 'tiny', 'base' (default) or 'large'.
+- `--model-size`: Model size to be used. Specify one of `tiny`, `base` (default), and `large`.
 
-`--device`: Device to be used. Please specify 'cpu' or 'gpu'.
+- `--device`: Device to be used. Specify `cpu` or `gpu`. If not specified, the device is automatically selected.
 
-`--typo-batch-size`: Batch size for typo module.
+- `--typo-batch-size`: Batch size for typo module.
 
-`--seq2seq-batch-size`: Batch size for seq2seq module.
+- `--senter-batch-size`: Batch size for senter module.
 
-`--char-batch-size`: Batch size for char module.
+- `--seq2seq-batch-size`: Batch size for seq2seq module.
 
-`--word-batch-size`: Batch size for word module.
+- `--char-batch-size`: Batch size for character module.
 
-`--tasks`: Tasks to be performed. Please specify 'typo', 'char', 'seq2seq', 'typo,char', 'typo,seq2seq', 'seq2seq,word', 'char,word', 'typo,char,word',  'typo,seq2seq,word', 'char,word,word_discourse', 'seq2seq,word,word_discourse', 'typo,char,word,word_discourse' or 'typo,char,word,word_discourse'.
+- `--word-batch-size`: Batch size for word module.
+
+- `--tasks`: Tasks to be performed. Specify one or more of the following values separated by commas:
   - `typo`: Typo correction
+  - `senter`: Sentence segmentation
   - `seq2seq`: Word segmentation, Word normalization, Reading prediction, lemmatization, and Canonicalization.
   - `char`: Word segmentation and Word normalization
   - `word`: Morphological analysis, Named entity recognition, Word feature tagging, Dependency parsing, PAS analysis, Bridging reference resolution, and Coreference resolution
-  - `word_discourse`: Discourse relation analysis
-    - If you need the results of discourse relation analysis, please specify this in addition to `word`.
+
+`--config-file`: Path to a custom configuration file.
 
 You can read a KNP format file with [rhoknp](https://github.com/ku-nlp/rhoknp).
 
@@ -127,172 +145,237 @@ analyzed_document = kwja.apply(
 )
 ```
 
+## Configuration
+
+`kwja` can be configured with a configuration file to set the default options.
+ Check [Config file content](#config-file-example) for details.
+
+### Config file location
+
+On non-Windows systems `kwja` follows the
+[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
+convention for the location of the configuration file.
+The configuration dir `kwja` uses is itself named `kwja`.
+In that directory it refers to a file named `config.yaml`.
+For most people it should be enough to put their config file at `~/.config/kwja/config.yaml`.
+You can also provide a configuration file in a non-standard location with an environment variable `KWJA_CONFIG_FILE` or a command line option `--config-file`.
+
+### Config file example
+
+```yaml
+model_size: base
+device: cpu
+num_workers: 0
+torch_compile: false
+typo_batch_size: 1
+senter_batch_size: 1
+seq2seq_batch_size: 1
+char_batch_size: 1
+word_batch_size: 1
+```
+
 ## Performance Table
 
-- The performance on each task except typo correction and discourse relation analysis is the mean over all the corpora (KC, KWDLC, Fuman, and WAC) and over three runs with different random seeds.
-- We set the learning rate of RoBERTa<sub>LARGE</sub> (word) to 2e-5 because we failed to fine-tune it with a higher learning rate.
-  Other hyperparameters are the same described in configs, which are tuned for DeBERTa<sub>BASE</sub>.
+- typo, senter, character, and word modules
+  - The performance on each task except typo correction and discourse relation analysis is the mean over all the corpora (KC, KWDLC, Fuman, and WAC) and over three runs with different random seeds.
+    - \* denotes results of a single run (TBU)
+  - We set the learning rate of RoBERTa<sub>LARGE</sub> (word) to 2e-5 because we failed to fine-tune it with a higher learning rate.
+    Other hyperparameters are the same described in configs, which are tuned for DeBERTa<sub>BASE</sub>.
+- seq2seq module
+  - The performance on each task is the mean over all the corpora (KC, KWDLC, Fuman, and WAC).
+    - \* denotes results of a single run
+  - Scores are calculated using a separate [script](https://github.com/ku-nlp/kwja/blob/main/scripts/view_seq2seq_results.py) from the character and word modules.
 
 <table>
-  <tr>
-    <th rowspan="2" colspan="2">Task</th>
-    <th colspan="4">Model</th>
-  </tr>
-  <tr>
-    <th>
-        RoBERTa<sub>BASE</sub><br>
+  <thead>
+    <tr>
+      <th rowspan="2" colspan="2">Task</th>
+      <th colspan="6">Model</th>
+    </tr>
+    <tr>
+      <th>
+        v1.x base<br>
         (
             <a href="https://huggingface.co/ku-nlp/roberta-base-japanese-char-wwm">char</a>,
             <a href="https://huggingface.co/nlp-waseda/roberta-base-japanese">word</a>
         )
-    </th>
-    <th>
-        DeBERTa<sub>BASE</sub><br>
+      </th>
+      <th>
+        v2.x base<br>
         (
             <a href="https://huggingface.co/ku-nlp/deberta-v2-base-japanese-char-wwm">char</a>,
-            <a href="https://huggingface.co/ku-nlp/deberta-v2-base-japanese">word</a>
+            <a href="https://huggingface.co/ku-nlp/deberta-v2-base-japanese">word</a> /
+            <a href="https://huggingface.co/retrieva-jp/t5-base-long">seq2seq</a>
         )
-    </th>
-    <th>
-        RoBERTa<sub>LARGE</sub><br>
+      </th>
+      <th>
+        v1.x large<br>
         (
             <a href="https://huggingface.co/ku-nlp/roberta-large-japanese-char-wwm">char</a>,
             <a href="https://huggingface.co/nlp-waseda/roberta-large-japanese-seq512">word</a>
         )
-    </th>
-    <th>
-        DeBERTa<sub>LARGE</sub><br>
+      </th>
+      <th>
+        v2.x large<br>
         (
             <a href="https://huggingface.co/ku-nlp/deberta-v2-large-japanese-char-wwm">char</a>,
-            <a href="https://huggingface.co/ku-nlp/deberta-v2-large-japanese">word</a>
+            <a href="https://huggingface.co/ku-nlp/deberta-v2-large-japanese">word</a> /
+            <a href="https://huggingface.co/retrieva-jp/t5-large-long">seq2seq</a>
         )
-    </th>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Typo Correction</th>
-    <td>TBU</td>
-    <td>TBU</td>
-    <td>TBU</td>
-    <td>TBU</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Sentence Segmentation</th>
-    <td>TBU</td>
-    <td>TBU</td>
-    <td>TBU</td>
-    <td>TBU</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Word Segmentation</th>
-    <td>98.5</td>
-    <td>98.6</td>
-    <td>98.7</td>
-    <td style="font-weight: bold;">98.9</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Word Normalization</th>
-    <td>44.0</td>
-    <td>39.2</td>
-    <td>39.8</td>
-    <td style="font-weight: bold;">46.0</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th rowspan="5" style="text-align: center;">Morphological<br>Analysis</th>
-    <th style="text-align: left;">POS</th>
-    <td>99.3</td>
-    <td style="font-weight: bold;">99.4</td>
-    <td>99.3</td>
-    <td style="font-weight: bold;">99.4</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th style="text-align: left;">sub-POS</th>
-    <td>98.1</td>
-    <td style="font-weight: bold;">98.4</td>
-    <td>98.2</td>
-    <td style="font-weight: bold;">98.4</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th style="text-align: left;">conjugation type</th>
-    <td>99.4</td>
-    <td style="font-weight: bold;">99.5</td>
-    <td>99.2</td>
-    <td>99.4</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th style="text-align: left;">conjugation form</th>
-    <td>99.5</td>
-    <td style="font-weight: bold;">99.6</td>
-    <td>99.4</td>
-    <td style="font-weight: bold;">99.6</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th style="text-align: left;">reading</th>
-    <td style="font-weight: bold;">95.5</td>
-    <td>95.2</td>
-    <td>90.8</td>
-    <td>95.1</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Named Entity Recognition</th>
-    <td>83.0</td>
-    <td>83.8</td>
-    <td>82.1</td>
-    <td style="font-weight: bold;">84.6</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th rowspan="2" style="text-align: center;">Linguistic<br>Feature<br>Tagging</th>
-    <th style="text-align: left;">word</th>
-    <td>98.3</td>
-    <td style="font-weight: bold;">98.5</td>
-    <td style="font-weight: bold;">98.5</td>
-    <td>98.4</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th style="text-align: left;">base phrase</th>
-    <td>86.6</td>
-    <td style="font-weight: bold;">89.5</td>
-    <td>86.4</td>
-    <td>89.3</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Dependency Parsing</th>
-    <td>92.9</td>
-    <td>93.4</td>
-    <td style="font-weight: bold;">93.8</td>
-    <td>93.3</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Pas Analysis</th>
-    <td>74.2</td>
-    <td>76.7</td>
-    <td>75.3</td>
-    <td style="font-weight: bold;">76.9</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Bridging Reference Resolution</th>
-    <td>66.5</td>
-    <td style="font-weight: bold;">67.3</td>
-    <td>65.2</td>
-    <td>67.0</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Coreference Resolution</th>
-    <td>74.9</td>
-    <td style="font-weight: bold;">78.4</td>
-    <td>75.9</td>
-    <td>78.0</td>
-  </tr>
-  <tr style="text-align: right;">
-    <th colspan="2" style="text-align: center;">Discourse Relation Analysis</th>
-    <td>42.2</td>
-    <td style="font-weight: bold;">44.8</td>
-    <td>41.3</td>
-    <td>41.0</td>
-  </tr>
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <th colspan="2">Typo Correction</th>
+      <td>TBU</td>
+      <td>76.7*</td>
+      <td>TBU</td>
+      <td>83.1</td>
+    </tr>
+    <tr>
+      <th colspan="2">Sentence Segmentation</th>
+      <td>-</td>
+      <td>98.2</td>
+      <td>-</td>
+      <td>98.2</td>
+    </tr>
+    <tr>
+      <th colspan="2">Word Segmentation</th>
+      <td>98.5</td>
+      <td>98.6 / 98.2*</td>
+      <td>98.7</td>
+      <td>98.9 / 98.4*</td>
+    </tr>
+    <tr>
+      <th colspan="2">Word Normalization</th>
+      <td>44.0</td>
+      <td>39.2</td>
+      <td>39.8</td>
+      <td>46.0</td>
+    </tr>
+    <tr>
+      <th rowspan="7">Morphological Analysis</th>
+      <th>POS</th>
+      <td>99.3</td>
+      <td>99.4</td>
+      <td>99.3</td>
+      <td>99.4</td>
+    </tr>
+    <tr>
+      <th>sub-POS</th>
+      <td>98.1</td>
+      <td>98.4</td>
+      <td>98.2</td>
+      <td>98.4</td>
+    </tr>
+    <tr>
+      <th>conjtype</th>
+      <td>99.4</td>
+      <td>99.5</td>
+      <td>99.2</td>
+      <td>99.4</td>
+    </tr>
+    <tr>
+      <th>conjform</th>
+      <td>99.5</td>
+      <td>99.6</td>
+      <td>99.4</td>
+      <td>99.6</td>
+    </tr>
+    <tr>
+      <th>reading</th>
+      <td>95.5</td>
+      <td>95.2 / 96.2*</td>
+      <td>90.8</td>
+      <td>95.1 / 96.8*</td>
+    </tr>
+    <tr>
+      <th>lemma</th>
+      <td>-</td>
+      <td>- / 97.8*</td>
+      <td>-</td>
+      <td>- / 98.1*</td>
+    </tr>
+    <tr>
+      <th>canon</th>
+      <td>-</td>
+      <td>- / 95.2*</td>
+      <td>-</td>
+      <td>- / 95.9*</td>
+    </tr>
+    <tr>
+      <th colspan="2">Named Entity Recognition</th>
+      <td>83.0</td>
+      <td>83.8</td>
+      <td>82.1</td>
+      <td>84.6</td>
+    </tr>
+    <tr>
+      <th rowspan="2">Linguistic Feature Tagging</th>
+      <th>word</th>
+      <td>98.3</td>
+      <td>98.5</td>
+      <td>98.5</td>
+      <td>98.4</td>
+    </tr>
+    <tr>
+      <th>base phrase</th>
+      <td>86.6</td>
+      <td>89.5</td>
+      <td>86.4</td>
+      <td>89.3</td>
+    </tr>
+    <tr>
+      <th colspan="2">Dependency Parsing</th>
+      <td>92.9</td>
+      <td>93.4</td>
+      <td>93.8</td>
+      <td>93.3</td>
+    </tr>
+    <tr>
+      <th colspan="2">Pas Analysis</th>
+      <td>74.2</td>
+      <td>76.7</td>
+      <td>75.3</td>
+      <td>76.9</td>
+    </tr>
+    <tr>
+      <th colspan="2">Bridging Reference Resolution</th>
+      <td>66.5</td>
+      <td>67.3</td>
+      <td>65.2</td>
+      <td>67.0</td>
+    </tr>
+    <tr>
+      <th colspan="2">Coreference Resolution</th>
+      <td>74.9</td>
+      <td>78.4</td>
+      <td>75.9</td>
+      <td>78.0</td>
+    </tr>
+    <tr>
+      <th colspan="2">Discourse Relation Analysis</th>
+      <td>42.2</td>
+      <td>44.8</td>
+      <td>41.3</td>
+      <td>41.0</td>
+    </tr>
+  </tbody>
 </table>
 
 ## Citation
 
+```bibtex
+@InProceedings{Ueda2023a,
+  author    = {Nobuhiro Ueda and Kazumasa Omura and Takashi Kodama and Hirokazu Kiyomaru and Yugo Murawaki and Daisuke Kawahara and Sadao Kurohashi},
+  title     = {KWJA: A Unified Japanese Analyzer Based on Foundation Models},
+  booktitle = {Proceedings of the 61st Annual Meeting of the Association for Computational Linguistics: System Demonstrations},
+  year      = {2023},
+  address   = {Toronto, Canada},
+}
+```
+
 ```bibtex
 @InProceedings{植田2022,
   author    = {植田 暢大 and 大村 和正 and 児玉 貴志 and 清丸 寛一 and 村脇 有吾 and 河原 大輔 and 黒橋 禎夫},
@@ -313,6 +396,10 @@ analyzed_document = kwja.apply(
 }
 ```
 
+## License
+
+This software is released under the MIT License, see [LICENSE](LICENSE).
+
 ## Reference
 
 - [KNP format](http://cr.fvcrc.i.nagoya-u.ac.jp/~sasano/knp/format.html)
diff --git a/configs/base_template.yaml b/configs/base_template.yaml
index 6d5b8247..3e714de1 100644
--- a/configs/base_template.yaml
+++ b/configs/base_template.yaml
@@ -47,3 +47,4 @@ hydra:
           - logger
           - max_batches_per_device
           - do_predict_after_train
+          - compile
diff --git a/configs/char_module.debug.yaml b/configs/char_module.debug.yaml
index ef41ad71..46d2364a 100644
--- a/configs/char_module.debug.yaml
+++ b/configs/char_module.debug.yaml
@@ -32,9 +32,10 @@ warmup_steps: 10
 effective_batch_size: 16
 
 # environment dependent settings
-devices: ${oc.env:DEVICES,0}
+devices: ${oc.env:DEVICES,1}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,0}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/char_module.yaml b/configs/char_module.yaml
index 1f5d1679..674fe0ff 100644
--- a/configs/char_module.yaml
+++ b/configs/char_module.yaml
@@ -27,7 +27,7 @@ aggregating_metrics:
 
 # hyper-parameters to be tuned
 lr: 2e-5
-max_epochs: 20
+max_epochs: 12
 warmup_steps: 2000
 effective_batch_size: 32
 
@@ -35,6 +35,7 @@ effective_batch_size: 32
 devices: ${oc.env:DEVICES,0}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,4}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/datamodule/char.yaml b/configs/datamodule/char.yaml
index c6bc449d..9f6aba86 100644
--- a/configs/datamodule/char.yaml
+++ b/configs/datamodule/char.yaml
@@ -1,6 +1,7 @@
 defaults:
   - train:
     - char_kyoto.yaml
+    - char_kyoto_ed.yaml
     - char_kwdlc.yaml
     - char_fuman.yaml
     - char_wac.yaml
@@ -19,3 +20,4 @@ defaults:
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: char
diff --git a/configs/datamodule/predict/char_inference.yaml b/configs/datamodule/predict/char_inference.yaml
index c22ebe5c..4fe4c090 100644
--- a/configs/datamodule/predict/char_inference.yaml
+++ b/configs/datamodule/predict/char_inference.yaml
@@ -3,5 +3,4 @@ defaults:
   - _self_
 
 _target_: kwja.datamodule.datasets.CharInferenceDataset
-texts: []
-doc_id_prefix: null
+senter_file: null
diff --git a/configs/datamodule/predict/seq2seq_inference.yaml b/configs/datamodule/predict/seq2seq_inference.yaml
index 6ec17525..0cdb8bb2 100644
--- a/configs/datamodule/predict/seq2seq_inference.yaml
+++ b/configs/datamodule/predict/seq2seq_inference.yaml
@@ -3,4 +3,4 @@ defaults:
   - _self_
 
 _target_: kwja.datamodule.datasets.Seq2SeqInferenceDataset
-texts: []
+senter_file: null
diff --git a/configs/datamodule/senter.yaml b/configs/datamodule/senter.yaml
index 93d957ab..d87b2b96 100644
--- a/configs/datamodule/senter.yaml
+++ b/configs/datamodule/senter.yaml
@@ -1,6 +1,7 @@
 defaults:
   - train:
     - senter_kyoto.yaml
+    - senter_kyoto_ed.yaml
     - senter_kwdlc.yaml
     - senter_fuman.yaml
     - senter_wac.yaml
@@ -19,3 +20,4 @@ defaults:
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: senter
diff --git a/configs/datamodule/seq2seq.yaml b/configs/datamodule/seq2seq.yaml
index fbb8c6d3..fba97fc2 100644
--- a/configs/datamodule/seq2seq.yaml
+++ b/configs/datamodule/seq2seq.yaml
@@ -1,21 +1,26 @@
 defaults:
   - train:
     - seq2seq_kyoto.yaml
+    - seq2seq_kyoto_ed.yaml
     - seq2seq_kwdlc.yaml
     - seq2seq_fuman.yaml
     - seq2seq_wac.yaml
+    - seq2seq_norm.yaml
   - valid:
     - seq2seq_kyoto.yaml
     - seq2seq_kwdlc.yaml
     - seq2seq_fuman.yaml
     - seq2seq_wac.yaml
+    - seq2seq_norm.yaml
   - test:
     - seq2seq_kyoto.yaml
     - seq2seq_kwdlc.yaml
     - seq2seq_fuman.yaml
     - seq2seq_wac.yaml
+    - seq2seq_norm.yaml
   - predict:
     - seq2seq_inference.yaml
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: seq2seq
diff --git a/configs/datamodule/train/char_kyoto_ed.yaml b/configs/datamodule/train/char_kyoto_ed.yaml
new file mode 100644
index 00000000..7bbb73de
--- /dev/null
+++ b/configs/datamodule/train/char_kyoto_ed.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/char@kyoto_ed
+
+kyoto_ed:
+  path: ${oc.env:DATA_DIR}/kyoto_ed/all
diff --git a/configs/datamodule/train/senter_kyoto_ed.yaml b/configs/datamodule/train/senter_kyoto_ed.yaml
new file mode 100644
index 00000000..5cd540a8
--- /dev/null
+++ b/configs/datamodule/train/senter_kyoto_ed.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/senter@kyoto_ed
+
+kyoto_ed:
+  path: ${oc.env:DATA_DIR}/kyoto_ed/all
diff --git a/configs/datamodule/train/seq2seq_kyoto_ed.yaml b/configs/datamodule/train/seq2seq_kyoto_ed.yaml
new file mode 100644
index 00000000..42bb7bff
--- /dev/null
+++ b/configs/datamodule/train/seq2seq_kyoto_ed.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/seq2seq@kyoto_ed
+
+kyoto_ed:
+  path: ${oc.env:DATA_DIR}/kyoto_ed/all
diff --git a/configs/datamodule/train/seq2seq_norm.yaml b/configs/datamodule/train/seq2seq_norm.yaml
new file mode 100644
index 00000000..0d9e0b13
--- /dev/null
+++ b/configs/datamodule/train/seq2seq_norm.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/seq2seq@norm
+
+norm:
+  path: ${oc.env:DATA_DIR}/norm/train
diff --git a/configs/datamodule/train/word_kyoto_ed.yaml b/configs/datamodule/train/word_kyoto_ed.yaml
new file mode 100644
index 00000000..42649c67
--- /dev/null
+++ b/configs/datamodule/train/word_kyoto_ed.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/word@kyoto_ed
+
+kyoto_ed:
+  path: ${oc.env:DATA_DIR}/kyoto_ed/all
diff --git a/configs/datamodule/typo.yaml b/configs/datamodule/typo.yaml
index f882e5e9..c52887c3 100644
--- a/configs/datamodule/typo.yaml
+++ b/configs/datamodule/typo.yaml
@@ -10,3 +10,4 @@ defaults:
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: typo
diff --git a/configs/datamodule/valid/seq2seq_norm.yaml b/configs/datamodule/valid/seq2seq_norm.yaml
new file mode 100644
index 00000000..0fe216fc
--- /dev/null
+++ b/configs/datamodule/valid/seq2seq_norm.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/seq2seq@norm
+
+norm:
+  path: ${oc.env:DATA_DIR}/norm/valid
diff --git a/configs/datamodule/word.yaml b/configs/datamodule/word.yaml
index d47baf88..9aa3de7e 100644
--- a/configs/datamodule/word.yaml
+++ b/configs/datamodule/word.yaml
@@ -1,6 +1,7 @@
 defaults:
   - train:
     - word_kyoto.yaml
+    - word_kyoto_ed.yaml
     - word_kwdlc.yaml
     - word_fuman.yaml
     - word_wac.yaml
@@ -19,3 +20,4 @@ defaults:
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: word
diff --git a/configs/datamodule/word_kwdlc.yaml b/configs/datamodule/word_kwdlc.yaml
index 9ad64346..afb0862a 100644
--- a/configs/datamodule/word_kwdlc.yaml
+++ b/configs/datamodule/word_kwdlc.yaml
@@ -10,3 +10,4 @@ defaults:
 
 batch_size: ${max_batches_per_device}
 num_workers: ${num_workers}
+dataset_type: word
diff --git a/configs/encoder/base.yaml b/configs/encoder/base.yaml
new file mode 100644
index 00000000..84cab04d
--- /dev/null
+++ b/configs/encoder/base.yaml
@@ -0,0 +1,9 @@
+pretrained_model_name_or_path: null
+from_config:
+  _target_: transformers.AutoModel.from_config
+  config:
+    _target_: transformers.AutoConfig.from_pretrained
+    pretrained_model_name_or_path: ${encoder.pretrained_model_name_or_path}
+from_pretrained:
+  _target_: transformers.AutoModel.from_pretrained
+  pretrained_model_name_or_path: ${encoder.pretrained_model_name_or_path}
diff --git a/configs/encoder/char_deberta_base.yaml b/configs/encoder/char_deberta_base.yaml
index 53050a8c..5c8788bd 100644
--- a/configs/encoder/char_deberta_base.yaml
+++ b/configs/encoder/char_deberta_base.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-base-japanese-char-wwm
diff --git a/configs/encoder/char_deberta_large.yaml b/configs/encoder/char_deberta_large.yaml
index a7d0ed6f..3cc588dc 100644
--- a/configs/encoder/char_deberta_large.yaml
+++ b/configs/encoder/char_deberta_large.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-large-japanese-char-wwm
diff --git a/configs/encoder/char_deberta_tiny.yaml b/configs/encoder/char_deberta_tiny.yaml
index 3adac086..36636551 100644
--- a/configs/encoder/char_deberta_tiny.yaml
+++ b/configs/encoder/char_deberta_tiny.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-tiny-japanese-char-wwm
diff --git a/configs/encoder/char_roberta_base.yaml b/configs/encoder/char_roberta_base.yaml
index 9a95a379..22c66679 100644
--- a/configs/encoder/char_roberta_base.yaml
+++ b/configs/encoder/char_roberta_base.yaml
@@ -1,3 +1,9 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/roberta-base-japanese-char-wwm
-add_pooling_layer: false
+from_config:
+  add_pooling_layer: false
+from_pretrained:
+  add_pooling_layer: false
diff --git a/configs/encoder/char_roberta_large.yaml b/configs/encoder/char_roberta_large.yaml
index 75ba45bd..1775056d 100644
--- a/configs/encoder/char_roberta_large.yaml
+++ b/configs/encoder/char_roberta_large.yaml
@@ -1,3 +1,9 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/roberta-large-japanese-char-wwm
-add_pooling_layer: false
+from_config:
+  add_pooling_layer: false
+from_pretrained:
+  add_pooling_layer: false
diff --git a/configs/encoder/seq2seq_mt5_base.yaml b/configs/encoder/seq2seq_mt5_base.yaml
index a0c34110..fde768cc 100644
--- a/configs/encoder/seq2seq_mt5_base.yaml
+++ b/configs/encoder/seq2seq_mt5_base.yaml
@@ -1,2 +1,9 @@
-_target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: google/mt5-base
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_mt5_large.yaml b/configs/encoder/seq2seq_mt5_large.yaml
index 077d4e1c..09760fdd 100644
--- a/configs/encoder/seq2seq_mt5_large.yaml
+++ b/configs/encoder/seq2seq_mt5_large.yaml
@@ -1,2 +1,9 @@
-_target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: google/mt5-large
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_mt5_small.yaml b/configs/encoder/seq2seq_mt5_small.yaml
index bae2dff9..caad2807 100644
--- a/configs/encoder/seq2seq_mt5_small.yaml
+++ b/configs/encoder/seq2seq_mt5_small.yaml
@@ -1,2 +1,9 @@
-_target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: google/mt5-small
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_mt5_xl.yaml b/configs/encoder/seq2seq_mt5_xl.yaml
index 14ca8aa1..52e975d2 100644
--- a/configs/encoder/seq2seq_mt5_xl.yaml
+++ b/configs/encoder/seq2seq_mt5_xl.yaml
@@ -1,2 +1,9 @@
-_target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: google/mt5-xl
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_t5_base.yaml b/configs/encoder/seq2seq_t5_base.yaml
new file mode 100644
index 00000000..38a6fb7c
--- /dev/null
+++ b/configs/encoder/seq2seq_t5_base.yaml
@@ -0,0 +1,9 @@
+defaults:
+  - base
+  - _self_
+
+pretrained_model_name_or_path: retrieva-jp/t5-base-long
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_t5_large.yaml b/configs/encoder/seq2seq_t5_large.yaml
new file mode 100644
index 00000000..bc7eb3f8
--- /dev/null
+++ b/configs/encoder/seq2seq_t5_large.yaml
@@ -0,0 +1,9 @@
+defaults:
+  - base
+  - _self_
+
+pretrained_model_name_or_path: retrieva-jp/t5-large-long
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_t5_small.yaml b/configs/encoder/seq2seq_t5_small.yaml
new file mode 100644
index 00000000..83f246e1
--- /dev/null
+++ b/configs/encoder/seq2seq_t5_small.yaml
@@ -0,0 +1,9 @@
+defaults:
+  - base
+  - _self_
+
+pretrained_model_name_or_path: retrieva-jp/t5-small-short
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/seq2seq_t5_xl.yaml b/configs/encoder/seq2seq_t5_xl.yaml
new file mode 100644
index 00000000..edaab6e9
--- /dev/null
+++ b/configs/encoder/seq2seq_t5_xl.yaml
@@ -0,0 +1,9 @@
+defaults:
+  - base
+  - _self_
+
+pretrained_model_name_or_path: retrieva-jp/t5-xl
+from_config:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_config
+from_pretrained:
+  _target_: transformers.AutoModelForSeq2SeqLM.from_pretrained
diff --git a/configs/encoder/word_deberta_base.yaml b/configs/encoder/word_deberta_base.yaml
index 6eb8791a..0048944c 100644
--- a/configs/encoder/word_deberta_base.yaml
+++ b/configs/encoder/word_deberta_base.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-base-japanese
diff --git a/configs/encoder/word_deberta_large.yaml b/configs/encoder/word_deberta_large.yaml
index f006b74e..e73bd482 100644
--- a/configs/encoder/word_deberta_large.yaml
+++ b/configs/encoder/word_deberta_large.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-large-japanese
diff --git a/configs/encoder/word_deberta_tiny.yaml b/configs/encoder/word_deberta_tiny.yaml
index f52a3cc5..ce2113b4 100644
--- a/configs/encoder/word_deberta_tiny.yaml
+++ b/configs/encoder/word_deberta_tiny.yaml
@@ -1,2 +1,5 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: ku-nlp/deberta-v2-tiny-japanese
diff --git a/configs/encoder/word_roberta_base.yaml b/configs/encoder/word_roberta_base.yaml
index 4b5b763b..5ef34bca 100644
--- a/configs/encoder/word_roberta_base.yaml
+++ b/configs/encoder/word_roberta_base.yaml
@@ -1,3 +1,9 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: nlp-waseda/roberta-base-japanese
-add_pooling_layer: false
+from_config:
+  add_pooling_layer: false
+from_pretrained:
+  add_pooling_layer: false
diff --git a/configs/encoder/word_roberta_large.yaml b/configs/encoder/word_roberta_large.yaml
index 74ead3b8..1aab5c8e 100644
--- a/configs/encoder/word_roberta_large.yaml
+++ b/configs/encoder/word_roberta_large.yaml
@@ -1,3 +1,9 @@
-_target_: transformers.AutoModel.from_pretrained
+defaults:
+  - base
+  - _self_
+
 pretrained_model_name_or_path: nlp-waseda/roberta-large-japanese-seq512
-add_pooling_layer: false
+from_config:
+  add_pooling_layer: false
+from_pretrained:
+  add_pooling_layer: false
diff --git a/configs/eval.yaml b/configs/eval.yaml
index 477b328e..e9010e2a 100644
--- a/configs/eval.yaml
+++ b/configs/eval.yaml
@@ -1,7 +1,7 @@
 defaults:
   - callbacks: [progress_bar.yaml]
   - logger: null
-  - module: null  # typo, seq2seq, char, or word
+  - module: null  # typo, senter, seq2seq, char, or word
   - _self_
 
 # required settings
@@ -28,3 +28,4 @@ discourse_parsing_threshold: 0.0
 num_workers: -1
 devices: ${oc.env:DEVICES,1}
 max_batches_per_device: 4
+compile: false
diff --git a/configs/senter_module.debug.yaml b/configs/senter_module.debug.yaml
index 334289af..feb7e026 100644
--- a/configs/senter_module.debug.yaml
+++ b/configs/senter_module.debug.yaml
@@ -28,9 +28,10 @@ warmup_steps: 10
 effective_batch_size: 16
 
 # environment dependent settings
-devices: ${oc.env:DEVICES,0}
+devices: ${oc.env:DEVICES,1}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,0}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/senter_module.yaml b/configs/senter_module.yaml
index f55bde59..bad622a9 100644
--- a/configs/senter_module.yaml
+++ b/configs/senter_module.yaml
@@ -22,15 +22,16 @@ aggregating_metrics:
   - sent_segmentation_f1
 
 # hyper-parameters to be tuned
-lr: 2e-5
-max_epochs: 20
-warmup_steps: 2000
-effective_batch_size: 32
+lr: 5e-5
+max_epochs: 12
+warmup_steps: 1500
+effective_batch_size: 48
 
 # environment dependent settings
 devices: ${oc.env:DEVICES,0}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,4}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/seq2seq_module.debug.yaml b/configs/seq2seq_module.debug.yaml
index e53596c6..85c62698 100644
--- a/configs/seq2seq_module.debug.yaml
+++ b/configs/seq2seq_module.debug.yaml
@@ -35,12 +35,114 @@ warmup_steps: 250
 effective_batch_size: 512
 
 # environment dependent settings
-devices: ${oc.env:DEVICES,0}
+devices: ${oc.env:DEVICES,1}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,0}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
 # constants
-special_tokens: ["<br>", "<no_read>", "<no_canon>"]
+special_tokens: [
+    "<extra_id_0>",
+    "<extra_id_1>",
+    "<extra_id_2>",
+    "<extra_id_3>",
+    "<extra_id_4>",
+    "<extra_id_5>",
+    "<extra_id_6>",
+    "<extra_id_7>",
+    "<extra_id_8>",
+    "<extra_id_9>",
+    "<extra_id_10>",
+    "<extra_id_11>",
+    "<extra_id_12>",
+    "<extra_id_13>",
+    "<extra_id_14>",
+    "<extra_id_15>",
+    "<extra_id_16>",
+    "<extra_id_17>",
+    "<extra_id_18>",
+    "<extra_id_19>",
+    "<extra_id_20>",
+    "<extra_id_21>",
+    "<extra_id_22>",
+    "<extra_id_23>",
+    "<extra_id_24>",
+    "<extra_id_25>",
+    "<extra_id_26>",
+    "<extra_id_27>",
+    "<extra_id_28>",
+    "<extra_id_29>",
+    "<extra_id_30>",
+    "<extra_id_31>",
+    "<extra_id_32>",
+    "<extra_id_33>",
+    "<extra_id_34>",
+    "<extra_id_35>",
+    "<extra_id_36>",
+    "<extra_id_37>",
+    "<extra_id_38>",
+    "<extra_id_39>",
+    "<extra_id_40>",
+    "<extra_id_41>",
+    "<extra_id_42>",
+    "<extra_id_43>",
+    "<extra_id_44>",
+    "<extra_id_45>",
+    "<extra_id_46>",
+    "<extra_id_47>",
+    "<extra_id_48>",
+    "<extra_id_49>",
+    "<extra_id_50>",
+    "<extra_id_51>",
+    "<extra_id_52>",
+    "<extra_id_53>",
+    "<extra_id_54>",
+    "<extra_id_55>",
+    "<extra_id_56>",
+    "<extra_id_57>",
+    "<extra_id_58>",
+    "<extra_id_59>",
+    "<extra_id_60>",
+    "<extra_id_61>",
+    "<extra_id_62>",
+    "<extra_id_63>",
+    "<extra_id_64>",
+    "<extra_id_65>",
+    "<extra_id_66>",
+    "<extra_id_67>",
+    "<extra_id_68>",
+    "<extra_id_69>",
+    "<extra_id_70>",
+    "<extra_id_71>",
+    "<extra_id_72>",
+    "<extra_id_73>",
+    "<extra_id_74>",
+    "<extra_id_75>",
+    "<extra_id_76>",
+    "<extra_id_77>",
+    "<extra_id_78>",
+    "<extra_id_79>",
+    "<extra_id_80>",
+    "<extra_id_81>",
+    "<extra_id_82>",
+    "<extra_id_83>",
+    "<extra_id_84>",
+    "<extra_id_85>",
+    "<extra_id_86>",
+    "<extra_id_87>",
+    "<extra_id_88>",
+    "<extra_id_89>",
+    "<extra_id_90>",
+    "<extra_id_91>",
+    "<extra_id_92>",
+    "<extra_id_93>",
+    "<extra_id_94>",
+    "<extra_id_95>",
+    "<extra_id_96>",
+    "<extra_id_97>",
+    "<extra_id_98>",
+    "<extra_id_99>"
+]
 hparams_to_ignore_on_save: []
diff --git a/configs/seq2seq_module.yaml b/configs/seq2seq_module.yaml
index 3e82b085..6feb8194 100644
--- a/configs/seq2seq_module.yaml
+++ b/configs/seq2seq_module.yaml
@@ -29,7 +29,7 @@ monitor: valid/seq2seq_loss
 mode: min
 
 # hyper-parameters to be tuned
-lr: 5e-4
+lr: 1e-3
 max_epochs: 100
 warmup_steps: 250
 effective_batch_size: 512
@@ -38,8 +38,110 @@ effective_batch_size: 512
 devices: ${oc.env:DEVICES,0}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,4}
+compile: ${oc.env:COMPILE,false}
 
-special_tokens: ["<br>", "<no_read>", "<no_canon>"]
+special_tokens: [
+    "<extra_id_0>",
+    "<extra_id_1>",
+    "<extra_id_2>",
+    "<extra_id_3>",
+    "<extra_id_4>",
+    "<extra_id_5>",
+    "<extra_id_6>",
+    "<extra_id_7>",
+    "<extra_id_8>",
+    "<extra_id_9>",
+    "<extra_id_10>",
+    "<extra_id_11>",
+    "<extra_id_12>",
+    "<extra_id_13>",
+    "<extra_id_14>",
+    "<extra_id_15>",
+    "<extra_id_16>",
+    "<extra_id_17>",
+    "<extra_id_18>",
+    "<extra_id_19>",
+    "<extra_id_20>",
+    "<extra_id_21>",
+    "<extra_id_22>",
+    "<extra_id_23>",
+    "<extra_id_24>",
+    "<extra_id_25>",
+    "<extra_id_26>",
+    "<extra_id_27>",
+    "<extra_id_28>",
+    "<extra_id_29>",
+    "<extra_id_30>",
+    "<extra_id_31>",
+    "<extra_id_32>",
+    "<extra_id_33>",
+    "<extra_id_34>",
+    "<extra_id_35>",
+    "<extra_id_36>",
+    "<extra_id_37>",
+    "<extra_id_38>",
+    "<extra_id_39>",
+    "<extra_id_40>",
+    "<extra_id_41>",
+    "<extra_id_42>",
+    "<extra_id_43>",
+    "<extra_id_44>",
+    "<extra_id_45>",
+    "<extra_id_46>",
+    "<extra_id_47>",
+    "<extra_id_48>",
+    "<extra_id_49>",
+    "<extra_id_50>",
+    "<extra_id_51>",
+    "<extra_id_52>",
+    "<extra_id_53>",
+    "<extra_id_54>",
+    "<extra_id_55>",
+    "<extra_id_56>",
+    "<extra_id_57>",
+    "<extra_id_58>",
+    "<extra_id_59>",
+    "<extra_id_60>",
+    "<extra_id_61>",
+    "<extra_id_62>",
+    "<extra_id_63>",
+    "<extra_id_64>",
+    "<extra_id_65>",
+    "<extra_id_66>",
+    "<extra_id_67>",
+    "<extra_id_68>",
+    "<extra_id_69>",
+    "<extra_id_70>",
+    "<extra_id_71>",
+    "<extra_id_72>",
+    "<extra_id_73>",
+    "<extra_id_74>",
+    "<extra_id_75>",
+    "<extra_id_76>",
+    "<extra_id_77>",
+    "<extra_id_78>",
+    "<extra_id_79>",
+    "<extra_id_80>",
+    "<extra_id_81>",
+    "<extra_id_82>",
+    "<extra_id_83>",
+    "<extra_id_84>",
+    "<extra_id_85>",
+    "<extra_id_86>",
+    "<extra_id_87>",
+    "<extra_id_88>",
+    "<extra_id_89>",
+    "<extra_id_90>",
+    "<extra_id_91>",
+    "<extra_id_92>",
+    "<extra_id_93>",
+    "<extra_id_94>",
+    "<extra_id_95>",
+    "<extra_id_96>",
+    "<extra_id_97>",
+    "<extra_id_98>",
+    "<extra_id_99>"
+  ]
 ignore_hparams_on_save: false
 
 # constants
diff --git a/configs/trainer/cpu.debug.yaml b/configs/trainer/cpu.debug.yaml
new file mode 100644
index 00000000..f1b68428
--- /dev/null
+++ b/configs/trainer/cpu.debug.yaml
@@ -0,0 +1,4 @@
+defaults:
+  - debug
+
+accelerator: cpu
diff --git a/configs/trainer/cpu.yaml b/configs/trainer/cpu.yaml
index b338543d..b6950c58 100644
--- a/configs/trainer/cpu.yaml
+++ b/configs/trainer/cpu.yaml
@@ -1,8 +1,4 @@
-_target_: pytorch_lightning.Trainer
-gradient_clip_val: 0.5
-accumulate_grad_batches: null
-max_epochs: ${max_epochs}
-min_epochs: 1
-accelerator: "cpu"
-strategy: ddp
-precision: 32
+defaults:
+  - default
+
+accelerator: cpu
diff --git a/configs/trainer/debug.yaml b/configs/trainer/debug.yaml
index 318f6fa5..06762fa9 100644
--- a/configs/trainer/debug.yaml
+++ b/configs/trainer/debug.yaml
@@ -1,12 +1,7 @@
-_target_: pytorch_lightning.Trainer
-gradient_clip_val: 0.5
+defaults:
+  - default
+
 #fast_dev_run: 3
-accumulate_grad_batches: null
-max_epochs: ${max_epochs}
-min_epochs: 1
 limit_train_batches: 10
 limit_val_batches: 10
 limit_test_batches: 10
-accelerator: "auto"     # set gpu or cpu automatically
-strategy: ddp
-precision: 32
diff --git a/configs/trainer/default.yaml b/configs/trainer/default.yaml
index aab155fd..4eb66968 100644
--- a/configs/trainer/default.yaml
+++ b/configs/trainer/default.yaml
@@ -1,8 +1,8 @@
 _target_: pytorch_lightning.Trainer
 gradient_clip_val: 0.5
-accumulate_grad_batches: null
+accumulate_grad_batches: 1
 max_epochs: ${max_epochs}
 min_epochs: 1
-accelerator: "auto"     # set gpu or cpu automatically
-strategy: ddp
 precision: 32
+accelerator: auto
+strategy: ddp_find_unused_parameters_true
diff --git a/configs/trainer/mps.debug.yaml b/configs/trainer/mps.debug.yaml
new file mode 100644
index 00000000..bdb7f799
--- /dev/null
+++ b/configs/trainer/mps.debug.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - debug
+
+accelerator: mps
+strategy: auto
diff --git a/configs/trainer/mps.yaml b/configs/trainer/mps.yaml
new file mode 100644
index 00000000..433d53b3
--- /dev/null
+++ b/configs/trainer/mps.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - default
+
+accelerator: mps
+strategy: auto
diff --git a/configs/typo_module.debug.yaml b/configs/typo_module.debug.yaml
index 6b9f3598..da8710b0 100644
--- a/configs/typo_module.debug.yaml
+++ b/configs/typo_module.debug.yaml
@@ -27,9 +27,10 @@ warmup_steps: 10
 effective_batch_size: 64
 
 # environment dependent settings
-devices: ${oc.env:DEVICES,0}
+devices: ${oc.env:DEVICES,1}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,0}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/typo_module.yaml b/configs/typo_module.yaml
index b865a91d..f4cea115 100644
--- a/configs/typo_module.yaml
+++ b/configs/typo_module.yaml
@@ -31,6 +31,7 @@ effective_batch_size: 176
 devices: ${oc.env:DEVICES,0}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,4}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
diff --git a/configs/word_module.debug.yaml b/configs/word_module.debug.yaml
index c82bbc6b..156ddbbf 100644
--- a/configs/word_module.debug.yaml
+++ b/configs/word_module.debug.yaml
@@ -58,12 +58,13 @@ warmup_steps: 10
 effective_batch_size: 16
 
 # environment dependent settings
-devices: ${oc.env:DEVICES,0}
+devices: ${oc.env:DEVICES,1}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,0}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
 # constants
-special_tokens: ${concat:${exophora_referents},["[NULL]", "[NA]", "[ROOT]"]}
+special_tokens: ["[著者]", "[読者]", "[不特定:人]", "[不特定:物]", "[NULL]", "[NA]", "[ROOT]"]
 hparams_to_ignore_on_save: []
diff --git a/configs/word_module.yaml b/configs/word_module.yaml
index 8009cccd..f6d77f96 100644
--- a/configs/word_module.yaml
+++ b/configs/word_module.yaml
@@ -53,7 +53,7 @@ aggregating_metrics:
 
 # hyper-parameters to be tuned
 lr: 1e-4
-max_epochs: 20
+max_epochs: 12
 warmup_steps: 100
 effective_batch_size: 16
 
@@ -61,11 +61,12 @@ effective_batch_size: 16
 devices: ${oc.env:DEVICES,0}
 max_batches_per_device: ${oc.env:MAX_BATCHES_PER_DEVICE,2}
 num_workers: ${oc.env:NUM_WORKERS,4}
+compile: ${oc.env:COMPILE,false}
 
 ignore_hparams_on_save: false
 
 # constants
-special_tokens: ${concat:${exophora_referents},["[NULL]", "[NA]", "[ROOT]"]}
+special_tokens: ["[著者]", "[読者]", "[不特定:人]", "[不特定:物]", "[NULL]", "[NA]", "[ROOT]"]
 hparams_to_ignore_on_save:
   - project
   - work_dir
diff --git a/poetry.lock b/poetry.lock
index c597d7f6..c352449a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "aiohttp"
 version = "3.8.4"
 description = "Async http client/server framework (asyncio)"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -113,7 +112,6 @@ speedups = ["Brotli", "aiodns", "cchardet"]
 name = "aiosignal"
 version = "1.3.1"
 description = "aiosignal: a list of registered asynchronous callbacks"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -128,7 +126,6 @@ frozenlist = ">=1.1.0"
 name = "antlr4-python3-runtime"
 version = "4.9.3"
 description = "ANTLR 4.9.3 runtime for Python 3.7"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -139,7 +136,6 @@ files = [
 name = "appdirs"
 version = "1.4.4"
 description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -151,7 +147,6 @@ files = [
 name = "appnope"
 version = "0.1.3"
 description = "Disable App Nap on macOS >= 10.9"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -163,7 +158,6 @@ files = [
 name = "asttokens"
 version = "2.2.1"
 description = "Annotate AST trees with source code positions"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -181,7 +175,6 @@ test = ["astroid", "pytest"]
 name = "async-timeout"
 version = "4.0.2"
 description = "Timeout context manager for asyncio programs"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -191,28 +184,26 @@ files = [
 
 [[package]]
 name = "attrs"
-version = "22.2.0"
+version = "23.1.0"
 description = "Classes Without Boilerplate"
-category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
-    {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
 ]
 
 [package.extras]
-cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
-tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
 
 [[package]]
 name = "backcall"
 version = "0.2.0"
 description = "Specifications for callback functions passed in to an API"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -222,21 +213,19 @@ files = [
 
 [[package]]
 name = "certifi"
-version = "2022.12.7"
+version = "2023.5.7"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
-    {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
+    {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"},
+    {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"},
 ]
 
 [[package]]
 name = "charset-normalizer"
 version = "3.1.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -321,7 +310,6 @@ files = [
 name = "click"
 version = "8.1.3"
 description = "Composable command line interface toolkit"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -332,11 +320,39 @@ files = [
 [package.dependencies]
 colorama = {version = "*", markers = "platform_system == \"Windows\""}
 
+[[package]]
+name = "cmake"
+version = "3.26.3"
+description = "CMake is an open-source, cross-platform family of tools designed to build, test and package software"
+optional = false
+python-versions = "*"
+files = [
+    {file = "cmake-3.26.3-py2.py3-none-macosx_10_10_universal2.macosx_10_10_x86_64.macosx_11_0_arm64.macosx_11_0_universal2.whl", hash = "sha256:9d38ea5b4999f8f042a071bea3e213f085bac26d7ab54cb5a4c6a193c4baf132"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2010_i686.manylinux_2_12_i686.whl", hash = "sha256:6e5fcd1cfaac33d015e2709e0dd1b7ad352a315367012ac359c9adc062cf075b"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:4d3185738a6405aa15801e684f8d589b00570da4cc676cb1b5bbc902e3023e53"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b20f7f7ea316ce7bb158df0e3c3453cfab5048939f1291017d16a8a36ad33ae6"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:46aa385e19c9e4fc95d7d6ce5ee0bbe0d69bdeac4e9bc95c61f78f3973c2f626"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:71e1df5587ad860b9829211380c42fc90ef2413363f12805b1fa2d87769bf876"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:543b6958d1615327f484a07ab041029b1740918a8baa336adc9f5f0cbcd8fbd8"},
+    {file = "cmake-3.26.3-py2.py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1bc7b47456256bdcc41069f5c658f232bd6e15bf4796d115f6ec98800793daff"},
+    {file = "cmake-3.26.3-py2.py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:2ae3db2c2be50fdaf0c9f3a23b2206e9dcd55ca124f16486a841b939f50b595e"},
+    {file = "cmake-3.26.3-py2.py3-none-musllinux_1_1_i686.whl", hash = "sha256:1798547b23b89030518c5668dc55aed0e1d01867cf91d7a94e15d33f62a56fd0"},
+    {file = "cmake-3.26.3-py2.py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:d3017a08e6ba53ec2486d89a7953a81d4c4a068fc9f29d83e209f295dd9c59f3"},
+    {file = "cmake-3.26.3-py2.py3-none-musllinux_1_1_s390x.whl", hash = "sha256:a922a6f6c1580d0db17b0b75f82e619441dd43c7f1d6a35f7d27e709db48bdbb"},
+    {file = "cmake-3.26.3-py2.py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e0ed796530641c8a21a423f9bb7882117dbbeee11ec78dbc335402a678d937ae"},
+    {file = "cmake-3.26.3-py2.py3-none-win32.whl", hash = "sha256:27a6fa1b97744311a7993d6a1e0ce14bd73696dab9ceb96701f1ec11edbd5053"},
+    {file = "cmake-3.26.3-py2.py3-none-win_amd64.whl", hash = "sha256:cf910bbb488659d300c86b1dac77e44eeb0457bde2cf76a42d7e51f691544b21"},
+    {file = "cmake-3.26.3-py2.py3-none-win_arm64.whl", hash = "sha256:24741a304ada699b339034958777d9a1472ac8ddb9b6194d74f814287ca091ae"},
+    {file = "cmake-3.26.3.tar.gz", hash = "sha256:b54cde1f1c0573321b22382bd2ffaf5d08f65188572d128cd4867fb9669723c5"},
+]
+
+[package.extras]
+test = ["codecov (>=2.0.5)", "coverage (>=4.2)", "flake8 (>=3.0.4)", "path.py (>=11.5.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.4.0)", "pytest-runner (>=2.9)", "pytest-virtualenv (>=1.7.0)", "scikit-build (>=0.10.0)", "setuptools (>=28.0.0)", "virtualenv (>=15.0.3)", "wheel"]
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -346,63 +362,71 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.2.1"
+version = "7.2.7"
 description = "Code coverage measurement for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"},
-    {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"},
-    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"},
-    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"},
-    {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"},
-    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"},
-    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"},
-    {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"},
-    {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"},
-    {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"},
-    {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"},
-    {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"},
-    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"},
-    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"},
-    {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"},
-    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"},
-    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"},
-    {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"},
-    {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"},
-    {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"},
-    {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"},
-    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"},
-    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"},
-    {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"},
-    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"},
-    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"},
-    {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"},
-    {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"},
-    {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"},
-    {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"},
-    {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"},
-    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"},
-    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"},
-    {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"},
-    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"},
-    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"},
-    {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"},
-    {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"},
-    {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"},
-    {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"},
-    {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"},
-    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"},
-    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"},
-    {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"},
-    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"},
-    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"},
-    {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"},
-    {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"},
-    {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"},
-    {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"},
-    {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"},
+    {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+    {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+    {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+    {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+    {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+    {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+    {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+    {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+    {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+    {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+    {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+    {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+    {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+    {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+    {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+    {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+    {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+    {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+    {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+    {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+    {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+    {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+    {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+    {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+    {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+    {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+    {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+    {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+    {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+    {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+    {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+    {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+    {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
 ]
 
 [package.dependencies]
@@ -415,7 +439,6 @@ toml = ["tomli"]
 name = "decorator"
 version = "5.1.1"
 description = "Decorators for Humans"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -427,7 +450,6 @@ files = [
 name = "docker-pycreds"
 version = "0.4.0"
 description = "Python bindings for the docker credentials store API"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -442,7 +464,6 @@ six = ">=1.4.0"
 name = "exceptiongroup"
 version = "1.1.1"
 description = "Backport of PEP 654 (exception groups)"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -457,7 +478,6 @@ test = ["pytest (>=6)"]
 name = "executing"
 version = "1.2.0"
 description = "Get the currently executing AST node of a frame, and other information"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -470,25 +490,23 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
 
 [[package]]
 name = "filelock"
-version = "3.9.0"
+version = "3.12.0"
 description = "A platform independent file lock."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"},
-    {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"},
+    {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
+    {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
 ]
 
 [package.extras]
-docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
-testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
 
 [[package]]
 name = "frozenlist"
 version = "1.3.3"
 description = "A list-like structure which implements collections.abc.MutableSequence"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -570,14 +588,13 @@ files = [
 
 [[package]]
 name = "fsspec"
-version = "2023.3.0"
+version = "2023.5.0"
 description = "File-system specification"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fsspec-2023.3.0-py3-none-any.whl", hash = "sha256:bf57215e19dbfa4fe7edae53040cc1deef825e3b1605cca9a8d2c2fadd2328a0"},
-    {file = "fsspec-2023.3.0.tar.gz", hash = "sha256:24e635549a590d74c6c18274ddd3ffab4753341753e923408b1904eaabafe04d"},
+    {file = "fsspec-2023.5.0-py3-none-any.whl", hash = "sha256:51a4ad01a5bb66fcc58036e288c0d53d3975a0df2a5dc59a93b59bade0391f2a"},
+    {file = "fsspec-2023.5.0.tar.gz", hash = "sha256:b3b56e00fb93ea321bc9e5d9cf6f8522a0198b20eb24e02774d329e9c6fb84ce"},
 ]
 
 [package.dependencies]
@@ -589,7 +606,9 @@ abfs = ["adlfs"]
 adl = ["adlfs"]
 arrow = ["pyarrow (>=1)"]
 dask = ["dask", "distributed"]
+devel = ["pytest", "pytest-cov"]
 dropbox = ["dropbox", "dropboxdrivefs", "requests"]
+full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"]
 fuse = ["fusepy"]
 gcs = ["gcsfs"]
 git = ["pygit2"]
@@ -610,7 +629,6 @@ tqdm = ["tqdm"]
 name = "gitdb"
 version = "4.0.10"
 description = "Git Object Database"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -625,7 +643,6 @@ smmap = ">=3.0.1,<6"
 name = "gitpython"
 version = "3.1.31"
 description = "GitPython is a Python library used to interact with Git repositories"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -640,7 +657,6 @@ gitdb = ">=4.0.1,<5"
 name = "gprof2dot"
 version = "2022.7.29"
 description = "Generate a dot graph from the output of several profilers."
-category = "dev"
 optional = false
 python-versions = ">=2.7"
 files = [
@@ -650,18 +666,18 @@ files = [
 
 [[package]]
 name = "huggingface-hub"
-version = "0.13.1"
+version = "0.14.1"
 description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "huggingface_hub-0.13.1-py3-none-any.whl", hash = "sha256:b9c2936f51842717ac5b95d014da7b506f344bcf1951b5fe2d79a2fbaed6b1cf"},
-    {file = "huggingface_hub-0.13.1.tar.gz", hash = "sha256:3bfd65465ff805079c39681eaf481882c37d8b80a04d41b3dd448e795bad8b2a"},
+    {file = "huggingface_hub-0.14.1-py3-none-any.whl", hash = "sha256:9fc619170d800ff3793ad37c9757c255c8783051e1b5b00501205eb43ccc4f27"},
+    {file = "huggingface_hub-0.14.1.tar.gz", hash = "sha256:9ab899af8e10922eac65e290d60ab956882ab0bf643e3d990b1394b6b47b7fbc"},
 ]
 
 [package.dependencies]
 filelock = "*"
+fsspec = "*"
 packaging = ">=20.9"
 pyyaml = ">=5.1"
 requests = "*"
@@ -669,13 +685,13 @@ tqdm = ">=4.42.1"
 typing-extensions = ">=3.7.4.3"
 
 [package.extras]
-all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (>=23.1,<24.0)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
+all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (>=23.1,<24.0)", "gradio", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
 cli = ["InquirerPy (==0.3.4)"]
-dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (>=23.1,<24.0)", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
+dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "black (>=23.1,<24.0)", "gradio", "jedi", "mypy (==0.982)", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "ruff (>=0.0.241)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
 fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
 quality = ["black (>=23.1,<24.0)", "mypy (==0.982)", "ruff (>=0.0.241)"]
 tensorflow = ["graphviz", "pydot", "tensorflow"]
-testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "jedi", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "soundfile"]
+testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "gradio", "jedi", "pytest", "pytest-cov", "pytest-env", "pytest-xdist", "soundfile"]
 torch = ["torch"]
 typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3"]
 
@@ -683,7 +699,6 @@ typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "t
 name = "hydra-core"
 version = "1.3.2"
 description = "A framework for elegantly configuring complex applications"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -692,7 +707,7 @@ files = [
 ]
 
 [package.dependencies]
-antlr4-python3-runtime = ">=4.9.0,<4.10.0"
+antlr4-python3-runtime = "==4.9.*"
 importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
 omegaconf = ">=2.2,<2.4"
 packaging = "*"
@@ -701,7 +716,6 @@ packaging = "*"
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -713,7 +727,6 @@ files = [
 name = "importlib-resources"
 version = "5.12.0"
 description = "Read resources from Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -732,7 +745,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
 name = "iniconfig"
 version = "2.0.0"
 description = "brain-dead simple config-ini parsing"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -744,7 +756,6 @@ files = [
 name = "ipdb"
 version = "0.13.13"
 description = "IPython-enabled pdb"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -753,20 +764,58 @@ files = [
 ]
 
 [package.dependencies]
-decorator = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
-ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
+decorator = {version = "*", markers = "python_version > \"3.6\""}
+ipython = {version = ">=7.31.1", markers = "python_version > \"3.6\""}
 tomli = {version = "*", markers = "python_version > \"3.6\" and python_version < \"3.11\""}
 
 [[package]]
 name = "ipython"
-version = "8.11.0"
+version = "8.12.2"
 description = "IPython: Productive Interactive Computing"
-category = "dev"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "ipython-8.11.0-py3-none-any.whl", hash = "sha256:5b54478e459155a326bf5f42ee4f29df76258c0279c36f21d71ddb560f88b156"},
-    {file = "ipython-8.11.0.tar.gz", hash = "sha256:735cede4099dbc903ee540307b9171fbfef4aa75cfcacc5a273b2cda2f02be04"},
+    {file = "ipython-8.12.2-py3-none-any.whl", hash = "sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc"},
+    {file = "ipython-8.12.2.tar.gz", hash = "sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
+pygments = ">=2.4.0"
+stack-data = "*"
+traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
+
+[package.extras]
+all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
+black = ["black"]
+doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
+test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
+
+[[package]]
+name = "ipython"
+version = "8.13.2"
+description = "IPython: Productive Interactive Computing"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "ipython-8.13.2-py3-none-any.whl", hash = "sha256:ffca270240fbd21b06b2974e14a86494d6d29290184e788275f55e0b55914926"},
+    {file = "ipython-8.13.2.tar.gz", hash = "sha256:7dff3fad32b97f6488e02f87b970f309d082f758d7b7fc252e3b19ee0e432dbb"},
 ]
 
 [package.dependencies]
@@ -782,6 +831,7 @@ prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
 pygments = ">=2.4.0"
 stack-data = "*"
 traitlets = ">=5"
+typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
 
 [package.extras]
 all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
@@ -800,7 +850,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa
 name = "jaconv"
 version = "0.3.4"
 description = "Pure-Python Japanese character interconverter for Hiragana, Katakana, Hankaku, Zenkaku and more"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -811,7 +860,6 @@ files = [
 name = "jedi"
 version = "0.18.2"
 description = "An autocompletion tool for Python that can be used for text editors."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -831,7 +879,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
 name = "jinf"
 version = "1.0.4"
 description = "A Japanese inflection converter."
-category = "main"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
@@ -842,11 +889,27 @@ files = [
 [package.extras]
 pyknp = ["pyknp[pyknp] (>=0.6,<1.0)"]
 
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+    {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
 [[package]]
 name = "joblib"
 version = "1.2.0"
 description = "Lightweight pipelining with Python functions"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -856,124 +919,122 @@ files = [
 
 [[package]]
 name = "levenshtein"
-version = "0.20.9"
+version = "0.21.0"
 description = "Python extension for computing string edit distances and similarities."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "Levenshtein-0.20.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:105c239ec786750cd5136991c58196b440cc39b6acf3ec8227f6562c9a94e4b9"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f7728bea7fe6dc55ceecde0dcda4287e74fe3b6733ad42530f46aaa8d2f81d0"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc7eca755c13c92814c8cce8175524cf764ce38f39228b602f59eac58cfdc51a"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8a552e79d053dc1324fb90d342447fd4e15736f4cbc5363b6fbd5577f53dce9"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5474b2681ee0b7944fb1e7fe281cd44e2dfe75b03ba4558dca49c96fa0861b62"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56e132c203b0dd8fc72a33e791c39ad0d5a25bcf24b130a1e202abbf489a3e75"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3badc94708ac05b405e795fde58a53272b90a9ee6099ecd54a345658b7b812e1"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48b9b3ae095b14dad7bc4bd219c7cd9113a7aa123a033337c85b00fe2ed565d3"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0d3a1f7328c91caeb1f857ddd2787e3f19d60cc2c688339d249ca8841da61454"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ef67c50428c99caf67d31bd209da21d9378da5f0cc3ad4f7bafb6caa78aee6f2"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:47f6d1592c0891f7355e38a302becd233336ca2f55f9a8be3a8635f946a6784f"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2891019740e874f05e0349e9f27b6af8ad837b1612f42e9c90c296d54d1404fd"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c554704eec4f4ba742febdcc79a85491f8f9a1d493cb103bb2af18536d6cf122"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-win32.whl", hash = "sha256:7628e356b3f9c78ad7272c3b9137f0641a1368849e749ff6f2c8fe372795806b"},
-    {file = "Levenshtein-0.20.9-cp310-cp310-win_amd64.whl", hash = "sha256:ba2bafe3511194a37044cae4e7d328cca70657933052691c37eba2ca428a379d"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7605a94145198d19fdaaa7e29c0f8a56ad719b12386f3ae8cd8ed4cb9fa6c2e4"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29db4dabfad2ddf33c7986eb6fd525c7587cca4c4d9e187365cff0a5281f5a35"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:965336c1772a4fc5fb2686a2a0bfaf3455dced96f19f50f278da8bc139076d31"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67235753035ac898d6475c0b29540521018db2e0027a3c1deb9aa0af0a84fd74"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:120dca58136aee3d8c7b190e30db7b6a6eb9579ea5712df84ad076a389801743"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6496ea66a6f755e48c0d82f1eee396d16edcd5592d4b3677d26fa789a636a728"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0af20327acc2c904d11611cb3a0d8d17f80c279a12e0b84189eafc35297186d"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d2f891ef53afbab6cf2eeb92ff13151884d17dc80a2d6d3c7ae74d7738b772"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2ab9c72380582bf4745d1c5b055b1df0c85f7a980a04bd7603a855dd91478c0f"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6de13be3eb5ac48053fb1635a7b4daa936b9114ad4b264942e9eb709fcaa41dd"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a9fc296860588251d8d72b4f4637cca4eef7351e042a7a23d44e6385aef1e160"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:35777b20fe35858248c22da37984469e6dd1278f55d17c53378312853d5d683d"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b9e0642ddb4c431f77c38cec9edbd0317e26c3f37d072ccf281ab58926dce69"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-win32.whl", hash = "sha256:f88ec322d86d3cc9d3936dbf6b421ad813950c2658599d48ac4ede59f2a6047e"},
-    {file = "Levenshtein-0.20.9-cp311-cp311-win_amd64.whl", hash = "sha256:2907a6888455f9915d5b656f5d058f63eaf6063b2c7f0f1ff6bc05706ae5bc39"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6bcebc79760be08488cb921732af34ade6abc7476a94866881c68b45ec4b6c82"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47d8d4f3825d1d8f3b19382537a8536e689cf57aaa224d2cb4f44cf844811885"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d40e18a5817ee7f0675401613a26c492fd4ea68d2103c1480fb5a6ab1b8763d"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d258f3d44f6bac17f33002fea34570049507d3476c3716b5267170c666b20b4"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c621e0c389546147ed43c33ca4168de0f91c920508ab8a94a400835fa084f486"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a31527dc7994353091626e62b7d82d53290cb00df48d3e5d29cb291fb4c03c"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:129c8f192e656b7c2c543bf0d704d677720771b8bc2f30c50db02fbc2001bac2"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5a01fca58255be6bf724a40af2575d7cf644c099c28a00d1f5f6a81675e60e7d"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:4c13749ea39a228f05d5bd9d473e76f726fc2dcd493cafc322f740921a6eeffb"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:69daa0f8eefa5b947255a81346741ed86fe7030e0909741dbd978e38b30da3fd"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fcc78a73ed423bbb09ac902dd2e1ff1094d159d1c6766e5e52da5f376a4cba18"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-win32.whl", hash = "sha256:d82ae57982a9f33c55778f1f0f63d5e51e291aee236abed3b90497578b944202"},
-    {file = "Levenshtein-0.20.9-cp36-cp36m-win_amd64.whl", hash = "sha256:4082379b406752fc1173ed1f8c3a122c5d5491e10e564ed721602e4e049e3d4c"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb499783b7126e6fc45c39ab34c8114148425c5d975b1ce35e6c47c0eda58a94"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ce747b296aad3bd8a563cccf2119cf37bf72f668076bfdad6ec55f0a0596dd9"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1347c3ebbe8f42f7a487e8d23a95bde6529379b4939ad51d32246d001565c499"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2f1c1e8360603a6da29416da61d1907a27656843e269413091c8c3a3e6286e"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73c1caaedbee3617fd29139aac8dab7743776b59c3c1fed2790308ecb43c7b25"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1f24133df69f8b618fc508d6023695130ad3c3c8968ef43aaeca21835eb337a"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cf7260722f8170c09af5cfa714bb45626a4dfc85d71d1c1c9c52c2a6901cc501"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:01668178fd9244df290db0340293982fe7641162a12a35ad9ffb3fe145ce6377"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e46f9d3483dc4991ac60ff3711b0d40f93e352cc8edc16b68df57ccc472bd6c"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:680cd250dc1875eb80cf2a0cca742bd13f6f9ab11c48317244fcc483eba1dd67"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2346e2f7dfbbc2936bd81e19f7734984e72486ffc086760c897b39b9f674b2fa"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-win32.whl", hash = "sha256:7f31bcf257fec9719d0d97185c419d315f6f20a194f0b442919e352d19418b2e"},
-    {file = "Levenshtein-0.20.9-cp37-cp37m-win_amd64.whl", hash = "sha256:48262bc9830ad60de96411fcb2e96a522c7206e7069169e04d89dd79364a7722"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eba5696e1f8e8da225498fd1d743886d639400cafd0e5be3c553978cbb54c345"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:679333188f9791c85109d2981e97e8721a99b2b975b5c52d16aca50ac9c70757"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06c9cfc61cf66833692d1ed258ec5a0871221b0779f1281c32a10348c492e2c5"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5d80d949168df406f2ac9ade1a5d0419cef0a8df611c8c2efe88f0248c9d0c0"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9275c6e601ff7f659116e2235e8585950c9c39d72504006077be85bf27950b35"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6414eea342d9632045e12b66bef043dbc6557189a283dc4dcc5966f63fa48998"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56571c58700600a382ecdf3f9efcb132ed16a0476cbb4e23a9478ab0ae788fd9"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7ccb76ffd9b851384f9cf1595b90b17cae46f0ab895e234de11ea48f9d9f73a"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109172943cff7fb10f28a9eb819eb3eaf9c88fe38661fb1d0f230a8ae68a615c"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:534c8bbdfd033fa20575d57332d9ac0447b5afbeca7db975ba169762ece2051f"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:381a725963c392585135654caa3c7fc32cb1755ed977fb9db72e8838fee261be"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7e4a44b1223980a9880e6f2bbf19121a125928580df9e4e81207199190343e11"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc0ced58ee6d07351cde140a7ec88e5f2ceb053c805af1f90514d21914d21cad"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-win32.whl", hash = "sha256:5eec0868ffcd825564dd5e3399305eaa159220554d1aedbff13af0de1fe01f6c"},
-    {file = "Levenshtein-0.20.9-cp38-cp38-win_amd64.whl", hash = "sha256:e9db476e40a3aa184631d102b716a019f70837eb0fcdd5b5d1504f099f91359c"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d5a20ecc20a09a32c72128c43d7df23877a2469b3c17780ae83f9a9d55873c08"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b7b772f2f62a19a15ccb1b09c6c7754ca7430bb7e19d4ca4ff232958786873b"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af92326b90ea6fe4521cf6a5dfe450e21150393c573ef3ad9ee446f1009fbfbd"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b48554dad328e198a636f937e2f4c057aac8e4bfcb8467b10e0f5daa94307b17"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82304821e128d5453d1755d1c2f3d9cdf75e9def3517cf913b09df174e20283b"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2052357c5da195ede7dbc81a4e3408ebd6374a1ff1b86a0a9d8b8ce9562b32c3"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d60c6b47ccd6841c990418f7f4f58c28f7da9b07b81eaafc99b836cf351df1"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dc2194c917e4466cb604580b16e42286f04e3fe0424489459e68f0834f5c527"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb1e20965d759d89318cac7ff7eb045eb1fafcb5c3fa3047a23f6ae20c810ad7"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:74e959035da10a54e7a2eee28408eff672297ce96cdadd6f4a2f269a06e395c4"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4a441b23d9704f57eb34af6a300ae5c335b9e77e6a065ada36ca69d6fc582af9"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f59470c49114a5da064712a427317f2b1fa5bb89aa2dfd0e300f8289e26aec28"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:06191f5d0527e3224107aea260b5cffc8a78722e0efb4e793f0e45c449b813a2"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-win32.whl", hash = "sha256:3235c461904fe94b4f62fee78a1658c1316344411c81b02400c27d692a893f8f"},
-    {file = "Levenshtein-0.20.9-cp39-cp39-win_amd64.whl", hash = "sha256:8b852def43d165c2f2b468239d66b847d9e6f52a775fc657773ced04d26062bd"},
-    {file = "Levenshtein-0.20.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f674cc75f127692525563155e500a3fa16aaf24dafd33a9bcda46e2979f793a1"},
-    {file = "Levenshtein-0.20.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a34e3fd21acb31fcd29a0c8353dca74dfbb59957210a6f142505907a9dff3d59"},
-    {file = "Levenshtein-0.20.9-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0ddddf2beafd1a2e17a87f80be562a7f7478e6098ccfc15de4c879972dfa2f9"},
-    {file = "Levenshtein-0.20.9-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9649af1a896a4a7fc7f6f1fd093e8a92f463297f56c7bd0f8d7d16dfabeb236d"},
-    {file = "Levenshtein-0.20.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d7bd7f25336849027fbe5ed32b6ffd404436727d78a014e348dcd17347c73fd8"},
-    {file = "Levenshtein-0.20.9-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0371d996ae81089296f42b6e886c7bf138d1cb0f002b0c724a9e5d689b29b5a0"},
-    {file = "Levenshtein-0.20.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e00e2fda9f225b5f4537647f6195cf220d468532739d3390eaf082b1d76c87"},
-    {file = "Levenshtein-0.20.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1600f5ebe2f2aebf13e88cf488ec2e5ce25f7a42b5846335018693baf4ea63bd"},
-    {file = "Levenshtein-0.20.9-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bcd59fcf06aaedda98da185ec289dc2c2c9922ce789f6a9c101709d4a22cac9"},
-    {file = "Levenshtein-0.20.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1549e307028fa5c3a8cf28ae8bcb1f6072df2abf7f36b9d7adf7fd60690fe372"},
-    {file = "Levenshtein-0.20.9-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:795f2e95d09a33c66c73cd49be3ee632fb4b8c41be72c0cb8df29a329ce7d111"},
-    {file = "Levenshtein-0.20.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:726bfb361d3b6786bea31392752f0ffcca568db7dc3f1e274f1b529489b8ad05"},
-    {file = "Levenshtein-0.20.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0fd315132786375de532355fa06b2f11c4b4af5784b7e064dc54b6ee0c3281"},
-    {file = "Levenshtein-0.20.9-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0674bc0549d5ea9edb934b3b03a160a116cc410feb5739a51f9c4f618ee674e3"},
-    {file = "Levenshtein-0.20.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1ef8f3ecdfca5d6f0538226338d58617270439a1cc9b6cacb30a388984bb1608"},
-    {file = "Levenshtein-0.20.9.tar.gz", hash = "sha256:70a8ad5e28bb76d87da1eb3f31de940836596547d6d01317c2289f5b7cd0b0ea"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f19fe25ea0dd845d0f48505e8947f6080728e10b7642ba0dad34e9b48c81130"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d23c647b03acbb5783f9bdfd51cfa5365d51f7df9f4029717a35eff5cc32bbcc"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:668ea30b311944c643f866ce5e45edf346f05e920075c0056f2ba7f74dde6071"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f42b8dba2cce257cd34efd1ce9678d06f3248cb0bb2a92a5db8402e1e4a6f30"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c27a5178ce322b56527a451185b4224217aa81955d9b0dad6f5a8de81ffe80f"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92bf2370b01d7a4862abf411f8f60f39f064cebebce176e3e9ee14e744db8288"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32dfda2e64d0c50553e47d0ab2956413970f940253351c196827ad46f17916d5"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f55623094b665d79a3b82ba77386ac34fa85049163edfe65387063e5127d4184"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25576ad9c337ecb342306fe87166b54b2f49e713d4ff592c752cc98e0046296e"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fae24c875c4ecc8c5f34a9715eb2a459743b4ca21d35c51819b640ee2f71cb51"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:4b2156f32e46d16b74a055ccb4f64ee3c64399372a6aaf1ee98f6dccfadecee1"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:04046878a57129da4e2352c032df7c1fceaa54870916d12772cad505ef998290"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:608beb1683508c3cdbfff669c1c872ea02b47965e1bbb8a630de548e2490f96a"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-win32.whl", hash = "sha256:cc36ba40027b4f8821155c9e3e0afadffccdccbe955556039d1d1169dfc659c9"},
+    {file = "Levenshtein-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:80e67bd73a05592ecd52aede4afa8ea49575de70f9d5bfbe2c52ebd3541b20be"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3305262cb85ff78ace9e2d8d2dfc029b34dc5f93aa2d24fd20b6ed723e2ad501"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:023ca95c833ca548280e444e9a4c34fdecb3be3851e96af95bad290ae0c708b9"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8476862a5c3150b8d63a7475563a4bff6dc50bbc0447894eb6b6a116ced0809d"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0236c8ff4648c50ebd81ac3692430d2241b134936ac9d86d7ca32ba6ab4a4e63"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cfbc4ed7ee2965e305bf81388fea377b795dabc82ee07f04f31d1fb8677a885"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6338a47b6f8c7f1ee8b5636cc8b245ad2d1d0ee47f7bb6f33f38a522ef0219cc"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dc79033140f82acaca40712a6d26ed190cc2dd403e104020a87c24f2771aa72"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88ccdc8dc20c16e8059ace00fb58d353346a04fd24c0733b009678b2554801d2"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c031cbe3685b0343f5cc2dcf2172fd21b82f8ccc5c487179a895009bf0e4ea8"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eab6c253983a6659e749f4c44fcc2215194c2e00bf7b1c5e90fe683ea3b7b00f"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8bdbcd1570340b07549f71e8a5ba3f0a6d84408bf86c4051dc7b70a29ae342bb"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4357bf8146cbadb10016ad3a950bba16e042f79015362a575f966181d95b4bc7"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be038321695267a8faa5ae1b1a83deb3748827f0b6f72471e0beed36afcbd72a"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-win32.whl", hash = "sha256:be87998ffcbb5fb0c37a76d100f63b4811f48527192677da0ec3624b49ab8a64"},
+    {file = "Levenshtein-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:f873af54014cac12082c7f5ccec6bbbeb5b57f63466e7f9c61a34588621313fb"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4ec2ef9836a34a3bb009a81e5efe4d9d43515455fb5f182c5d2cf8ae61c79496"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e748c2349719cb1bc90f802d9d7f07310633dcf166d468a5bd821f78ed17698"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37a99d858fa1d88b1a917b4059a186becd728534e5e889d583086482356b7ca1"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742b785c93d16c63289902607219c200bd2b6077dafc788073c74337cae382fb"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cefd5a668f6d7af1279aca10104b43882fdd83f9bdc68933ba5429257a628abe"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e1723d515ab287b9b2c2e4a111894dc6b474f5d28826fff379647486cae98d2"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3e22d31375d5fea5797c9b7aa0f8cc36579c31dcf5754e9931ca86c27d9011f8"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:31cb59d86a5f99147cd4a67ebced8d6df574b5d763dcb63c033a642e29568746"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:76d5d34a8e21de8073c66ae801f053520f946d499fa533fbba654712775f8132"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:46dab8c6e8fae563ca77acfaeb3824c4dd4b599996328b8a081b06f16befa6a0"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ee62ec5882a857b252faffeb7867679f7e418052ca6bf7d6b56099f6498a2b0e"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-win32.whl", hash = "sha256:7e40a4bac848c9a8883225f926cfa7b2bc9f651e989a8b7006cdb596edc7ac9b"},
+    {file = "Levenshtein-0.21.0-cp36-cp36m-win_amd64.whl", hash = "sha256:709a727f58d31a5ee1e5e83b247972fe55ef0014f6222256c9692c5efa471785"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01dd427cf72b4978b09558e3d36e3f92c8eef467e3eb4653c3fdccd8d70aaa08"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff1255c499fcb41ba37a578ad8c1b8dab5c44f78941b8e1c1d7fab5b5e831bc"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aa92b05156dfa2e248c3743670d5deb41a45b5789416d5fa31be009f4f043ab"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d932cb21e40beb93cfc8973de7f25fbf25ba4a07d1dccac3b9ba977164cf9887"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d4ba0df46bb41d660d77e7cc6b4d38c8d5b6f977d51c48ed1217db6a8474cde"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c8eaaa6f0df2838437d1d8739629486b145f7a3405d3ef0874301a9f5bc7dcd"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ede583155f24c8b2456a7720fbbfa5d9c1154ae04b4da3cf63368e2406ea099"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a18c8e4d1aae3f9950797d049020c64a8a63cc8b4e43afcca91ec400bf6304c5"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf87a5e2962431d7260dd81dc1ca0697f61aad81036145d3666f4c0d514ce3a"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bd0bfa71b1441be359e99e77709885b79c22857bf9bb7f4e84c09e501f6c5fad"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e9a6251818b9eb6d519bffd7a0b745f3a99b3e99563a4c9d3cad26e34f6ac880"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-win32.whl", hash = "sha256:8dd8ef4239b24fb1c9f0b536e48e55194d5966d351d349af23e67c9eb3875c68"},
+    {file = "Levenshtein-0.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:26c6fb012538a245d78adea786d2cfe3c1506b835762c1c523a4ed6b9e08dc0b"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b0ba9723c7d67a61e160b3457259552f7d679d74aaa144b892eb68b7e2a5ebb6"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:426883be613d912495cf6ee2a776d2ab84aa6b3de5a8d82c43a994267ea6e0e3"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5369827ace536c6df04e0e670d782999bc17bf9eb111e77435fdcdaecb10c2a3"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebabcf982ae161534f8729d13fe05eebc977b497ac34936551f97cf8b07dd9e"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13e8a5b1b58de49befea555bb913dc394614f2d3553bc5b86bc672c69ef1a85a"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d647f1e0c30c7a73f70f4de7376ed7dafc2b856b67fe480d32a81af133edbaeb"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5378a8139ba61d7271c0f9350201259c11eb90bfed0ac45539c4aeaed3907230"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df9b0f8f511270ad259c7bfba22ab6d5a0c33d81cd594461668e67cd80dd9052"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9485f2a5c88113410153256657072bc93b81bf5c8690d47e4cc3df58135dbadb"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:aa39bb773915e4df330d311bb6c100a8613e265cc50d5b25b015c8db824e1c47"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:de2dfd6498454c7d89036d56a53c0a01fd9bcf1c2970253e469b5e8bb938b69f"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4515f9511cb91c66d254ee30154206aad76b57d8b25f64ba1402aad43efdb251"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f622f542bd065ffec7d26b26d44d0c9a25c9c1295fd8ba6e4d77778e2293a12c"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-win32.whl", hash = "sha256:ee757fd36bad66ad8b961958840894021ecaad22194f65219a666432739393ff"},
+    {file = "Levenshtein-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:457442911df185e28a32fd8b788b14ca22ab3a552256b556e7687173d5f18bc4"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b646ace5085a60d4f89b28c81301c9d9e8cd6a9bdda908181b2fa3dfac7fc10d"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cc3679978cd0250bf002963cf2e08855b93f70fa0fc9f74956115c343983fbb"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84b55b732e311629a8308ad2778a0f9824e29e3c35987eb35610fc52eb6d4634"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7adaabe07c5ceb6228332b9184f06eb9cda89c227d198a1b8a6f78c05b3c672"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac8b6266799645827980ab1af4e0bfae209c1f747a10bdf6e5da96a6ebe511a2"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c2d67220867d640e36931b3d63b8349369b485d52cf6f4a2635bec8da92d678"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb26e69fc6c12534fbaa1657efed3b6482f1a166ba8e31227fa6f6f062a59070"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a68b05614d25cc2a5fbcc4d2fd124be7668d075fd5ac3d82f292eec573157361"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b167b32b3e336c5ec5e0212f025587f9248344ae6e73ed668270eba5c6a506e5"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:04850a0719e503014acb3fee6d4ec7d7f170a2c7375ffbc5833c7256b7cd10ee"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ce7e76c6341abb498368d42b8081f2f45c245ac2a221af6a0394349d41302c08"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ec64b7b3fb95bc9c20c72548277794b81281a6ba9da85eda2c87324c218441ff"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24843f28cbbdcbcfc18b08e7d3409dbaad7896fb7113442592fa978590a7bbf0"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-win32.whl", hash = "sha256:c290a7211f1b4f87c300df4424cc46b7379cead3b6f37fa8d3e7e6c6212ccd39"},
+    {file = "Levenshtein-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:1fde464f937878e6f5c30c234b95ce2cb969331a175b3089367e077113428062"},
+    {file = "Levenshtein-0.21.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:66d303cd485710fe6d62108209219b7a695bdd10a722f4e86abdaf26f4bf2202"},
+    {file = "Levenshtein-0.21.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bc550d0986ace95bde003b8a60e622449baf2bdf24d8412f7a50f401a289ec3"},
+    {file = "Levenshtein-0.21.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6858cfd84568bc1df3ad545553b5c27af6ed3346973e8f4b57d23c318cf8f4"},
+    {file = "Levenshtein-0.21.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ce3f14a8e006fb7e3fc7bab965ab7da5817f48fc48d25cf735fcec8f1d2e39a"},
+    {file = "Levenshtein-0.21.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:024302c82d49fc1f1d044794997ef7aa9d01b509a9040e222480b64a01cd4b80"},
+    {file = "Levenshtein-0.21.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e043b79e39f165026bc941c95582bfc4bfdd297a1de6f13ace0d0a7abf486288"},
+    {file = "Levenshtein-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8446f8da38857482ec0cfd616fe5e7dcd3695fd323cc65f37366a9ff6a31c9cb"},
+    {file = "Levenshtein-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:587ad51770de41eb491bea1bfb676abc7ff9a94dbec0e2bc51fc6a25abef99c4"},
+    {file = "Levenshtein-0.21.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac4ed77d3263eac7f9b6ed89d451644332aecd55cda921201e348803a1e5c57"},
+    {file = "Levenshtein-0.21.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cf2dee0f8c71598f8be51e3feceb9142ac01576277b9e691e25740987761c86e"},
+    {file = "Levenshtein-0.21.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4bbceef2caba4b2ae613b0e853a7aaab990c1a13bddb9054ba1328a84bccdbf7"},
+    {file = "Levenshtein-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2290732763e3b75979888364b26acce79d72b8677441b5762a4e97b3630cc3d9"},
+    {file = "Levenshtein-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db7567997ffbc2feb999e30002a92461a76f17a596a142bdb463b5f7037f160c"},
+    {file = "Levenshtein-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c270487d60b33102efea73be6dcd5835f3ddc3dc06e77499f0963df6cba2ec71"},
+    {file = "Levenshtein-0.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e2686c37d22faf27d02a19e83b55812d248b32b7ba3aa638e768d0ea032e1f3c"},
+    {file = "Levenshtein-0.21.0.tar.gz", hash = "sha256:545635d9e857711d049dcdb0b8609fb707b34b032517376c531ca159fcd46265"},
 ]
 
 [package.dependencies]
-rapidfuzz = ">=2.3.0,<3.0.0"
+rapidfuzz = ">=2.3.0,<4.0.0"
 
 [[package]]
 name = "lightning-utilities"
 version = "0.8.0"
 description = "PyTorch Lightning Sample project."
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -991,11 +1052,20 @@ docs = ["sphinx (>=4.0,<5.0)"]
 test = ["coverage (==6.5.0)"]
 typing = ["mypy (>=1.0.0)"]
 
+[[package]]
+name = "lit"
+version = "16.0.5"
+description = "A Software Testing Tool"
+optional = false
+python-versions = "*"
+files = [
+    {file = "lit-16.0.5.tar.gz", hash = "sha256:40b0224271f7832991874a6285e13da343963110f0d7c9c6e1e53f3ace901dc0"},
+]
+
 [[package]]
 name = "markdown-it-py"
 version = "2.2.0"
 description = "Python port of markdown-it. Markdown parsing, done right!"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1016,11 +1086,69 @@ profiling = ["gprof2dot"]
 rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
 testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
+[[package]]
+name = "markupsafe"
+version = "2.1.2"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
+]
+
 [[package]]
 name = "matplotlib-inline"
 version = "0.1.6"
 description = "Inline Matplotlib backend for Jupyter"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1035,7 +1163,6 @@ traitlets = "*"
 name = "mdurl"
 version = "0.1.2"
 description = "Markdown URL utilities"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1043,11 +1170,27 @@ files = [
     {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
 ]
 
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+description = "Python library for arbitrary-precision floating-point arithmetic"
+optional = false
+python-versions = "*"
+files = [
+    {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"},
+    {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"},
+]
+
+[package.extras]
+develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"]
+docs = ["sphinx"]
+gmpy = ["gmpy2 (>=2.1.0a4)"]
+tests = ["pytest (>=4.6)"]
+
 [[package]]
 name = "multidict"
 version = "6.0.4"
 description = "multidict implementation"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1127,49 +1270,65 @@ files = [
     {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"},
 ]
 
+[[package]]
+name = "networkx"
+version = "3.1"
+description = "Python package for creating and manipulating graphs and networks"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"},
+    {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"},
+]
+
+[package.extras]
+default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"]
+developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"]
+doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"]
+extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"]
+test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"]
+
 [[package]]
 name = "numpy"
-version = "1.24.2"
+version = "1.24.3"
 description = "Fundamental package for array computing in Python"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"},
-    {file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"},
-    {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"},
-    {file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"},
-    {file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"},
-    {file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"},
-    {file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"},
-    {file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"},
-    {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"},
-    {file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"},
-    {file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"},
-    {file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"},
-    {file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"},
-    {file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"},
-    {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"},
-    {file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"},
-    {file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"},
-    {file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"},
-    {file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"},
-    {file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"},
-    {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"},
-    {file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"},
-    {file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"},
-    {file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"},
-    {file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"},
-    {file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"},
-    {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
-    {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
+    {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"},
+    {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"},
+    {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"},
+    {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"},
+    {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"},
+    {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"},
+    {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"},
+    {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"},
+    {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"},
+    {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"},
+    {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"},
+    {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"},
+    {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"},
+    {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"},
+    {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"},
+    {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"},
+    {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"},
+    {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"},
+    {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"},
+    {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"},
+    {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"},
+    {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"},
+    {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"},
+    {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"},
+    {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"},
+    {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"},
+    {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"},
+    {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"},
 ]
 
 [[package]]
 name = "nvidia-cublas-cu11"
 version = "11.10.3.66"
 description = "CUBLAS native runtime libraries"
-category = "main"
 optional = false
 python-versions = ">=3"
 files = [
@@ -1181,11 +1340,25 @@ files = [
 setuptools = "*"
 wheel = "*"
 
+[[package]]
+name = "nvidia-cuda-cupti-cu11"
+version = "11.7.101"
+description = "CUDA profiling tools runtime libs."
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_cuda_cupti_cu11-11.7.101-py3-none-manylinux1_x86_64.whl", hash = "sha256:e0cfd9854e1f2edaa36ca20d21cd0bdd5dcfca4e3b9e130a082e05b33b6c5895"},
+    {file = "nvidia_cuda_cupti_cu11-11.7.101-py3-none-win_amd64.whl", hash = "sha256:7cc5b8f91ae5e1389c3c0ad8866b3b016a175e827ea8f162a672990a402ab2b0"},
+]
+
+[package.dependencies]
+setuptools = "*"
+wheel = "*"
+
 [[package]]
 name = "nvidia-cuda-nvrtc-cu11"
 version = "11.7.99"
 description = "NVRTC native runtime libraries"
-category = "main"
 optional = false
 python-versions = ">=3"
 files = [
@@ -1202,7 +1375,6 @@ wheel = "*"
 name = "nvidia-cuda-runtime-cu11"
 version = "11.7.99"
 description = "CUDA Runtime native Libraries"
-category = "main"
 optional = false
 python-versions = ">=3"
 files = [
@@ -1218,7 +1390,6 @@ wheel = "*"
 name = "nvidia-cudnn-cu11"
 version = "8.5.0.96"
 description = "cuDNN runtime libraries"
-category = "main"
 optional = false
 python-versions = ">=3"
 files = [
@@ -1230,11 +1401,92 @@ files = [
 setuptools = "*"
 wheel = "*"
 
+[[package]]
+name = "nvidia-cufft-cu11"
+version = "10.9.0.58"
+description = "CUFFT native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_cufft_cu11-10.9.0.58-py3-none-manylinux1_x86_64.whl", hash = "sha256:222f9da70c80384632fd6035e4c3f16762d64ea7a843829cb278f98b3cb7dd81"},
+    {file = "nvidia_cufft_cu11-10.9.0.58-py3-none-win_amd64.whl", hash = "sha256:c4d316f17c745ec9c728e30409612eaf77a8404c3733cdf6c9c1569634d1ca03"},
+]
+
+[[package]]
+name = "nvidia-curand-cu11"
+version = "10.2.10.91"
+description = "CURAND native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_curand_cu11-10.2.10.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:eecb269c970fa599a2660c9232fa46aaccbf90d9170b96c462e13bcb4d129e2c"},
+    {file = "nvidia_curand_cu11-10.2.10.91-py3-none-win_amd64.whl", hash = "sha256:f742052af0e1e75523bde18895a9ed016ecf1e5aa0ecddfcc3658fd11a1ff417"},
+]
+
+[package.dependencies]
+setuptools = "*"
+wheel = "*"
+
+[[package]]
+name = "nvidia-cusolver-cu11"
+version = "11.4.0.1"
+description = "CUDA solver native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_cusolver_cu11-11.4.0.1-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:72fa7261d755ed55c0074960df5904b65e2326f7adce364cbe4945063c1be412"},
+    {file = "nvidia_cusolver_cu11-11.4.0.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:700b781bfefd57d161443aff9ace1878584b93e0b2cfef3d6e9296d96febbf99"},
+    {file = "nvidia_cusolver_cu11-11.4.0.1-py3-none-win_amd64.whl", hash = "sha256:00f70b256add65f8c1eb3b6a65308795a93e7740f6df9e273eccbba770d370c4"},
+]
+
+[package.dependencies]
+setuptools = "*"
+wheel = "*"
+
+[[package]]
+name = "nvidia-cusparse-cu11"
+version = "11.7.4.91"
+description = "CUSPARSE native runtime libraries"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_cusparse_cu11-11.7.4.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:a3389de714db63321aa11fbec3919271f415ef19fda58aed7f2ede488c32733d"},
+    {file = "nvidia_cusparse_cu11-11.7.4.91-py3-none-win_amd64.whl", hash = "sha256:304a01599534f5186a8ed1c3756879282c72c118bc77dd890dc1ff868cad25b9"},
+]
+
+[package.dependencies]
+setuptools = "*"
+wheel = "*"
+
+[[package]]
+name = "nvidia-nccl-cu11"
+version = "2.14.3"
+description = "NVIDIA Collective Communication Library (NCCL) Runtime"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_nccl_cu11-2.14.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:5e5534257d1284b8e825bc3a182c6f06acd6eb405e9f89d49340e98cd8f136eb"},
+]
+
+[[package]]
+name = "nvidia-nvtx-cu11"
+version = "11.7.91"
+description = "NVIDIA Tools Extension"
+optional = false
+python-versions = ">=3"
+files = [
+    {file = "nvidia_nvtx_cu11-11.7.91-py3-none-manylinux1_x86_64.whl", hash = "sha256:b22c64eee426a62fc00952b507d6d29cf62b4c9df7a480fcc417e540e05fd5ac"},
+    {file = "nvidia_nvtx_cu11-11.7.91-py3-none-win_amd64.whl", hash = "sha256:dfd7fcb2a91742513027d63a26b757f38dd8b07fecac282c4d132a9d373ff064"},
+]
+
+[package.dependencies]
+setuptools = "*"
+wheel = "*"
+
 [[package]]
 name = "omegaconf"
 version = "2.3.0"
 description = "A flexible configuration library"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1243,74 +1495,91 @@ files = [
 ]
 
 [package.dependencies]
-antlr4-python3-runtime = ">=4.9.0,<4.10.0"
+antlr4-python3-runtime = "==4.9.*"
 PyYAML = ">=5.1.0"
 
 [[package]]
 name = "packaging"
-version = "23.0"
+version = "23.1"
 description = "Core utilities for Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
-    {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
+    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
 ]
 
 [[package]]
 name = "pandas"
-version = "1.5.3"
+version = "2.0.2"
 description = "Powerful data structures for data analysis, time series, and statistics"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3749077d86e3a2f0ed51367f30bf5b82e131cc0f14260c4d3e499186fccc4406"},
-    {file = "pandas-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:972d8a45395f2a2d26733eb8d0f629b2f90bebe8e8eddbb8829b180c09639572"},
-    {file = "pandas-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50869a35cbb0f2e0cd5ec04b191e7b12ed688874bd05dd777c19b28cbea90996"},
-    {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ac844a0fe00bfaeb2c9b51ab1424e5c8744f89860b138434a363b1f620f354"},
-    {file = "pandas-1.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0a56cef15fd1586726dace5616db75ebcfec9179a3a55e78f72c5639fa2a23"},
-    {file = "pandas-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:478ff646ca42b20376e4ed3fa2e8d7341e8a63105586efe54fa2508ee087f328"},
-    {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6973549c01ca91ec96199e940495219c887ea815b2083722821f1d7abfa2b4dc"},
-    {file = "pandas-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c39a8da13cede5adcd3be1182883aea1c925476f4e84b2807a46e2775306305d"},
-    {file = "pandas-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f76d097d12c82a535fda9dfe5e8dd4127952b45fea9b0276cb30cca5ea313fbc"},
-    {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e474390e60ed609cec869b0da796ad94f420bb057d86784191eefc62b65819ae"},
-    {file = "pandas-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f2b952406a1588ad4cad5b3f55f520e82e902388a6d5a4a91baa8d38d23c7f6"},
-    {file = "pandas-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc4c368f42b551bf72fac35c5128963a171b40dce866fb066540eeaf46faa003"},
-    {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14e45300521902689a81f3f41386dc86f19b8ba8dd5ac5a3c7010ef8d2932813"},
-    {file = "pandas-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9842b6f4b8479e41968eced654487258ed81df7d1c9b7b870ceea24ed9459b31"},
-    {file = "pandas-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:26d9c71772c7afb9d5046e6e9cf42d83dd147b5cf5bcb9d97252077118543792"},
-    {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fbcb19d6fceb9e946b3e23258757c7b225ba450990d9ed63ccceeb8cae609f7"},
-    {file = "pandas-1.5.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:565fa34a5434d38e9d250af3c12ff931abaf88050551d9fbcdfafca50d62babf"},
-    {file = "pandas-1.5.3-cp38-cp38-win32.whl", hash = "sha256:87bd9c03da1ac870a6d2c8902a0e1fd4267ca00f13bc494c9e5a9020920e1d51"},
-    {file = "pandas-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:41179ce559943d83a9b4bbacb736b04c928b095b5f25dd2b7389eda08f46f373"},
-    {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c74a62747864ed568f5a82a49a23a8d7fe171d0c69038b38cedf0976831296fa"},
-    {file = "pandas-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c00e0b0597c8e4f59e8d461f797e5d70b4d025880516a8261b2817c47759ee"},
-    {file = "pandas-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a50d9a4336a9621cab7b8eb3fb11adb82de58f9b91d84c2cd526576b881a0c5a"},
-    {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd05f7783b3274aa206a1af06f0ceed3f9b412cf665b7247eacd83be41cf7bf0"},
-    {file = "pandas-1.5.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f69c4029613de47816b1bb30ff5ac778686688751a5e9c99ad8c7031f6508e5"},
-    {file = "pandas-1.5.3-cp39-cp39-win32.whl", hash = "sha256:7cec0bee9f294e5de5bbfc14d0573f65526071029d036b753ee6507d2a21480a"},
-    {file = "pandas-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:dfd681c5dc216037e0b0a2c821f5ed99ba9f03ebcf119c7dac0e9a7b960b9ec9"},
-    {file = "pandas-1.5.3.tar.gz", hash = "sha256:74a3fd7e5a7ec052f183273dc7b0acd3a863edf7520f5d3a1765c04ffdb3b0b1"},
+    {file = "pandas-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ebb9f1c22ddb828e7fd017ea265a59d80461d5a79154b49a4207bd17514d122"},
+    {file = "pandas-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eb09a242184092f424b2edd06eb2b99d06dc07eeddff9929e8667d4ed44e181"},
+    {file = "pandas-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7319b6e68de14e6209460f72a8d1ef13c09fb3d3ef6c37c1e65b35d50b5c145"},
+    {file = "pandas-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd46bde7309088481b1cf9c58e3f0e204b9ff9e3244f441accd220dd3365ce7c"},
+    {file = "pandas-2.0.2-cp310-cp310-win32.whl", hash = "sha256:51a93d422fbb1bd04b67639ba4b5368dffc26923f3ea32a275d2cc450f1d1c86"},
+    {file = "pandas-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:66d00300f188fa5de73f92d5725ced162488f6dc6ad4cecfe4144ca29debe3b8"},
+    {file = "pandas-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02755de164da6827764ceb3bbc5f64b35cb12394b1024fdf88704d0fa06e0e2f"},
+    {file = "pandas-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0a1e0576611641acde15c2322228d138258f236d14b749ad9af498ab69089e2d"},
+    {file = "pandas-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6b5f14cd24a2ed06e14255ff40fe2ea0cfaef79a8dd68069b7ace74bd6acbba"},
+    {file = "pandas-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50e451932b3011b61d2961b4185382c92cc8c6ee4658dcd4f320687bb2d000ee"},
+    {file = "pandas-2.0.2-cp311-cp311-win32.whl", hash = "sha256:7b21cb72958fc49ad757685db1919021d99650d7aaba676576c9e88d3889d456"},
+    {file = "pandas-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:c4af689352c4fe3d75b2834933ee9d0ccdbf5d7a8a7264f0ce9524e877820c08"},
+    {file = "pandas-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69167693cb8f9b3fc060956a5d0a0a8dbfed5f980d9fd2c306fb5b9c855c814c"},
+    {file = "pandas-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30a89d0fec4263ccbf96f68592fd668939481854d2ff9da709d32a047689393b"},
+    {file = "pandas-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a18e5c72b989ff0f7197707ceddc99828320d0ca22ab50dd1b9e37db45b010c0"},
+    {file = "pandas-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7376e13d28eb16752c398ca1d36ccfe52bf7e887067af9a0474de6331dd948d2"},
+    {file = "pandas-2.0.2-cp38-cp38-win32.whl", hash = "sha256:6d6d10c2142d11d40d6e6c0a190b1f89f525bcf85564707e31b0a39e3b398e08"},
+    {file = "pandas-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:e69140bc2d29a8556f55445c15f5794490852af3de0f609a24003ef174528b79"},
+    {file = "pandas-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b42b120458636a981077cfcfa8568c031b3e8709701315e2bfa866324a83efa8"},
+    {file = "pandas-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f908a77cbeef9bbd646bd4b81214cbef9ac3dda4181d5092a4aa9797d1bc7774"},
+    {file = "pandas-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713f2f70abcdade1ddd68fc91577cb090b3544b07ceba78a12f799355a13ee44"},
+    {file = "pandas-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf3f0c361a4270185baa89ec7ab92ecaa355fe783791457077473f974f654df5"},
+    {file = "pandas-2.0.2-cp39-cp39-win32.whl", hash = "sha256:598e9020d85a8cdbaa1815eb325a91cfff2bb2b23c1442549b8a3668e36f0f77"},
+    {file = "pandas-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:77550c8909ebc23e56a89f91b40ad01b50c42cfbfab49b3393694a50549295ea"},
+    {file = "pandas-2.0.2.tar.gz", hash = "sha256:dd5476b6c3fe410ee95926873f377b856dbc4e81a9c605a0dc05aaccc6a7c6c6"},
 ]
 
 [package.dependencies]
 numpy = [
     {version = ">=1.20.3", markers = "python_version < \"3.10\""},
     {version = ">=1.21.0", markers = "python_version >= \"3.10\""},
+    {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
 ]
-python-dateutil = ">=2.8.1"
+python-dateutil = ">=2.8.2"
 pytz = ">=2020.1"
+tzdata = ">=2022.1"
 
 [package.extras]
-test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
+all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"]
+aws = ["s3fs (>=2021.08.0)"]
+clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"]
+compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"]
+computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"]
+feather = ["pyarrow (>=7.0.0)"]
+fss = ["fsspec (>=2021.07.0)"]
+gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"]
+hdf5 = ["tables (>=3.6.1)"]
+html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"]
+mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"]
+parquet = ["pyarrow (>=7.0.0)"]
+performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"]
+plot = ["matplotlib (>=3.6.1)"]
+postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"]
+spss = ["pyreadstat (>=1.1.2)"]
+sql-other = ["SQLAlchemy (>=1.4.16)"]
+test = ["hypothesis (>=6.34.2)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.6.3)"]
 
 [[package]]
 name = "parso"
 version = "0.8.3"
 description = "A Python Parser"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1326,7 +1595,6 @@ testing = ["docopt", "pytest (<6.0.0)"]
 name = "pathtools"
 version = "0.1.2"
 description = "File system general utilities"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1337,7 +1605,6 @@ files = [
 name = "pexpect"
 version = "4.8.0"
 description = "Pexpect allows easy control of interactive console applications."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1352,7 +1619,6 @@ ptyprocess = ">=0.5"
 name = "pickleshare"
 version = "0.7.5"
 description = "Tiny 'shelve'-like database with concurrency support"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1364,7 +1630,6 @@ files = [
 name = "pluggy"
 version = "1.0.0"
 description = "plugin and hook calling mechanisms for python"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1380,7 +1645,6 @@ testing = ["pytest", "pytest-benchmark"]
 name = "prompt-toolkit"
 version = "3.0.38"
 description = "Library for building powerful interactive command lines in Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7.0"
 files = [
@@ -1395,7 +1659,6 @@ wcwidth = "*"
 name = "protobuf"
 version = "3.20.2"
 description = "Protocol Buffers"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1425,26 +1688,25 @@ files = [
 
 [[package]]
 name = "psutil"
-version = "5.9.4"
+version = "5.9.5"
 description = "Cross-platform lib for process and system monitoring in Python."
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
-    {file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"},
-    {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"},
-    {file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"},
-    {file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"},
-    {file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"},
-    {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"},
-    {file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"},
-    {file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"},
-    {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"},
-    {file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"},
-    {file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"},
-    {file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"},
-    {file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"},
-    {file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"},
+    {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"},
+    {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"},
+    {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"},
+    {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"},
+    {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"},
+    {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"},
+    {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"},
+    {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"},
+    {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"},
+    {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"},
+    {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"},
+    {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"},
+    {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"},
+    {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"},
 ]
 
 [package.extras]
@@ -1454,7 +1716,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
 name = "ptyprocess"
 version = "0.7.0"
 description = "Run a subprocess in a pseudo terminal"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1466,7 +1727,6 @@ files = [
 name = "pure-cdb"
 version = "4.0.0"
 description = "Pure Python reader/writer for Dan J. Berstein's CDB format."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1483,7 +1743,6 @@ files = [
 name = "pure-eval"
 version = "0.2.2"
 description = "Safely evaluate AST nodes without side effects"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1496,14 +1755,13 @@ tests = ["pytest"]
 
 [[package]]
 name = "pygments"
-version = "2.14.0"
+version = "2.15.1"
 description = "Pygments is a syntax highlighting package written in Python."
-category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
-    {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
+    {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
+    {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
 ]
 
 [package.extras]
@@ -1511,18 +1769,16 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pytest"
-version = "7.2.2"
+version = "7.3.1"
 description = "pytest: simple powerful testing with Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"},
-    {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"},
+    {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
+    {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
 ]
 
 [package.dependencies]
-attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
 iniconfig = "*"
@@ -1531,18 +1787,17 @@ pluggy = ">=0.12,<2.0"
 tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
 
 [[package]]
 name = "pytest-cov"
-version = "4.0.0"
+version = "4.1.0"
 description = "Pytest plugin for measuring coverage."
-category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
-    {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
-    {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+    {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+    {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
 ]
 
 [package.dependencies]
@@ -1556,7 +1811,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
 name = "pytest-profiling"
 version = "1.7.0"
 description = "Profiling plugin for py.test"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1576,7 +1830,6 @@ tests = ["pytest-virtualenv"]
 name = "python-dateutil"
 version = "2.8.2"
 description = "Extensions to the standard Python datetime module"
-category = "main"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
 files = [
@@ -1591,7 +1844,6 @@ six = ">=1.5"
 name = "python-dotenv"
 version = "1.0.0"
 description = "Read key-value pairs from a .env file and set them as environment variables"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -1604,57 +1856,50 @@ cli = ["click (>=5.0)"]
 
 [[package]]
 name = "pytorch-lightning"
-version = "1.9.4"
+version = "2.0.2"
 description = "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate."
-category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pytorch-lightning-1.9.4.tar.gz", hash = "sha256:188a7f4468acf23512e7f4903253d86fc7929a49f0c09d699872e364162001e8"},
-    {file = "pytorch_lightning-1.9.4-py3-none-any.whl", hash = "sha256:a2d2bd7657716087c294b076fe385ed17879764d6daaad0a541394a8f7164f93"},
+    {file = "pytorch-lightning-2.0.2.tar.gz", hash = "sha256:b37a3677a65cda920260de3899ea9dfdd221b0a2d838a0439e31bd927efa61f4"},
+    {file = "pytorch_lightning-2.0.2-py3-none-any.whl", hash = "sha256:e65a99f0ae53d70c35ec821225241387ec8bb01920003ab8f2d49e753d8a84d0"},
 ]
 
 [package.dependencies]
 fsspec = {version = ">2021.06.0", extras = ["http"]}
-lightning-utilities = ">=0.6.0.post0"
+lightning-utilities = ">=0.7.0"
 numpy = ">=1.17.2"
 packaging = ">=17.1"
 PyYAML = ">=5.4"
-torch = ">=1.10.0"
+torch = ">=1.11.0"
 torchmetrics = ">=0.7.0"
 tqdm = ">=4.57.0"
 typing-extensions = ">=4.0.0"
 
 [package.extras]
-all = ["colossalai (>=0.2.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "gym[classic-control] (>=0.17.0)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=10.14.0,!=10.15.0.a)", "tensorboardX (>=2.2)", "torchvision (>=0.11.1)"]
-colossalai = ["colossalai (>=0.2.0)"]
+all = ["deepspeed (>=0.6.0)", "gym[classic-control] (>=0.17.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "lightning-utilities (>=0.7.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=12.3.0)", "tensorboardX (>=2.2)", "torchmetrics (>=0.10.0)", "torchvision (>=0.11.1)"]
 deepspeed = ["deepspeed (>=0.6.0)"]
-dev = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "colossalai (>=0.2.0)", "coverage (==6.5.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "fastapi (<0.87.0)", "gym[classic-control] (>=0.17.0)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "rich (>=10.14.0,!=10.15.0.a)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "tensorboardX (>=2.2)", "torchvision (>=0.11.1)", "uvicorn (<0.19.1)"]
-examples = ["gym[classic-control] (>=0.17.0)", "ipython[all] (<8.7.1)", "torchvision (>=0.11.1)"]
-extra = ["hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=10.14.0,!=10.15.0.a)", "tensorboardX (>=2.2)"]
-fairscale = ["fairscale (>=0.4.5)"]
-hivemind = ["hivemind (==1.1.5)"]
-horovod = ["horovod (>=0.21.2,!=0.24.0)"]
-strategies = ["colossalai (>=0.2.0)", "deepspeed (>=0.6.0)", "fairscale (>=0.4.5)", "hivemind (==1.1.5)", "horovod (>=0.21.2,!=0.24.0)"]
-test = ["cloudpickle (>=1.3)", "codecov (==2.1.12)", "coverage (==6.5.0)", "fastapi (<0.87.0)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "pre-commit (==2.20.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn (<0.19.1)"]
+dev = ["cloudpickle (>=1.3)", "coverage (==6.5.0)", "deepspeed (>=0.6.0)", "fastapi (<0.87.0)", "gym[classic-control] (>=0.17.0)", "hydra-core (>=1.0.5)", "ipython[all] (<8.7.1)", "jsonargparse[signatures] (>=4.18.0)", "lightning-utilities (>=0.7.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "onnx (<1.14.0)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "rich (>=12.3.0)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "tensorboardX (>=2.2)", "torchmetrics (>=0.10.0)", "torchvision (>=0.11.1)", "uvicorn (<0.19.1)"]
+examples = ["gym[classic-control] (>=0.17.0)", "ipython[all] (<8.7.1)", "lightning-utilities (>=0.7.0)", "torchmetrics (>=0.10.0)", "torchvision (>=0.11.1)"]
+extra = ["hydra-core (>=1.0.5)", "jsonargparse[signatures] (>=4.18.0)", "matplotlib (>3.1)", "omegaconf (>=2.0.5)", "rich (>=12.3.0)", "tensorboardX (>=2.2)"]
+strategies = ["deepspeed (>=0.6.0)"]
+test = ["cloudpickle (>=1.3)", "coverage (==6.5.0)", "fastapi (<0.87.0)", "onnx (<1.14.0)", "onnxruntime (<1.14.0)", "pandas (>1.0)", "protobuf (<=3.20.1)", "psutil (<5.9.5)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-forked (==1.4.0)", "pytest-rerunfailures (==10.3)", "scikit-learn (>0.22.1)", "tensorboard (>=2.9.1)", "uvicorn (<0.19.1)"]
 
 [[package]]
 name = "pytz"
-version = "2022.7.1"
+version = "2023.3"
 description = "World timezone definitions, modern and historical"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
-    {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
+    {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"},
+    {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"},
 ]
 
 [[package]]
 name = "pyyaml"
 version = "6.0"
 description = "YAML parser and emitter for Python"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1702,101 +1947,103 @@ files = [
 
 [[package]]
 name = "rapidfuzz"
-version = "2.13.7"
+version = "3.0.0"
 description = "rapid fuzzy string matching"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b75dd0928ce8e216f88660ab3d5c5ffe990f4dd682fd1709dba29d5dafdde6de"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24d3fea10680d085fd0a4d76e581bfb2b1074e66e78fd5964d4559e1fcd2a2d4"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8109e0324d21993d5b2d111742bf5958f3516bf8c59f297c5d1cc25a2342eb66"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f705652360d520c2de52bee11100c92f59b3e3daca308ebb150cbc58aecdad"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7496e8779905b02abc0ab4ba2a848e802ab99a6e20756ffc967a0de4900bd3da"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24eb6b843492bdc63c79ee4b2f104059b7a2201fef17f25177f585d3be03405a"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:467c1505362823a5af12b10234cb1c4771ccf124c00e3fc9a43696512bd52293"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53dcae85956853b787c27c1cb06f18bb450e22cf57a4ad3444cf03b8ff31724a"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46b9b8aa09998bc48dd800854e8d9b74bc534d7922c1d6e1bbf783e7fa6ac29c"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1fbad8fb28d98980f5bff33c7842efef0315d42f0cd59082108482a7e6b61410"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:43fb8cb030f888c3f076d40d428ed5eb4331f5dd6cf1796cfa39c67bf0f0fc1e"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b6bad92de071cbffa2acd4239c1779f66851b60ffbbda0e4f4e8a2e9b17e7eef"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d00df2e4a81ffa56a6b1ec4d2bc29afdcb7f565e0b8cd3092fece2290c4c7a79"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-win32.whl", hash = "sha256:2c836f0f2d33d4614c3fbaf9a1eb5407c0fe23f8876f47fd15b90f78daa64c34"},
-    {file = "rapidfuzz-2.13.7-cp310-cp310-win_amd64.whl", hash = "sha256:c36fd260084bb636b9400bb92016c6bd81fd80e59ed47f2466f85eda1fc9f782"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b34e8c0e492949ecdd5da46a1cfc856a342e2f0389b379b1a45a3cdcd3176a6e"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:875d51b3497439a72e2d76183e1cb5468f3f979ab2ddfc1d1f7dde3b1ecfb42f"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae33a72336059213996fe4baca4e0e4860913905c2efb7c991eab33b95a98a0a"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5585189b3d90d81ccd62d4f18530d5ac8972021f0aaaa1ffc6af387ff1dce75"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42085d4b154a8232767de8296ac39c8af5bccee6b823b0507de35f51c9cbc2d7"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:585206112c294e335d84de5d5f179c0f932837752d7420e3de21db7fdc476278"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f891b98f8bc6c9d521785816085e9657212621e93f223917fb8e32f318b2957e"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08590905a95ccfa43f4df353dcc5d28c15d70664299c64abcad8721d89adce4f"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5dd713a1734574c2850c566ac4286594bacbc2d60b9170b795bee4b68656625"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:988f8f6abfba7ee79449f8b50687c174733b079521c3cc121d65ad2d38831846"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b3210869161a864f3831635bb13d24f4708c0aa7208ef5baac1ac4d46e9b4208"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f6fe570e20e293eb50491ae14ddeef71a6a7e5f59d7e791393ffa99b13f1f8c2"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6120f2995f5154057454c5de99d86b4ef3b38397899b5da1265467e8980b2f60"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-win32.whl", hash = "sha256:b20141fa6cee041917801de0bab503447196d372d4c7ee9a03721b0a8edf5337"},
-    {file = "rapidfuzz-2.13.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec55a81ac2b0f41b8d6fb29aad16e55417036c7563bad5568686931aa4ff08f7"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7d005e058d86f2a968a8d28ca6f2052fab1f124a39035aa0523261d6baf21e1f"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe59a0c21a032024edb0c8e43f5dee5623fef0b65a1e3c1281836d9ce199af3b"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfc04f7647c29fb48da7a04082c34cdb16f878d3c6d098d62d5715c0ad3000c"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68a89bb06d5a331511961f4d3fa7606f8e21237467ba9997cae6f67a1c2c2b9e"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:effe182767d102cb65dfbbf74192237dbd22d4191928d59415aa7d7c861d8c88"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25b4cedf2aa19fb7212894ce5f5219010cce611b60350e9a0a4d492122e7b351"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3a9bd02e1679c0fd2ecf69b72d0652dbe2a9844eaf04a36ddf4adfbd70010e95"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5e2b3d020219baa75f82a4e24b7c8adcb598c62f0e54e763c39361a9e5bad510"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:cf62dacb3f9234f3fddd74e178e6d25c68f2067fde765f1d95f87b1381248f58"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:fa263135b892686e11d5b84f6a1892523123a00b7e5882eff4fbdabb38667347"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa4c598ed77f74ec973247ca776341200b0f93ec3883e34c222907ce72cb92a4"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-win32.whl", hash = "sha256:c2523f8180ebd9796c18d809e9a19075a1060b1a170fde3799e83db940c1b6d5"},
-    {file = "rapidfuzz-2.13.7-cp37-cp37m-win_amd64.whl", hash = "sha256:5ada0a14c67452358c1ee52ad14b80517a87b944897aaec3e875279371a9cb96"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ca8a23097c1f50e0fdb4de9e427537ca122a18df2eead06ed39c3a0bef6d9d3a"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9be02162af0376d64b840f2fc8ee3366794fc149f1e06d095a6a1d42447d97c5"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af4f7c3c904ca709493eb66ca9080b44190c38e9ecb3b48b96d38825d5672559"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f50d1227e6e2a0e3ae1fb1c9a2e1c59577d3051af72c7cab2bcc430cb5e18da"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c71d9d512b76f05fa00282227c2ae884abb60e09f08b5ca3132b7e7431ac7f0d"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b52ac2626945cd21a2487aeefed794c14ee31514c8ae69b7599170418211e6f6"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca00fafd2756bc9649bf80f1cf72c647dce38635f0695d7ce804bc0f759aa756"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d248a109699ce9992304e79c1f8735c82cc4c1386cd8e27027329c0549f248a2"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c88adbcb933f6b8612f6c593384bf824e562bb35fc8a0f55fac690ab5b3486e5"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c8601a66fbfc0052bb7860d2eacd303fcde3c14e87fdde409eceff516d659e77"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:27be9c63215d302ede7d654142a2e21f0d34ea6acba512a4ae4cfd52bbaa5b59"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3dcffe1f3cbda0dc32133a2ae2255526561ca594f15f9644384549037b355245"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8450d15f7765482e86ef9be2ad1a05683cd826f59ad236ef7b9fb606464a56aa"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-win32.whl", hash = "sha256:460853983ab88f873173e27cc601c5276d469388e6ad6e08c4fd57b2a86f1064"},
-    {file = "rapidfuzz-2.13.7-cp38-cp38-win_amd64.whl", hash = "sha256:424f82c35dbe4f83bdc3b490d7d696a1dc6423b3d911460f5493b7ffae999fd2"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c3fbe449d869ea4d0909fc9d862007fb39a584fb0b73349a6aab336f0d90eaed"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:16080c05a63d6042643ae9b6cfec1aefd3e61cef53d0abe0df3069b9d4b72077"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dbcf5371ea704759fcce772c66a07647751d1f5dbdec7818331c9b31ae996c77"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114810491efb25464016fd554fdf1e20d390309cecef62587494fc474d4b926f"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99a84ab9ac9a823e7e93b4414f86344052a5f3e23b23aa365cda01393ad895bd"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81642a24798851b118f82884205fc1bd9ff70b655c04018c467824b6ecc1fabc"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3741cb0bf9794783028e8b0cf23dab917fa5e37a6093b94c4c2f805f8e36b9f"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:759a3361711586a29bc753d3d1bdb862983bd9b9f37fbd7f6216c24f7c972554"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1333fb3d603d6b1040e365dca4892ba72c7e896df77a54eae27dc07db90906e3"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:916bc2e6cf492c77ad6deb7bcd088f0ce9c607aaeabc543edeb703e1fbc43e31"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:23524635840500ce6f4d25005c9529a97621689c85d2f727c52eed1782839a6a"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ebe303cd9839af69dd1f7942acaa80b1ba90bacef2e7ded9347fbed4f1654672"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fe56659ccadbee97908132135de4b875543353351e0c92e736b7c57aee298b5a"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-win32.whl", hash = "sha256:3f11a7eff7bc6301cd6a5d43f309e22a815af07e1f08eeb2182892fca04c86cb"},
-    {file = "rapidfuzz-2.13.7-cp39-cp39-win_amd64.whl", hash = "sha256:e8914dad106dacb0775718e54bf15e528055c4e92fb2677842996f2d52da5069"},
-    {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f7930adf84301797c3f09c94b9c5a9ed90a9e8b8ed19b41d2384937e0f9f5bd"},
-    {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31022d9970177f6affc6d5dd757ed22e44a10890212032fabab903fdee3bfe7"},
-    {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f42b82f268689f429def9ecfb86fa65ceea0eaf3fed408b570fe113311bf5ce7"},
-    {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b477b43ced896301665183a5e0faec0f5aea2373005648da8bdcb3c4b73f280"},
-    {file = "rapidfuzz-2.13.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d63def9bbc6b35aef4d76dc740301a4185867e8870cbb8719ec9de672212fca8"},
-    {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c66546e30addb04a16cd864f10f5821272a1bfe6462ee5605613b4f1cb6f7b48"},
-    {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f799d1d6c33d81e983d3682571cc7d993ae7ff772c19b3aabb767039c33f6d1e"},
-    {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82f20c0060ffdaadaf642b88ab0aa52365b56dffae812e188e5bdb998043588"},
-    {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042644133244bfa7b20de635d500eb9f46af7097f3d90b1724f94866f17cb55e"},
-    {file = "rapidfuzz-2.13.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75c45dcd595f8178412367e302fd022860ea025dc4a78b197b35428081ed33d5"},
-    {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3d8b081988d0a49c486e4e845a547565fee7c6e7ad8be57ff29c3d7c14c6894c"},
-    {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16ffad751f43ab61001187b3fb4a9447ec2d1aedeff7c5bac86d3b95f9980cc3"},
-    {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:020858dd89b60ce38811cd6e37875c4c3c8d7fcd8bc20a0ad2ed1f464b34dc4e"},
-    {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cda1e2f66bb4ba7261a0f4c2d052d5d909798fca557cbff68f8a79a87d66a18f"},
-    {file = "rapidfuzz-2.13.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b6389c50d8d214c9cd11a77f6d501529cb23279a9c9cafe519a3a4b503b5f72a"},
-    {file = "rapidfuzz-2.13.7.tar.gz", hash = "sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ab635a544243e1924508bfc3f294c28bdced6d74388ac25041d3dabcaefab75"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf8b1b29028dc1bc6a5654f22425ee6d3967bbd44bc3a117be0f43b03300f928"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bbf9aad86b70283362dc2db3cacb7dcde0ffe6027f54feb0ccb23cf87b6aa11"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a9b7d22e46ada4e6a1f1404c267f3f023b44594929913d855f14bc5fb11b53d"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c81891e570e50d0afe43f722f426b0bd602d3c5315f0f290514511d9520b1e6"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abc2f05c1f30b9533cb9b85d73c28d93aa99c7ae2992df04c1704fcaf248b59c"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:339b94c536ab9c1b1bac245fb6814df3ba104603d2c1a97f8fb41922357bd772"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8883267df996b42494f40d533ef3a3fea247531d137773a649fb851747ae12c8"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:59c6c9d3ca7d84c5878a74d142816350a3bdfb51e4d10ac104afd396168481f6"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5759bb0fd13ee030626e8bbb5b644092a043817fb192335ff4c481402b1edd0e"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:1fe6ea9300f347fd3352c755aa04d71a2786afa008d1af1a35830e6a44e7fd5f"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:481c0389c3b26cd2aa498b6924ca6e9a1d1dd5b15ad5f009d273292949e47e24"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:658be4cabcc229f52a902f5e87205e1b9c29c66e463a267c8d8f237acde56002"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-win32.whl", hash = "sha256:7f8d89b16b4752deeb66dd321548c4cfa59819982d43d2ae7ab5d6e0f15bee94"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:2132d7724c03dd322035cf1b0c23ca9e7e449ec2b7225040a2ca2fa3f1a3bbfa"},
+    {file = "rapidfuzz-3.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:0bf1953a4c32ce6e2f3ed1897d0a8dbbbf19456ef0a8e37bae26e007d9fb5096"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:526df35d07083480e751f9679fd1f3e8a0819e8a13586e3860db5b65549a408a"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88c9e93508128168708aae3ef98eeb422a88204d81ac4492fcea1e1162a6af74"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:562caa39652c72156574fcf60ce7adf29964a031a57ae977c180947e00425b4a"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a149c944f3c349f6279a8cc4cbfa3e80cc2baaec9d983359698aa792faa44653"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c40a626050e37c2f74e2ba00538578d3c4a6baa171d08ed5091b6a03512ac4a"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ee35eddeddb5f5750d2a9cc55894926969fa0bac80bbe57211ae6fd0d34b39f"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94fe53da481d8580e6410f3e7e4ba4e9c5786cad1f289fbb6c9c9585c6d78e1"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4032713943c32fff97d863e6618162923e3b3c31917d437126d9fcf7e33c83d2"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dc1a39d1cc8e679c7240b2d1ed8366cf740ab8429cc9b582ebd94a5c6ededbe5"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f213bb5d4cd0b1fddf209bafe2d2896320a737fbded3a567d454e54875e4d9cc"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c9ca5f4ae767605cefa5156f5fa8561bee61849f9b2ccfb165d7087b4f9af90c"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:938cc766d0ce9eca95549593b6ca7ff86a2917b9e68c1989ad95485aed0f49dd"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:87eb7e9fb49265c33bda0417cc74c474a891cae60735fbbd75d79a106483888e"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-win32.whl", hash = "sha256:3f51d35521f86e767d3e640d0ab42908d01c3e05cf54ac1f0b547f3f602800f1"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:4ef3a6aa07b996c789c8d5ab99ed0563d551d32fa9330fd0f52ba28d20fcb662"},
+    {file = "rapidfuzz-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:7eea0ca53da78040a6b7bb041af8051c52efa7facc6f18dce33e679f2decaf62"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6dfd138dcc0920b71c1d1bc017413b032286a1f33488613dce9e254c454abaf2"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b81d15da16e97c288c645eb642d8a08d0ab98b827efb2682cab282a45893efe"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ff8835a3ba17f3baf3838f2612e3758d7b1ca09eb16c9a382df3bec5bb9bda3"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82a4ea0742b9e375d4856714ef59241007765edbce34fd2f7d76c552ed93a7d2"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5767b0e8220b6f8afcc1fe77529e5678470f9e94a1cfc9e29f5b0721dc1496c"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8c372dc278d1d5ced7cdc99ad8cc1d3de0b245e769a6b327c462b98873e5d4"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5d8664d6f844ea9018b4866e8a8dbf49c87f703668b1b3265de83aa3c9941272"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa098429af4e17fb5bacb0c39f1f8349891356ba7ca540521515b5708fec4a76"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:738ae2d59ab254c4f173b40b00a9c1f092697949284c59e0879e6e3beb337a69"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1a5eb2d1844f375f34e3c83b1a03094293d894472fdd1a095cf35e4dfa2ecf01"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:24d77723bb20030a91b096326d14b673d2ff1f0c7bbc64ed519992ed8eb5b869"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-win32.whl", hash = "sha256:b85dfb6f0c353c4b37499529f9831620a7bdc61c375e07f8c38b595f93e906e5"},
+    {file = "rapidfuzz-3.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:54fb70b667b882f9939bc6f581957fcb47fec2e7ad652259835c80e9e30230c9"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4e1da3dce34d742567da0722b9c8dc2b51554ab5a22fdaf763b60209445a7b37"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c50825de43442c4625a2ca1d948c911d116cf9007ad7f29cd27561c99b16947c"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69270e0cd850984f562b2239d07cde2213e5c3642cd8d550d5ac9a0fcd0882df"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5ddf48f63895f949355a1c4d643e0a531c9317d52901f80d5a6299d967b766"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd2ad4160d8ad9a2abdad1c765fd29e4d9b6b8ce6a707ee48eb2869e7dff0f89"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc39af05cdf89be426d96fce579c812948a324b022fb11dfea1e99e180d4f68b"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d25555297466ab5ded3875913fc0bfa78b89b0a32d79bd65ffbd32ae71f07c2d"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76da6c8972acd58c31efdd09c4c85263ba3b4394d2c2929be4c171a22293bab3"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2b50f2d429d81a65910f5ee9b14e172e300a09b8b2ecb91d3e4efc5d2583915c"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f00a8b3d0b21884ea972d5430797b1a25b9d2c715b3eaf03366903aac5d8398c"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e058ecfd8edb04b221d1b2d005f17be932075a16f75b100b275de1d3d220da5f"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:35e21f0718fd1c1853f8f433f2f84f618f6d4a6d9d96bb7c42e39797be600b58"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d128f615da9a198cd9b33be658a0c26fabe06a6d28fa4652953853e8d174c2c6"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-win32.whl", hash = "sha256:bc593306faa6c73e50cb31b81efbb580957272b14c5cf6bcf0233adf8a7e118d"},
+    {file = "rapidfuzz-3.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:0f7041c550b69d35675e04dc3f0690d0c26499039e942a0b1604c6547951e6fc"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:351df02889ac3da9f3f7b10e6812740927cfab9def453079da94f83697b03f2f"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35506d04429440333224c3711dfdb4195d34eff733cb48648d0c89a9b99faf14"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d24054843f4cfbb86df608ec1209e6a29b0d2635230577a94e38a9cfa3880d18"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a94fe0a42da816e4a6279ac4c23e4ba6de86a529b61b08d5e8e2633b29c781b"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41e98b3cebfa3e720186eeab37e6c0565895edf848fd958c34ab94c39e743311"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea5e25378591a698ae5076c582a0135db2cb43270fb2866737ab4cb6fcc34474"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6bf090b9b4ec4df5f0899bbc4055b8b173b33169186d4de1dd3d9c609bd330a2"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f241ca0bcbfcbf90bb48bb1c8dbc1fddc205bee5520f898b994adda3d3f150a"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f80986d3c8d55b848d679084231a35273320f658e64f0d86d725bb360e6cd2c4"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:62c760748b1253e08ab5138855e8f8d2c25a7cb5a0bfad74bb66db63c27d8a50"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8b5d78052e840191b9c7556eb3bd4fe52435e58bd979c75298b65262368dd1fa"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:fc764665ba19b923696eae6912a2f0fc52bdd7db6c53be178d1dd70eb72f2f68"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:62aac81ef17bab9f664828b9087d0afe5a94ed48396b0456a2503b68e3d567f2"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-win32.whl", hash = "sha256:aeb855b62bc351884a672b8f87875c552492d9199c78f33cc8650c283fd7504d"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a0e0d8798b7048c9db4e139bafb21792013fb043df07bfaf0d3dc9e1df2be5e6"},
+    {file = "rapidfuzz-3.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:ebf96d236a52058c010f354256e8de4274621a7f8b5a15dffa54d9b6a1c7e6e8"},
+    {file = "rapidfuzz-3.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fbe4b8305cb427b49d70c182a01c91fd85112e0573193a1f9e4fbcec35ea3eff"},
+    {file = "rapidfuzz-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db57085c9dbd0b1005d6ad905c610920c49d0752f522d2f34477b13cba24e1d1"},
+    {file = "rapidfuzz-3.0.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e334225a97824d9f75f5cf8e949e129bc183f0762f4c9b7a127d1809461bdc55"},
+    {file = "rapidfuzz-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c22a36b611a2d53fada2cb03b276135d08c2703039078ce985d7cc42734fd7"},
+    {file = "rapidfuzz-3.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:05130d9d33c4770116037de9f131e488825165105588cc7143f77733c5b25a6f"},
+    {file = "rapidfuzz-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9c263840bda0f532714ecd66f1f82ed3d3460f45e79e8a907f4df8eaafd93d31"},
+    {file = "rapidfuzz-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dbaa605c0f81208efbf166afb23f73b0f3847a1a966bec828f4167f61d0ca4b"},
+    {file = "rapidfuzz-3.0.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ba129c3c8202e8bef0d9964b8798913905ad1dc6293e94d7a02d87cdbef2544"},
+    {file = "rapidfuzz-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e85b4f4aeb994c841478e98a9e05bcb7ed8ead084d93bd2ca0683dc5e93b1c36"},
+    {file = "rapidfuzz-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:284f216f0500cd977830f107da5c3f96e91356dc7993512efc414dbd55679d51"},
+    {file = "rapidfuzz-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:494b613a3e730e08df1c7c14e45c303a0f5c8a701162bfc8ac9079585837de43"},
+    {file = "rapidfuzz-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a5670d5475777450fbc70ed8de8d4e3f7c69230a8b539f45bda358a6f9699f2"},
+    {file = "rapidfuzz-3.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14e9924108a26f2f58aa8cb90f1a106398fa43e359fa5a96b0f328c7bb7f76da"},
+    {file = "rapidfuzz-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d953b5f1462eba5a0830ea7df11b784f090ba5409fc92bccb856d2539b618"},
+    {file = "rapidfuzz-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5fdcc1ce830bf46fbc098c8b6eb3201a8299476153bae7a5d5e86576f7228d0a"},
+    {file = "rapidfuzz-3.0.0.tar.gz", hash = "sha256:4c1d895d16f62e9ac88d303eb918d90a390bd712055c849e01c558b7ae0fa908"},
 ]
 
 [package.extras]
@@ -1804,119 +2051,117 @@ full = ["numpy"]
 
 [[package]]
 name = "regex"
-version = "2022.10.31"
+version = "2023.5.5"
 description = "Alternative regular expression module, to replace re."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"},
-    {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"},
-    {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"},
-    {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"},
-    {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"},
-    {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"},
-    {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"},
-    {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"},
-    {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"},
-    {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"},
-    {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"},
-    {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"},
-    {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"},
-    {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"},
-    {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"},
-    {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"},
-    {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"},
-    {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"},
-    {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"},
-    {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"},
-    {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"},
-    {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"},
-    {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"},
-    {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"},
-    {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"},
-    {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"},
-    {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"},
-    {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"},
-    {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"},
-    {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"},
-    {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"},
-    {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"},
-    {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"},
-    {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"},
-    {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"},
+    {file = "regex-2023.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:48c9ec56579d4ba1c88f42302194b8ae2350265cb60c64b7b9a88dcb7fbde309"},
+    {file = "regex-2023.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f4541550459c08fdd6f97aa4e24c6f1932eec780d58a2faa2068253df7d6ff"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e22e4460f0245b468ee645156a4f84d0fc35a12d9ba79bd7d79bdcd2f9629d"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b870b6f632fc74941cadc2a0f3064ed8409e6f8ee226cdfd2a85ae50473aa94"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:171c52e320fe29260da550d81c6b99f6f8402450dc7777ef5ced2e848f3b6f8f"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad5524c2aedaf9aa14ef1bc9327f8abd915699dea457d339bebbe2f0d218f86"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a0f874ee8c0bc820e649c900243c6d1e6dc435b81da1492046716f14f1a2a96"},
+    {file = "regex-2023.5.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e645c757183ee0e13f0bbe56508598e2d9cd42b8abc6c0599d53b0d0b8dd1479"},
+    {file = "regex-2023.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a4c5da39bca4f7979eefcbb36efea04471cd68db2d38fcbb4ee2c6d440699833"},
+    {file = "regex-2023.5.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5e3f4468b8c6fd2fd33c218bbd0a1559e6a6fcf185af8bb0cc43f3b5bfb7d636"},
+    {file = "regex-2023.5.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:59e4b729eae1a0919f9e4c0fc635fbcc9db59c74ad98d684f4877be3d2607dd6"},
+    {file = "regex-2023.5.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ba73a14e9c8f9ac409863543cde3290dba39098fc261f717dc337ea72d3ebad2"},
+    {file = "regex-2023.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0bbd5dcb19603ab8d2781fac60114fb89aee8494f4505ae7ad141a3314abb1f9"},
+    {file = "regex-2023.5.5-cp310-cp310-win32.whl", hash = "sha256:40005cbd383438aecf715a7b47fe1e3dcbc889a36461ed416bdec07e0ef1db66"},
+    {file = "regex-2023.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:59597cd6315d3439ed4b074febe84a439c33928dd34396941b4d377692eca810"},
+    {file = "regex-2023.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f08276466fedb9e36e5193a96cb944928301152879ec20c2d723d1031cd4ddd"},
+    {file = "regex-2023.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd46f30e758629c3ee91713529cfbe107ac50d27110fdcc326a42ce2acf4dafc"},
+    {file = "regex-2023.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2910502f718828cecc8beff004917dcf577fc5f8f5dd40ffb1ea7612124547b"},
+    {file = "regex-2023.5.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:445d6f4fc3bd9fc2bf0416164454f90acab8858cd5a041403d7a11e3356980e8"},
+    {file = "regex-2023.5.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18196c16a584619c7c1d843497c069955d7629ad4a3fdee240eb347f4a2c9dbe"},
+    {file = "regex-2023.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d430a23b661629661f1fe8395be2004006bc792bb9fc7c53911d661b69dd7e"},
+    {file = "regex-2023.5.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72a28979cc667e5f82ef433db009184e7ac277844eea0f7f4d254b789517941d"},
+    {file = "regex-2023.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f764e4dfafa288e2eba21231f455d209f4709436baeebb05bdecfb5d8ddc3d35"},
+    {file = "regex-2023.5.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23d86ad2121b3c4fc78c58f95e19173790e22ac05996df69b84e12da5816cb17"},
+    {file = "regex-2023.5.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:690a17db524ee6ac4a27efc5406530dd90e7a7a69d8360235323d0e5dafb8f5b"},
+    {file = "regex-2023.5.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:1ecf3dcff71f0c0fe3e555201cbe749fa66aae8d18f80d2cc4de8e66df37390a"},
+    {file = "regex-2023.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:811040d7f3dd9c55eb0d8b00b5dcb7fd9ae1761c454f444fd9f37fe5ec57143a"},
+    {file = "regex-2023.5.5-cp311-cp311-win32.whl", hash = "sha256:c8c143a65ce3ca42e54d8e6fcaf465b6b672ed1c6c90022794a802fb93105d22"},
+    {file = "regex-2023.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:586a011f77f8a2da4b888774174cd266e69e917a67ba072c7fc0e91878178a80"},
+    {file = "regex-2023.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b6365703e8cf1644b82104cdd05270d1a9f043119a168d66c55684b1b557d008"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a56c18f21ac98209da9c54ae3ebb3b6f6e772038681d6cb43b8d53da3b09ee81"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8b942d8b3ce765dbc3b1dad0a944712a89b5de290ce8f72681e22b3c55f3cc8"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:844671c9c1150fcdac46d43198364034b961bd520f2c4fdaabfc7c7d7138a2dd"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2ce65bdeaf0a386bb3b533a28de3994e8e13b464ac15e1e67e4603dd88787fa"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fee0016cc35a8a91e8cc9312ab26a6fe638d484131a7afa79e1ce6165328a135"},
+    {file = "regex-2023.5.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:18f05d14f14a812fe9723f13afafefe6b74ca042d99f8884e62dbd34dcccf3e2"},
+    {file = "regex-2023.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:941b3f1b2392f0bcd6abf1bc7a322787d6db4e7457be6d1ffd3a693426a755f2"},
+    {file = "regex-2023.5.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:921473a93bcea4d00295799ab929522fc650e85c6b9f27ae1e6bb32a790ea7d3"},
+    {file = "regex-2023.5.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:e2205a81f815b5bb17e46e74cc946c575b484e5f0acfcb805fb252d67e22938d"},
+    {file = "regex-2023.5.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:385992d5ecf1a93cb85adff2f73e0402dd9ac29b71b7006d342cc920816e6f32"},
+    {file = "regex-2023.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:890a09cb0a62198bff92eda98b2b507305dd3abf974778bae3287f98b48907d3"},
+    {file = "regex-2023.5.5-cp36-cp36m-win32.whl", hash = "sha256:821a88b878b6589c5068f4cc2cfeb2c64e343a196bc9d7ac68ea8c2a776acd46"},
+    {file = "regex-2023.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:7918a1b83dd70dc04ab5ed24c78ae833ae8ea228cef84e08597c408286edc926"},
+    {file = "regex-2023.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:338994d3d4ca4cf12f09822e025731a5bdd3a37aaa571fa52659e85ca793fb67"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a69cf0c00c4d4a929c6c7717fd918414cab0d6132a49a6d8fc3ded1988ed2ea"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f5e06df94fff8c4c85f98c6487f6636848e1dc85ce17ab7d1931df4a081f657"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8906669b03c63266b6a7693d1f487b02647beb12adea20f8840c1a087e2dfb5"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fda3e50abad8d0f48df621cf75adc73c63f7243cbe0e3b2171392b445401550"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ac2b7d341dc1bd102be849d6dd33b09701223a851105b2754339e390be0627a"},
+    {file = "regex-2023.5.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fb2b495dd94b02de8215625948132cc2ea360ae84fe6634cd19b6567709c8ae2"},
+    {file = "regex-2023.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aa7d032c1d84726aa9edeb6accf079b4caa87151ca9fabacef31fa028186c66d"},
+    {file = "regex-2023.5.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d45864693351c15531f7e76f545ec35000d50848daa833cead96edae1665559"},
+    {file = "regex-2023.5.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21e90a288e6ba4bf44c25c6a946cb9b0f00b73044d74308b5e0afd190338297c"},
+    {file = "regex-2023.5.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:10250a093741ec7bf74bcd2039e697f519b028518f605ff2aa7ac1e9c9f97423"},
+    {file = "regex-2023.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6b8d0c153f07a953636b9cdb3011b733cadd4178123ef728ccc4d5969e67f3c2"},
+    {file = "regex-2023.5.5-cp37-cp37m-win32.whl", hash = "sha256:10374c84ee58c44575b667310d5bbfa89fb2e64e52349720a0182c0017512f6c"},
+    {file = "regex-2023.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9b320677521aabf666cdd6e99baee4fb5ac3996349c3b7f8e7c4eee1c00dfe3a"},
+    {file = "regex-2023.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:afb1c70ec1e594a547f38ad6bf5e3d60304ce7539e677c1429eebab115bce56e"},
+    {file = "regex-2023.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf123225945aa58b3057d0fba67e8061c62d14cc8a4202630f8057df70189051"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99757ad7fe5c8a2bb44829fc57ced11253e10f462233c1255fe03888e06bc19"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a623564d810e7a953ff1357f7799c14bc9beeab699aacc8b7ab7822da1e952b8"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ced02e3bd55e16e89c08bbc8128cff0884d96e7f7a5633d3dc366b6d95fcd1d6"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cbe6b5be3b9b698d8cc4ee4dee7e017ad655e83361cd0ea8e653d65e469468"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a6e4b0e0531223f53bad07ddf733af490ba2b8367f62342b92b39b29f72735a"},
+    {file = "regex-2023.5.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e9c4f778514a560a9c9aa8e5538bee759b55f6c1dcd35613ad72523fd9175b8"},
+    {file = "regex-2023.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:256f7f4c6ba145f62f7a441a003c94b8b1af78cee2cccacfc1e835f93bc09426"},
+    {file = "regex-2023.5.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bd7b68fd2e79d59d86dcbc1ccd6e2ca09c505343445daaa4e07f43c8a9cc34da"},
+    {file = "regex-2023.5.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4a5059bd585e9e9504ef9c07e4bc15b0a621ba20504388875d66b8b30a5c4d18"},
+    {file = "regex-2023.5.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6893544e06bae009916a5658ce7207e26ed17385149f35a3125f5259951f1bbe"},
+    {file = "regex-2023.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c64d5abe91a3dfe5ff250c6bb267ef00dbc01501518225b45a5f9def458f31fb"},
+    {file = "regex-2023.5.5-cp38-cp38-win32.whl", hash = "sha256:7923470d6056a9590247ff729c05e8e0f06bbd4efa6569c916943cb2d9b68b91"},
+    {file = "regex-2023.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:4035d6945cb961c90c3e1c1ca2feb526175bcfed44dfb1cc77db4fdced060d3e"},
+    {file = "regex-2023.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:50fd2d9b36938d4dcecbd684777dd12a407add4f9f934f235c66372e630772b0"},
+    {file = "regex-2023.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d19e57f888b00cd04fc38f5e18d0efbd91ccba2d45039453ab2236e6eec48d4d"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd966475e963122ee0a7118ec9024388c602d12ac72860f6eea119a3928be053"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db09e6c18977a33fea26fe67b7a842f706c67cf8bda1450974d0ae0dd63570df"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6164d4e2a82f9ebd7752a06bd6c504791bedc6418c0196cd0a23afb7f3e12b2d"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84397d3f750d153ebd7f958efaa92b45fea170200e2df5e0e1fd4d85b7e3f58a"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c3efee9bb53cbe7b285760c81f28ac80dc15fa48b5fe7e58b52752e642553f1"},
+    {file = "regex-2023.5.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:144b5b017646b5a9392a5554a1e5db0000ae637be4971c9747566775fc96e1b2"},
+    {file = "regex-2023.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1189fbbb21e2c117fda5303653b61905aeeeea23de4a94d400b0487eb16d2d60"},
+    {file = "regex-2023.5.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f83fe9e10f9d0b6cf580564d4d23845b9d692e4c91bd8be57733958e4c602956"},
+    {file = "regex-2023.5.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:72aa4746993a28c841e05889f3f1b1e5d14df8d3daa157d6001a34c98102b393"},
+    {file = "regex-2023.5.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:de2f780c3242ea114dd01f84848655356af4dd561501896c751d7b885ea6d3a1"},
+    {file = "regex-2023.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:290fd35219486dfbc00b0de72f455ecdd63e59b528991a6aec9fdfc0ce85672e"},
+    {file = "regex-2023.5.5-cp39-cp39-win32.whl", hash = "sha256:732176f5427e72fa2325b05c58ad0b45af341c459910d766f814b0584ac1f9ac"},
+    {file = "regex-2023.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:1307aa4daa1cbb23823d8238e1f61292fd07e4e5d8d38a6efff00b67a7cdb764"},
+    {file = "regex-2023.5.5.tar.gz", hash = "sha256:7d76a8a1fc9da08296462a18f16620ba73bcbf5909e42383b253ef34d9d5141e"},
 ]
 
 [[package]]
 name = "requests"
-version = "2.28.2"
+version = "2.31.0"
 description = "Python HTTP for Humans."
-category = "main"
 optional = false
-python-versions = ">=3.7, <4"
+python-versions = ">=3.7"
 files = [
-    {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
-    {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
+    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
 ]
 
 [package.dependencies]
 certifi = ">=2017.4.17"
 charset-normalizer = ">=2,<4"
 idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<1.27"
+urllib3 = ">=1.21.1,<3"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
@@ -1924,29 +2169,27 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "rhoknp"
-version = "1.2.1"
+version = "1.3.1"
 description = "Yet another Python binding for Juman++/KNP/KWJA"
-category = "main"
 optional = false
-python-versions = ">=3.7,<3.12"
+python-versions = ">=3.7,<4.0"
 files = [
-    {file = "rhoknp-1.2.1-py3-none-any.whl", hash = "sha256:93ce06a6564cfd00956fdc2e022578025122657abb2a3c069c6a72fdd0be9da9"},
-    {file = "rhoknp-1.2.1.tar.gz", hash = "sha256:5d29f9a865d396e4bb5a589236c789fdb649c05e226794e98c6c565db643ce25"},
+    {file = "rhoknp-1.3.1-py3-none-any.whl", hash = "sha256:c1f8292eef6640ebbb5247e23d9910981ba80151a6c5878d5dc240df658fe032"},
+    {file = "rhoknp-1.3.1.tar.gz", hash = "sha256:70213e011d9246e80d2a89a2ada725fabf9a746f844c9b413322319eaf94527a"},
 ]
 
 [package.extras]
-cli = ["PyYAML (>=6.0,<7.0)", "fastapi (>=0.92.0,<0.93.0)", "rich (>=12.6,<14)", "typer (>=0.6.1,<0.8)", "uvicorn (>=0.20.0,<0.21.0)"]
+cli = ["PyYAML (>=6.0,<7.0)", "fastapi (>=0.92.0,<1)", "jinja2 (>=3.1,<4.0)", "rich (>=12.6)", "typer (>=0.7)", "typer (>=0.7,<0.9)", "uvicorn (>=0.21.0,<1)"]
 
 [[package]]
 name = "rich"
-version = "13.3.2"
+version = "13.4.1"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "rich-13.3.2-py3-none-any.whl", hash = "sha256:a104f37270bf677148d8acb07d33be1569eeee87e2d1beb286a4e9113caf6f2f"},
-    {file = "rich-13.3.2.tar.gz", hash = "sha256:91954fe80cfb7985727a467ca98a7618e5dd15178cc2da10f553b36a93859001"},
+    {file = "rich-13.4.1-py3-none-any.whl", hash = "sha256:d204aadb50b936bf6b1a695385429d192bc1fdaf3e8b907e8e26f4c4e4b5bf75"},
+    {file = "rich-13.4.1.tar.gz", hash = "sha256:76f6b65ea7e5c5d924ba80e322231d7cb5b5981aa60bfc1e694f1bc097fe6fe1"},
 ]
 
 [package.dependencies]
@@ -1961,7 +2204,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
 name = "scikit-learn"
 version = "1.2.2"
 description = "A set of python modules for machine learning and data mining"
-category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
@@ -2002,105 +2244,110 @@ tests = ["black (>=22.3.0)", "flake8 (>=3.8.2)", "matplotlib (>=3.1.3)", "mypy (
 
 [[package]]
 name = "scipy"
-version = "1.10.1"
+version = "1.9.3"
 description = "Fundamental algorithms for scientific computing in Python"
-category = "main"
-optional = false
-python-versions = "<3.12,>=3.8"
-files = [
-    {file = "scipy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7354fd7527a4b0377ce55f286805b34e8c54b91be865bac273f527e1b839019"},
-    {file = "scipy-1.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4b3f429188c66603a1a5c549fb414e4d3bdc2a24792e061ffbd607d3d75fd84e"},
-    {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1553b5dcddd64ba9a0d95355e63fe6c3fc303a8fd77c7bc91e77d61363f7433f"},
-    {file = "scipy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0ff64b06b10e35215abce517252b375e580a6125fd5fdf6421b98efbefb2d2"},
-    {file = "scipy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:fae8a7b898c42dffe3f7361c40d5952b6bf32d10c4569098d276b4c547905ee1"},
-    {file = "scipy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f1564ea217e82c1bbe75ddf7285ba0709ecd503f048cb1236ae9995f64217bd"},
-    {file = "scipy-1.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d925fa1c81b772882aa55bcc10bf88324dadb66ff85d548c71515f6689c6dac5"},
-    {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaea0a6be54462ec027de54fca511540980d1e9eea68b2d5c1dbfe084797be35"},
-    {file = "scipy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15a35c4242ec5f292c3dd364a7c71a61be87a3d4ddcc693372813c0b73c9af1d"},
-    {file = "scipy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:43b8e0bcb877faf0abfb613d51026cd5cc78918e9530e375727bf0625c82788f"},
-    {file = "scipy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5678f88c68ea866ed9ebe3a989091088553ba12c6090244fdae3e467b1139c35"},
-    {file = "scipy-1.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:39becb03541f9e58243f4197584286e339029e8908c46f7221abeea4b749fa88"},
-    {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bce5869c8d68cf383ce240e44c1d9ae7c06078a9396df68ce88a1230f93a30c1"},
-    {file = "scipy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07c3457ce0b3ad5124f98a86533106b643dd811dd61b548e78cf4c8786652f6f"},
-    {file = "scipy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:049a8bbf0ad95277ffba9b3b7d23e5369cc39e66406d60422c8cfef40ccc8415"},
-    {file = "scipy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cd9f1027ff30d90618914a64ca9b1a77a431159df0e2a195d8a9e8a04c78abf9"},
-    {file = "scipy-1.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:79c8e5a6c6ffaf3a2262ef1be1e108a035cf4f05c14df56057b64acc5bebffb6"},
-    {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51af417a000d2dbe1ec6c372dfe688e041a7084da4fdd350aeb139bd3fb55353"},
-    {file = "scipy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b4735d6c28aad3cdcf52117e0e91d6b39acd4272f3f5cd9907c24ee931ad601"},
-    {file = "scipy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ff7f37b1bf4417baca958d254e8e2875d0cc23aaadbe65b3d5b3077b0eb23ea"},
-    {file = "scipy-1.10.1.tar.gz", hash = "sha256:2cf9dfb80a7b4589ba4c40ce7588986d6d5cebc5457cad2c2880f6bc2d42f3a5"},
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"},
+    {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"},
+    {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"},
+    {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"},
+    {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"},
+    {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"},
+    {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"},
+    {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"},
+    {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"},
+    {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"},
+    {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"},
+    {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"},
+    {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"},
+    {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"},
+    {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"},
+    {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"},
+    {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"},
+    {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"},
+    {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"},
+    {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"},
+    {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"},
 ]
 
 [package.dependencies]
-numpy = ">=1.19.5,<1.27.0"
+numpy = ">=1.18.5,<1.26.0"
 
 [package.extras]
-dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", "rich-click", "typing_extensions"]
-doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
-test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
+dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"]
+doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"]
+test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
 
 [[package]]
 name = "sentencepiece"
-version = "0.1.97"
+version = "0.1.99"
 description = "SentencePiece python wrapper"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "sentencepiece-0.1.97-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6f249c8f1852893be86eae66b19d522c5fb30bbad4fe2d1b07f06fdc86e1907e"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09e1bc53178de70c557a9ba4fece07364b4416ce3d36570726b3372b68aea135"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667193c57fb48b238be7e3d7636cfc8da56cb5bac5559d8f0b647334e1175be8"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2780531985af79c6163f63d4f200fec8a28b70b6768d2c19f70d01568a4524e8"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:205050670c53ef9015e2a98cce3934bfbcf0aafaa14caa0c618dd5667bc217ee"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b183dadef8e8b6b4645c1c20692d7be0a13ecc3ec1a07b3885c8905516675f"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-win32.whl", hash = "sha256:ee3c9dbd558d8d85bb1617087b86df6ea2b856a528669630ce6cedeb4353b823"},
-    {file = "sentencepiece-0.1.97-cp310-cp310-win_amd64.whl", hash = "sha256:f7dc55379e2f7dee86537180283db2e5f8418c6825fdd2fe436c724eb5604c05"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ba1b4154f9144c5a7528b00aff5cffaa1a896a1c6ca53ca78b6e74cd2dae5244"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac3d90aee5581e55d029d124ac11b6ae2fbae0817863b664b2f2302e966ababb"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c27400f1ac46518a01c87cb7703650e4e48728649feb115d2e3f1102a946a42"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6e12a166eba75994ca749aadc4a5056b91b31405f805d6de6e8914cc9741c60"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-win32.whl", hash = "sha256:ed85dff5c0a9b3dd1a414c7e1119f2a19e863fc3f81da525bf7f885ebc883de0"},
-    {file = "sentencepiece-0.1.97-cp36-cp36m-win_amd64.whl", hash = "sha256:91a19ab6f40ffbae6d6127119953d2c6a85e93d734953dbc8629fde0d21ace66"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bae580e4a35a9314ff49561ac7c06574fe6afc71b821ed6bb00534e571458156"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad7262e7530c683b186672b5dd0082f82719a50a500a8cfbc4bbd7cde5bff8c"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:620cee35279720016735a7c7103cddbd9b84fe5e2f098bd5e673834d69fee2b8"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93b921b59914c0ec6697e8c6d5e6b44d99d1298fb1a0af56980a79ade0540c19"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-win32.whl", hash = "sha256:9b9a4c44a31d5f47616e9568dcf31e029b0bfa776e0a252c0b59247881598b09"},
-    {file = "sentencepiece-0.1.97-cp37-cp37m-win_amd64.whl", hash = "sha256:f31533cdacced56219e239d3459a003ece35116920dd64b2309d4ad047b77644"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7d643c01d1cad13b9206a276bbe5bc1a468e3d7cf6a26bde7783f945277f859d"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:542f1985b1ee279a92bef7740ec0781452372028ce01e15aa88df3228b197ba3"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93701da21fea906dd244bf88cdbe640385a89c45d3c1812b76dbadf8782cdbcd"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a51514047b964047b7fadb480d88a5e0f72c02f6ca1ba96258fbbc6e79274a94"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3ae2e9b7a5b6f2aa64ec9240b0c185dabe597d0e787dc4344acfbaef1ffe0b2"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923ee4af16dbae1f2ab358ed09f8a0eb89e40a8198a8b343bf54181482342721"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-win32.whl", hash = "sha256:fa6f2b88850b5fae3a05053658824cf9f147c8e3c3b40eb64539a976c83d8a24"},
-    {file = "sentencepiece-0.1.97-cp38-cp38-win_amd64.whl", hash = "sha256:5137ff0d0b1cc574751d178650ef800ff8d90bf21eb9f71e9567d4a0548940a5"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f92876271a10494671431ad955bff2d6f8ea59baaf957f5ae5946aff56dfcb90"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35c227b6d55e473033db7e0ecc51b1e99e6ed7607cc08602fb5768132543c81d"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1706a8a8188f7b3d4b7922db9bb00c64c4e16ee68ab4caaae79f55b3e18748c7"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce61efc1862ccb18856c4aabbd930e13d5bfbb4b09b4f111081ac53a9dc62275"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a78c03800ef9f02d320e0159f5768b15357f3e9ebea545c9c4ba7928ba8ba254"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753b8088fd685ee787d9f54c84275ab347de558c7c4ebc6accb4c35bf7776f20"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-win32.whl", hash = "sha256:24306fd86031c17a1a6ae92671e76a350390a3140a65620bc2843dad7db24e2a"},
-    {file = "sentencepiece-0.1.97-cp39-cp39-win_amd64.whl", hash = "sha256:c6641d0b7acec61fde5881ea6ebe098c169557ac9aa3bdabdf124eab5a5592bb"},
-    {file = "sentencepiece-0.1.97.tar.gz", hash = "sha256:c901305e0a710bbcd296f66d79e96f744e6e175b29812bd5178318437d4e1f6c"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0eb528e70571b7c02723e5804322469b82fe7ea418c96051d0286c0fa028db73"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d7fafb2c4e4659cbdf303929503f37a26eabc4ff31d3a79bf1c5a1b338caa7"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be9cf5b9e404c245aeb3d3723c737ba7a8f5d4ba262ef233a431fa6c45f732a0"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baed1a26464998f9710d20e52607c29ffd4293e7c71c6a1f83f51ad0911ec12c"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9832f08bb372d4c8b567612f8eab9e36e268dff645f1c28f9f8e851be705f6d1"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:019e7535108e309dae2b253a75834fc3128240aa87c00eb80732078cdc182588"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-win32.whl", hash = "sha256:fa16a830416bb823fa2a52cbdd474d1f7f3bba527fd2304fb4b140dad31bb9bc"},
+    {file = "sentencepiece-0.1.99-cp310-cp310-win_amd64.whl", hash = "sha256:14b0eccb7b641d4591c3e12ae44cab537d68352e4d3b6424944f0c447d2348d5"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d3c56f24183a1e8bd61043ff2c58dfecdc68a5dd8955dc13bab83afd5f76b81"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed6ea1819fd612c989999e44a51bf556d0ef6abfb553080b9be3d347e18bcfb7"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2a0260cd1fb7bd8b4d4f39dc2444a8d5fd4e0a0c4d5c899810ef1abf99b2d45"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a1abff4d1ff81c77cac3cc6fefa34fa4b8b371e5ee51cb7e8d1ebc996d05983"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:004e6a621d4bc88978eecb6ea7959264239a17b70f2cbc348033d8195c9808ec"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db361e03342c41680afae5807590bc88aa0e17cfd1a42696a160e4005fcda03b"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-win32.whl", hash = "sha256:2d95e19168875b70df62916eb55428a0cbcb834ac51d5a7e664eda74def9e1e0"},
+    {file = "sentencepiece-0.1.99-cp311-cp311-win_amd64.whl", hash = "sha256:f90d73a6f81248a909f55d8e6ef56fec32d559e1e9af045f0b0322637cb8e5c7"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:62e24c81e74bd87a6e0d63c51beb6527e4c0add67e1a17bac18bcd2076afcfeb"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57efcc2d51caff20d9573567d9fd3f854d9efe613ed58a439c78c9f93101384a"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a904c46197993bd1e95b93a6e373dca2f170379d64441041e2e628ad4afb16f"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89adf59854741c0d465f0e1525b388c0d174f611cc04af54153c5c4f36088c4"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-win32.whl", hash = "sha256:47c378146928690d1bc106fdf0da768cebd03b65dd8405aa3dd88f9c81e35dba"},
+    {file = "sentencepiece-0.1.99-cp36-cp36m-win_amd64.whl", hash = "sha256:9ba142e7a90dd6d823c44f9870abdad45e6c63958eb60fe44cca6828d3b69da2"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7b1a9ae4d7c6f1f867e63370cca25cc17b6f4886729595b885ee07a58d3cec3"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0f644c9d4d35c096a538507b2163e6191512460035bf51358794a78515b74f7"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8843d23a0f686d85e569bd6dcd0dd0e0cbc03731e63497ca6d5bacd18df8b85"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6f690a1caebb4867a2e367afa1918ad35be257ecdb3455d2bbd787936f155"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-win32.whl", hash = "sha256:8a321866c2f85da7beac74a824b4ad6ddc2a4c9bccd9382529506d48f744a12c"},
+    {file = "sentencepiece-0.1.99-cp37-cp37m-win_amd64.whl", hash = "sha256:c42f753bcfb7661c122a15b20be7f684b61fc8592c89c870adf52382ea72262d"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:85b476406da69c70586f0bb682fcca4c9b40e5059814f2db92303ea4585c650c"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfbcfe13c69d3f87b7fcd5da168df7290a6d006329be71f90ba4f56bc77f8561"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:445b0ec381af1cd4eef95243e7180c63d9c384443c16c4c47a28196bd1cda937"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6890ea0f2b4703f62d0bf27932e35808b1f679bdb05c7eeb3812b935ba02001"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb71af492b0eefbf9f2501bec97bcd043b6812ab000d119eaf4bd33f9e283d03"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b866b5bd3ddd54166bbcbf5c8d7dd2e0b397fac8537991c7f544220b1f67bc"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-win32.whl", hash = "sha256:b133e8a499eac49c581c3c76e9bdd08c338cc1939e441fee6f92c0ccb5f1f8be"},
+    {file = "sentencepiece-0.1.99-cp38-cp38-win_amd64.whl", hash = "sha256:0eaf3591dd0690a87f44f4df129cf8d05d8a4029b5b6709b489b8e27f9a9bcff"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38efeda9bbfb55052d482a009c6a37e52f42ebffcea9d3a98a61de7aee356a28"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c030b081dc1e1bcc9fadc314b19b740715d3d566ad73a482da20d7d46fd444c"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:84dbe53e02e4f8a2e45d2ac3e430d5c83182142658e25edd76539b7648928727"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b0f55d0a0ee1719b4b04221fe0c9f0c3461dc3dabd77a035fa2f4788eb3ef9a"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e800f206cd235dc27dc749299e05853a4e4332e8d3dfd81bf13d0e5b9007d9"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae1c40cda8f9d5b0423cfa98542735c0235e7597d79caf318855cdf971b2280"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-win32.whl", hash = "sha256:c84ce33af12ca222d14a1cdd37bd76a69401e32bc68fe61c67ef6b59402f4ab8"},
+    {file = "sentencepiece-0.1.99-cp39-cp39-win_amd64.whl", hash = "sha256:350e5c74d739973f1c9643edb80f7cc904dc948578bcb1d43c6f2b173e5d18dd"},
+    {file = "sentencepiece-0.1.99.tar.gz", hash = "sha256:189c48f5cb2949288f97ccdb97f0473098d9c3dcf5a3d99d4eabe719ec27297f"},
 ]
 
 [[package]]
 name = "sentry-sdk"
-version = "1.16.0"
+version = "1.24.0"
 description = "Python client for Sentry (https://sentry.io)"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
-    {file = "sentry-sdk-1.16.0.tar.gz", hash = "sha256:a900845bd78c263d49695d48ce78a4bce1030bbd917e0b6cc021fc000c901113"},
-    {file = "sentry_sdk-1.16.0-py2.py3-none-any.whl", hash = "sha256:633edefead34d976ff22e7edc367cdf57768e24bc714615ccae746d9d91795ae"},
+    {file = "sentry-sdk-1.24.0.tar.gz", hash = "sha256:0bbcecda9f51936904c1030e7fef0fe693e633888f02a14d1cb68646a50e83b3"},
+    {file = "sentry_sdk-1.24.0-py2.py3-none-any.whl", hash = "sha256:56d6d9d194c898d853a7c1dd99bed92ce82334ee1282292c15bcc967ff1a49b5"},
 ]
 
 [package.dependencies]
 certifi = "*"
-urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
+urllib3 = {version = ">=1.26.11,<2.0.0", markers = "python_version >= \"3.6\""}
 
 [package.extras]
 aiohttp = ["aiohttp (>=3.5)"]
@@ -2112,9 +2359,11 @@ chalice = ["chalice (>=1.16.0)"]
 django = ["django (>=1.8)"]
 falcon = ["falcon (>=1.4)"]
 fastapi = ["fastapi (>=0.79.0)"]
-flask = ["blinker (>=1.1)", "flask (>=0.11)"]
+flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
+grpcio = ["grpcio (>=1.21.1)"]
 httpx = ["httpx (>=0.16.0)"]
 huey = ["huey (>=2)"]
+loguru = ["loguru (>=0.5)"]
 opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
 pure-eval = ["asttokens", "executing", "pure-eval"]
 pymongo = ["pymongo (>=3.1)"]
@@ -2131,7 +2380,6 @@ tornado = ["tornado (>=5)"]
 name = "seqeval"
 version = "1.2.2"
 description = "Testing framework for sequence labeling"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -2146,7 +2394,6 @@ scikit-learn = ">=0.21.3"
 name = "setproctitle"
 version = "1.3.2"
 description = "A Python module to customize the process title"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2229,26 +2476,24 @@ test = ["pytest"]
 
 [[package]]
 name = "setuptools"
-version = "67.6.0"
+version = "67.8.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"},
-    {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"},
+    {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"},
+    {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"},
 ]
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -2260,7 +2505,6 @@ files = [
 name = "smmap"
 version = "5.0.0"
 description = "A pure Python implementation of a sliding window memory map manager"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2272,7 +2516,6 @@ files = [
 name = "stack-data"
 version = "0.6.2"
 description = "Extract data from python stack frames and tracebacks for informative displays"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2288,11 +2531,24 @@ pure-eval = "*"
 [package.extras]
 tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
 
+[[package]]
+name = "sympy"
+version = "1.12"
+description = "Computer algebra system (CAS) in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"},
+    {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"},
+]
+
+[package.dependencies]
+mpmath = ">=0.19"
+
 [[package]]
 name = "threadpoolctl"
 version = "3.1.0"
 description = "threadpoolctl"
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -2302,52 +2558,51 @@ files = [
 
 [[package]]
 name = "tokenizers"
-version = "0.13.2"
+version = "0.13.3"
 description = "Fast and Customizable Tokenizers"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "tokenizers-0.13.2-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:a6f36b1b499233bb4443b5e57e20630c5e02fba61109632f5e00dab970440157"},
-    {file = "tokenizers-0.13.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:bc6983282ee74d638a4b6d149e5dadd8bc7ff1d0d6de663d69f099e0c6bddbeb"},
-    {file = "tokenizers-0.13.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16756e6ab264b162f99c0c0a8d3d521328f428b33374c5ee161c0ebec42bf3c0"},
-    {file = "tokenizers-0.13.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b10db6e4b036c78212c6763cb56411566edcf2668c910baa1939afd50095ce48"},
-    {file = "tokenizers-0.13.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:238e879d1a0f4fddc2ce5b2d00f219125df08f8532e5f1f2ba9ad42f02b7da59"},
-    {file = "tokenizers-0.13.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47ef745dbf9f49281e900e9e72915356d69de3a4e4d8a475bda26bfdb5047736"},
-    {file = "tokenizers-0.13.2-cp310-cp310-win32.whl", hash = "sha256:96cedf83864bcc15a3ffd088a6f81a8a8f55b8b188eabd7a7f2a4469477036df"},
-    {file = "tokenizers-0.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:eda77de40a0262690c666134baf19ec5c4f5b8bde213055911d9f5a718c506e1"},
-    {file = "tokenizers-0.13.2-cp311-cp311-macosx_10_11_universal2.whl", hash = "sha256:9eee037bb5aa14daeb56b4c39956164b2bebbe6ab4ca7779d88aa16b79bd4e17"},
-    {file = "tokenizers-0.13.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d1b079c4c9332048fec4cb9c2055c2373c74fbb336716a5524c9a720206d787e"},
-    {file = "tokenizers-0.13.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a689654fc745135cce4eea3b15e29c372c3e0b01717c6978b563de5c38af9811"},
-    {file = "tokenizers-0.13.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3606528c07cda0566cff6cbfbda2b167f923661be595feac95701ffcdcbdbb21"},
-    {file = "tokenizers-0.13.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41291d0160946084cbd53c8ec3d029df3dc2af2673d46b25ff1a7f31a9d55d51"},
-    {file = "tokenizers-0.13.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7892325f9ca1cc5fca0333d5bfd96a19044ce9b092ce2df625652109a3de16b8"},
-    {file = "tokenizers-0.13.2-cp311-cp311-win32.whl", hash = "sha256:93714958d4ebe5362d3de7a6bd73dc86c36b5af5941ebef6c325ac900fa58865"},
-    {file = "tokenizers-0.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:fa7ef7ee380b1f49211bbcfac8a006b1a3fa2fa4c7f4ee134ae384eb4ea5e453"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:da521bfa94df6a08a6254bb8214ea04854bb9044d61063ae2529361688b5440a"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a739d4d973d422e1073989769723f3b6ad8b11e59e635a63de99aea4b2208188"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cac01fc0b868e4d0a3aa7c5c53396da0a0a63136e81475d32fcf5c348fcb2866"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0901a5c6538d2d2dc752c6b4bde7dab170fddce559ec75662cfad03b3187c8f6"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ba9baa76b5a3eefa78b6cc351315a216232fd727ee5e3ce0f7c6885d9fb531b"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-win32.whl", hash = "sha256:a537061ee18ba104b7f3daa735060c39db3a22c8a9595845c55b6c01d36c5e87"},
-    {file = "tokenizers-0.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c82fb87b1cbfa984d8f05b2b3c3c73e428b216c1d4f0e286d0a3b27f521b32eb"},
-    {file = "tokenizers-0.13.2-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:ce298605a833ac7f81b8062d3102a42dcd9fa890493e8f756112c346339fe5c5"},
-    {file = "tokenizers-0.13.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:f44d59bafe3d61e8a56b9e0a963075187c0f0091023120b13fbe37a87936f171"},
-    {file = "tokenizers-0.13.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a51b93932daba12ed07060935978a6779593a59709deab04a0d10e6fd5c29e60"},
-    {file = "tokenizers-0.13.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6969e5ea7ccb909ce7d6d4dfd009115dc72799b0362a2ea353267168667408c4"},
-    {file = "tokenizers-0.13.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92f040c4d938ea64683526b45dfc81c580e3b35aaebe847e7eec374961231734"},
-    {file = "tokenizers-0.13.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d3bc9f7d7f4c1aa84bb6b8d642a60272c8a2c987669e9bb0ac26daf0c6a9fc8"},
-    {file = "tokenizers-0.13.2-cp38-cp38-win32.whl", hash = "sha256:efbf189fb9cf29bd29e98c0437bdb9809f9de686a1e6c10e0b954410e9ca2142"},
-    {file = "tokenizers-0.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b4cb2c60c094f31ea652f6cf9f349aae815f9243b860610c29a69ed0d7a88f8"},
-    {file = "tokenizers-0.13.2-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:b47d6212e7dd05784d7330b3b1e5a170809fa30e2b333ca5c93fba1463dec2b7"},
-    {file = "tokenizers-0.13.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:80a57501b61ec4f94fb7ce109e2b4a1a090352618efde87253b4ede6d458b605"},
-    {file = "tokenizers-0.13.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61507a9953f6e7dc3c972cbc57ba94c80c8f7f686fbc0876afe70ea2b8cc8b04"},
-    {file = "tokenizers-0.13.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c09f4fa620e879debdd1ec299bb81e3c961fd8f64f0e460e64df0818d29d845c"},
-    {file = "tokenizers-0.13.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66c892d85385b202893ac6bc47b13390909e205280e5df89a41086cfec76fedb"},
-    {file = "tokenizers-0.13.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3e306b0941ad35087ae7083919a5c410a6b672be0343609d79a1171a364ce79"},
-    {file = "tokenizers-0.13.2-cp39-cp39-win32.whl", hash = "sha256:79189e7f706c74dbc6b753450757af172240916d6a12ed4526af5cc6d3ceca26"},
-    {file = "tokenizers-0.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:486d637b413fddada845a10a45c74825d73d3725da42ccd8796ccd7a1c07a024"},
-    {file = "tokenizers-0.13.2.tar.gz", hash = "sha256:f9525375582fd1912ac3caa2f727d36c86ff8c0c6de45ae1aaff90f87f33b907"},
+    {file = "tokenizers-0.13.3-cp310-cp310-macosx_10_11_x86_64.whl", hash = "sha256:f3835c5be51de8c0a092058a4d4380cb9244fb34681fd0a295fbf0a52a5fdf33"},
+    {file = "tokenizers-0.13.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:4ef4c3e821730f2692489e926b184321e887f34fb8a6b80b8096b966ba663d07"},
+    {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5fd1a6a25353e9aa762e2aae5a1e63883cad9f4e997c447ec39d071020459bc"},
+    {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee0b1b311d65beab83d7a41c56a1e46ab732a9eed4460648e8eb0bd69fc2d059"},
+    {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ef4215284df1277dadbcc5e17d4882bda19f770d02348e73523f7e7d8b8d396"},
+    {file = "tokenizers-0.13.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4d53976079cff8a033f778fb9adca2d9d69d009c02fa2d71a878b5f3963ed30"},
+    {file = "tokenizers-0.13.3-cp310-cp310-win32.whl", hash = "sha256:1f0e3b4c2ea2cd13238ce43548959c118069db7579e5d40ec270ad77da5833ce"},
+    {file = "tokenizers-0.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:89649c00d0d7211e8186f7a75dfa1db6996f65edce4b84821817eadcc2d3c79e"},
+    {file = "tokenizers-0.13.3-cp311-cp311-macosx_10_11_universal2.whl", hash = "sha256:56b726e0d2bbc9243872b0144515ba684af5b8d8cd112fb83ee1365e26ec74c8"},
+    {file = "tokenizers-0.13.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc5c022ce692e1f499d745af293ab9ee6f5d92538ed2faf73f9708c89ee59ce6"},
+    {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55c981ac44ba87c93e847c333e58c12abcbb377a0c2f2ef96e1a266e4184ff2"},
+    {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f247eae99800ef821a91f47c5280e9e9afaeed9980fc444208d5aa6ba69ff148"},
+    {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b3e3215d048e94f40f1c95802e45dcc37c5b05eb46280fc2ccc8cd351bff839"},
+    {file = "tokenizers-0.13.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ba2b0bf01777c9b9bc94b53764d6684554ce98551fec496f71bc5be3a03e98b"},
+    {file = "tokenizers-0.13.3-cp311-cp311-win32.whl", hash = "sha256:cc78d77f597d1c458bf0ea7c2a64b6aa06941c7a99cb135b5969b0278824d808"},
+    {file = "tokenizers-0.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:ecf182bf59bd541a8876deccf0360f5ae60496fd50b58510048020751cf1724c"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:0527dc5436a1f6bf2c0327da3145687d3bcfbeab91fed8458920093de3901b44"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07cbb2c307627dc99b44b22ef05ff4473aa7c7cc1fec8f0a8b37d8a64b1a16d2"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4560dbdeaae5b7ee0d4e493027e3de6d53c991b5002d7ff95083c99e11dd5ac0"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64064bd0322405c9374305ab9b4c07152a1474370327499911937fd4a76d004b"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8c6e2ab0f2e3d939ca66aa1d596602105fe33b505cd2854a4c1717f704c51de"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-win32.whl", hash = "sha256:6cc29d410768f960db8677221e497226e545eaaea01aa3613fa0fdf2cc96cff4"},
+    {file = "tokenizers-0.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fc2a7fdf864554a0dacf09d32e17c0caa9afe72baf9dd7ddedc61973bae352d8"},
+    {file = "tokenizers-0.13.3-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:8791dedba834c1fc55e5f1521be325ea3dafb381964be20684b92fdac95d79b7"},
+    {file = "tokenizers-0.13.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:d607a6a13718aeb20507bdf2b96162ead5145bbbfa26788d6b833f98b31b26e1"},
+    {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3791338f809cd1bf8e4fee6b540b36822434d0c6c6bc47162448deee3f77d425"},
+    {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2f35f30e39e6aab8716f07790f646bdc6e4a853816cc49a95ef2a9016bf9ce6"},
+    {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310204dfed5aa797128b65d63538a9837cbdd15da2a29a77d67eefa489edda26"},
+    {file = "tokenizers-0.13.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0f9b92ea052305166559f38498b3b0cae159caea712646648aaa272f7160963"},
+    {file = "tokenizers-0.13.3-cp38-cp38-win32.whl", hash = "sha256:9a3fa134896c3c1f0da6e762d15141fbff30d094067c8f1157b9fdca593b5806"},
+    {file = "tokenizers-0.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:8e7b0cdeace87fa9e760e6a605e0ae8fc14b7d72e9fc19c578116f7287bb873d"},
+    {file = "tokenizers-0.13.3-cp39-cp39-macosx_10_11_x86_64.whl", hash = "sha256:00cee1e0859d55507e693a48fa4aef07060c4bb6bd93d80120e18fea9371c66d"},
+    {file = "tokenizers-0.13.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:a23ff602d0797cea1d0506ce69b27523b07e70f6dda982ab8cf82402de839088"},
+    {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ce07445050b537d2696022dafb115307abdffd2a5c106f029490f84501ef97"},
+    {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:280ffe95f50eaaf655b3a1dc7ff1d9cf4777029dbbc3e63a74e65a056594abc3"},
+    {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97acfcec592f7e9de8cadcdcda50a7134423ac8455c0166b28c9ff04d227b371"},
+    {file = "tokenizers-0.13.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7730c98a3010cd4f523465867ff95cd9d6430db46676ce79358f65ae39797b"},
+    {file = "tokenizers-0.13.3-cp39-cp39-win32.whl", hash = "sha256:48625a108029cb1ddf42e17a81b5a3230ba6888a70c9dc14e81bc319e812652d"},
+    {file = "tokenizers-0.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:bc0a6f1ba036e482db6453571c9e3e60ecd5489980ffd95d11dc9f960483d783"},
+    {file = "tokenizers-0.13.3.tar.gz", hash = "sha256:2e546dbb68b623008a5442353137fbb0123d311a6d7ba52f2667c8862a75af2e"},
 ]
 
 [package.extras]
@@ -2359,7 +2614,6 @@ testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2369,40 +2623,50 @@ files = [
 
 [[package]]
 name = "torch"
-version = "1.13.1"
+version = "2.0.0"
 description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration"
-category = "main"
 optional = false
-python-versions = ">=3.7.0"
-files = [
-    {file = "torch-1.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:fd12043868a34a8da7d490bf6db66991108b00ffbeecb034228bfcbbd4197143"},
-    {file = "torch-1.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d9fe785d375f2e26a5d5eba5de91f89e6a3be5d11efb497e76705fdf93fa3c2e"},
-    {file = "torch-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:98124598cdff4c287dbf50f53fb455f0c1e3a88022b39648102957f3445e9b76"},
-    {file = "torch-1.13.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:393a6273c832e047581063fb74335ff50b4c566217019cc6ace318cd79eb0566"},
-    {file = "torch-1.13.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:0122806b111b949d21fa1a5f9764d1fd2fcc4a47cb7f8ff914204fd4fc752ed5"},
-    {file = "torch-1.13.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:22128502fd8f5b25ac1cd849ecb64a418382ae81dd4ce2b5cebaa09ab15b0d9b"},
-    {file = "torch-1.13.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:76024be052b659ac1304ab8475ab03ea0a12124c3e7626282c9c86798ac7bc11"},
-    {file = "torch-1.13.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ea8dda84d796094eb8709df0fcd6b56dc20b58fdd6bc4e8d7109930dafc8e419"},
-    {file = "torch-1.13.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2ee7b81e9c457252bddd7d3da66fb1f619a5d12c24d7074de91c4ddafb832c93"},
-    {file = "torch-1.13.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:0d9b8061048cfb78e675b9d2ea8503bfe30db43d583599ae8626b1263a0c1380"},
-    {file = "torch-1.13.1-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:f402ca80b66e9fbd661ed4287d7553f7f3899d9ab54bf5c67faada1555abde28"},
-    {file = "torch-1.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:727dbf00e2cf858052364c0e2a496684b9cb5aa01dc8a8bc8bbb7c54502bdcdd"},
-    {file = "torch-1.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:df8434b0695e9ceb8cc70650afc1310d8ba949e6db2a0525ddd9c3b2b181e5fe"},
-    {file = "torch-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:5e1e722a41f52a3f26f0c4fcec227e02c6c42f7c094f32e49d4beef7d1e213ea"},
-    {file = "torch-1.13.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:33e67eea526e0bbb9151263e65417a9ef2d8fa53cbe628e87310060c9dcfa312"},
-    {file = "torch-1.13.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:eeeb204d30fd40af6a2d80879b46a7efbe3cf43cdbeb8838dd4f3d126cc90b2b"},
-    {file = "torch-1.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:50ff5e76d70074f6653d191fe4f6a42fdbe0cf942fbe2a3af0b75eaa414ac038"},
-    {file = "torch-1.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2c3581a3fd81eb1f0f22997cddffea569fea53bafa372b2c0471db373b26aafc"},
-    {file = "torch-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:0aa46f0ac95050c604bcf9ef71da9f1172e5037fdf2ebe051962d47b123848e7"},
-    {file = "torch-1.13.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6930791efa8757cb6974af73d4996b6b50c592882a324b8fb0589c6a9ba2ddaf"},
-    {file = "torch-1.13.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e0df902a7c7dd6c795698532ee5970ce898672625635d885eade9976e5a04949"},
+python-versions = ">=3.8.0"
+files = [
+    {file = "torch-2.0.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:7a9319a67294ef02459a19738bbfa8727bb5307b822dadd708bc2ccf6c901aca"},
+    {file = "torch-2.0.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:9f01fe1f6263f31bd04e1757946fd63ad531ae37f28bb2dbf66f5c826ee089f4"},
+    {file = "torch-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:527f4ae68df7b8301ee6b1158ca56350282ea633686537b30dbb5d7b4a52622a"},
+    {file = "torch-2.0.0-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:ce9b5a49bd513dff7950a5a07d6e26594dd51989cee05ba388b03e8e366fd5d5"},
+    {file = "torch-2.0.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:53e1c33c6896583cdb9a583693e22e99266444c4a43392dddc562640d39e542b"},
+    {file = "torch-2.0.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:09651bff72e439d004c991f15add0c397c66f98ab36fe60d5514b44e4da722e8"},
+    {file = "torch-2.0.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d439aec349c98f12819e8564b8c54008e4613dd4428582af0e6e14c24ca85870"},
+    {file = "torch-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:2802f84f021907deee7e9470ed10c0e78af7457ac9a08a6cd7d55adef835fede"},
+    {file = "torch-2.0.0-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:01858620f25f25e7a9ec4b547ff38e5e27c92d38ec4ccba9cfbfb31d7071ed9c"},
+    {file = "torch-2.0.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:9a2e53b5783ef5896a6af338b36d782f28e83c8ddfc2ac44b67b066d9d76f498"},
+    {file = "torch-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ec5fff2447663e369682838ff0f82187b4d846057ef4d119a8dea7772a0b17dd"},
+    {file = "torch-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11b0384fe3c18c01b8fc5992e70fc519cde65e44c51cc87be1838c1803daf42f"},
+    {file = "torch-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:e54846aa63855298cfb1195487f032e413e7ac9cbfa978fda32354cc39551475"},
+    {file = "torch-2.0.0-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:cc788cbbbbc6eb4c90e52c550efd067586c2693092cf367c135b34893a64ae78"},
+    {file = "torch-2.0.0-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:d292640f0fd72b7a31b2a6e3b635eb5065fcbedd4478f9cad1a1e7a9ec861d35"},
+    {file = "torch-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6befaad784004b7af357e3d87fa0863c1f642866291f12a4c2af2de435e8ac5c"},
+    {file = "torch-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a83b26bd6ae36fbf5fee3d56973d9816e2002e8a3b7d9205531167c28aaa38a7"},
+    {file = "torch-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c7e67195e1c3e33da53954b026e89a8e1ff3bc1aeb9eb32b677172d4a9b5dcbf"},
+    {file = "torch-2.0.0-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6e0b97beb037a165669c312591f242382e9109a240e20054d5a5782d9236cad0"},
+    {file = "torch-2.0.0-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:297a4919aff1c0f98a58ebe969200f71350a1d4d4f986dbfd60c02ffce780e99"},
 ]
 
 [package.dependencies]
-nvidia-cublas-cu11 = {version = "11.10.3.66", markers = "platform_system == \"Linux\""}
-nvidia-cuda-nvrtc-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""}
-nvidia-cuda-runtime-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""}
-nvidia-cudnn-cu11 = {version = "8.5.0.96", markers = "platform_system == \"Linux\""}
+filelock = "*"
+jinja2 = "*"
+networkx = "*"
+nvidia-cublas-cu11 = {version = "11.10.3.66", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-cupti-cu11 = {version = "11.7.101", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-nvrtc-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cuda-runtime-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cudnn-cu11 = {version = "8.5.0.96", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cufft-cu11 = {version = "10.9.0.58", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-curand-cu11 = {version = "10.2.10.91", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cusolver-cu11 = {version = "11.4.0.1", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-cusparse-cu11 = {version = "11.7.4.91", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-nccl-cu11 = {version = "2.14.3", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+nvidia-nvtx-cu11 = {version = "11.7.91", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
+sympy = "*"
+triton = {version = "2.0.0", markers = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
 typing-extensions = "*"
 
 [package.extras]
@@ -2412,7 +2676,6 @@ opt-einsum = ["opt-einsum (>=3.3)"]
 name = "torchmetrics"
 version = "0.11.4"
 description = "PyTorch native Metrics"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2439,7 +2702,6 @@ text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)"]
 name = "tqdm"
 version = "4.65.0"
 description = "Fast, Extensible Progress Meter"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2460,7 +2722,6 @@ telegram = ["requests"]
 name = "traitlets"
 version = "5.9.0"
 description = "Traitlets Python configuration system"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2474,19 +2735,18 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
 
 [[package]]
 name = "transformers"
-version = "4.25.1"
+version = "4.28.1"
 description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow"
-category = "main"
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "transformers-4.25.1-py3-none-any.whl", hash = "sha256:60f1be15e17e4a54373c787c713ec149dabcc63464131ac45611618fe7c2016e"},
-    {file = "transformers-4.25.1.tar.gz", hash = "sha256:6dad398b792d45dc04e9ee7e9e06bf758ab19dca2efc119065e661bb0f8f843b"},
+    {file = "transformers-4.28.1-py3-none-any.whl", hash = "sha256:f30a006220d0475789ac0e7c874f51bf5143956797616d89975b637883ce0be6"},
+    {file = "transformers-4.28.1.tar.gz", hash = "sha256:7334f8730cff7ac31d9ba5c12f2113fcb7a7a5b61eeb5dbbdb162117c3aaa2d1"},
 ]
 
 [package.dependencies]
 filelock = "*"
-huggingface-hub = ">=0.10.0,<1.0"
+huggingface-hub = ">=0.11.0,<1.0"
 numpy = ">=1.17"
 packaging = ">=20.0"
 protobuf = {version = "<=3.20.2", optional = true, markers = "extra == \"sentencepiece\""}
@@ -2499,28 +2759,28 @@ tqdm = ">=4.27"
 
 [package.extras]
 accelerate = ["accelerate (>=0.10.0)"]
-all = ["Pillow", "accelerate (>=0.10.0)", "codecarbon (==1.2.0)", "flax (>=0.4.1)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.2)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.11)", "tensorflow-text", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.7,!=1.12.0)", "torchaudio"]
+all = ["Pillow", "accelerate (>=0.10.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.2)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision"]
 audio = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
 codecarbon = ["codecarbon (==1.2.0)"]
-deepspeed = ["accelerate (>=0.10.0)", "deepspeed (>=0.6.5)"]
-deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.10.0)", "beautifulsoup4", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.6.5)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "optuna", "parameterized", "protobuf (<=3.20.2)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "timeout-decorator"]
-dev = ["GitPython (<3.1.19)", "Pillow", "accelerate (>=0.10.0)", "beautifulsoup4", "black (==22.3)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "flax (>=0.4.1)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pyknp (>=0.6.1)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorflow (>=2.4,<2.11)", "tensorflow-text", "tf2onnx", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.7,!=1.12.0)", "torchaudio", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
-dev-tensorflow = ["GitPython (<3.1.19)", "Pillow", "beautifulsoup4", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>=2.4,<2.11)", "tensorflow-text", "tf2onnx", "timeout-decorator", "tokenizers (>=0.11.1,!=0.11.3,<0.14)"]
-dev-torch = ["GitPython (<3.1.19)", "Pillow", "beautifulsoup4", "black (==22.3)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flake8 (>=3.8.3)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pyknp (>=0.6.1)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.7,!=1.12.0)", "torchaudio", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
-docs = ["Pillow", "accelerate (>=0.10.0)", "codecarbon (==1.2.0)", "flax (>=0.4.1)", "hf-doc-builder", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.2)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.11)", "tensorflow-text", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.7,!=1.12.0)", "torchaudio"]
+deepspeed = ["accelerate (>=0.10.0)", "deepspeed (>=0.8.3)"]
+deepspeed-testing = ["GitPython (<3.1.19)", "accelerate (>=0.10.0)", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "deepspeed (>=0.8.3)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "optuna", "parameterized", "protobuf (<=3.20.2)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "sentencepiece (>=0.1.91,!=0.1.92)", "timeout-decorator"]
+dev = ["GitPython (<3.1.19)", "Pillow", "accelerate (>=0.10.0)", "av (==9.2.0)", "beautifulsoup4", "black (>=23.1,<24.0)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "decord (==0.6.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "flax (>=0.4.1)", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rhoknp (>=1.1.0)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+dev-tensorflow = ["GitPython (<3.1.19)", "Pillow", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "nltk", "onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timeout-decorator", "tokenizers (>=0.11.1,!=0.11.3,<0.14)"]
+dev-torch = ["GitPython (<3.1.19)", "Pillow", "beautifulsoup4", "black (>=23.1,<24.0)", "codecarbon (==1.2.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "fugashi (>=1.0)", "hf-doc-builder", "hf-doc-builder (>=0.3.0)", "ipadic (>=1.0.0,<2.0)", "isort (>=5.5.4)", "kenlm", "librosa", "nltk", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "optuna", "parameterized", "phonemizer", "protobuf (<=3.20.2)", "psutil", "pyctcdecode (>=0.4.0)", "pytest", "pytest-timeout", "pytest-xdist", "ray[tune]", "rhoknp (>=1.1.0)", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "ruff (>=0.0.241,<=0.0.259)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "scikit-learn", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "timeout-decorator", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+docs = ["Pillow", "accelerate (>=0.10.0)", "av (==9.2.0)", "codecarbon (==1.2.0)", "decord (==0.6.0)", "flax (>=0.4.1)", "hf-doc-builder", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "kenlm", "keras-nlp (>=0.3.1)", "librosa", "onnxconverter-common", "optax (>=0.0.8)", "optuna", "phonemizer", "protobuf (<=3.20.2)", "pyctcdecode (>=0.4.0)", "ray[tune]", "sentencepiece (>=0.1.91,!=0.1.92)", "sigopt", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx", "timm", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "torchaudio", "torchvision"]
 docs-specific = ["hf-doc-builder"]
 fairscale = ["fairscale (>0.3)"]
 flax = ["flax (>=0.4.1)", "jax (>=0.2.8,!=0.3.2,<=0.3.6)", "jaxlib (>=0.1.65,<=0.3.6)", "optax (>=0.0.8)"]
 flax-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
 ftfy = ["ftfy"]
 integrations = ["optuna", "ray[tune]", "sigopt"]
-ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "pyknp (>=0.6.1)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
+ja = ["fugashi (>=1.0)", "ipadic (>=1.0.0,<2.0)", "rhoknp (>=1.1.0)", "sudachidict-core (>=20220729)", "sudachipy (>=0.6.6)", "unidic (>=1.0.2)", "unidic-lite (>=1.0.7)"]
 modelcreation = ["cookiecutter (==1.7.3)"]
-natten = ["natten (>=0.14.4)"]
+natten = ["natten (>=0.14.6)"]
 onnx = ["onnxconverter-common", "onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)", "tf2onnx"]
 onnxruntime = ["onnxruntime (>=1.4.0)", "onnxruntime-tools (>=1.4.2)"]
 optuna = ["optuna"]
-quality = ["GitPython (<3.1.19)", "black (==22.3)", "datasets (!=2.5.0)", "flake8 (>=3.8.3)", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)"]
+quality = ["GitPython (<3.1.19)", "black (>=23.1,<24.0)", "datasets (!=2.5.0)", "hf-doc-builder (>=0.3.0)", "isort (>=5.5.4)", "ruff (>=0.0.241,<=0.0.259)"]
 ray = ["ray[tune]"]
 retrieval = ["datasets (!=2.5.0)", "faiss-cpu"]
 sagemaker = ["sagemaker (>=2.31.0)"]
@@ -2529,71 +2789,120 @@ serving = ["fastapi", "pydantic", "starlette", "uvicorn"]
 sigopt = ["sigopt"]
 sklearn = ["scikit-learn"]
 speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"]
-testing = ["GitPython (<3.1.19)", "beautifulsoup4", "black (==22.3)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "parameterized", "protobuf (<=3.20.2)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "timeout-decorator"]
-tf = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow (>=2.4,<2.11)", "tensorflow-text", "tf2onnx"]
-tf-cpu = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow-cpu (>=2.4,<2.11)", "tensorflow-text", "tf2onnx"]
+testing = ["GitPython (<3.1.19)", "beautifulsoup4", "black (>=23.1,<24.0)", "cookiecutter (==1.7.3)", "datasets (!=2.5.0)", "dill (<0.3.5)", "evaluate (>=0.2.0)", "faiss-cpu", "hf-doc-builder (>=0.3.0)", "nltk", "parameterized", "protobuf (<=3.20.2)", "psutil", "pytest", "pytest-timeout", "pytest-xdist", "rjieba", "rouge-score (!=0.0.7,!=0.0.8,!=0.1,!=0.1.1)", "sacrebleu (>=1.4.12,<2.0.0)", "sacremoses", "safetensors (>=0.2.1)", "timeout-decorator"]
+tf = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx"]
+tf-cpu = ["keras-nlp (>=0.3.1)", "onnxconverter-common", "tensorflow-cpu (>=2.4,<2.13)", "tensorflow-text (<2.13)", "tf2onnx"]
 tf-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)"]
 timm = ["timm"]
 tokenizers = ["tokenizers (>=0.11.1,!=0.11.3,<0.14)"]
-torch = ["torch (>=1.7,!=1.12.0)"]
+torch = ["torch (>=1.9,!=1.12.0)"]
 torch-speech = ["kenlm", "librosa", "phonemizer", "pyctcdecode (>=0.4.0)", "torchaudio"]
-torchhub = ["filelock", "huggingface-hub (>=0.10.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf (<=3.20.2)", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.7,!=1.12.0)", "tqdm (>=4.27)"]
+torch-vision = ["Pillow", "torchvision"]
+torchhub = ["filelock", "huggingface-hub (>=0.11.0,<1.0)", "importlib-metadata", "numpy (>=1.17)", "packaging (>=20.0)", "protobuf (<=3.20.2)", "regex (!=2019.12.17)", "requests", "sentencepiece (>=0.1.91,!=0.1.92)", "tokenizers (>=0.11.1,!=0.11.3,<0.14)", "torch (>=1.9,!=1.12.0)", "tqdm (>=4.27)"]
+video = ["av (==9.2.0)", "decord (==0.6.0)"]
 vision = ["Pillow"]
 
+[[package]]
+name = "triton"
+version = "2.0.0"
+description = "A language and compiler for custom Deep Learning operations"
+optional = false
+python-versions = "*"
+files = [
+    {file = "triton-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f05a7e64e4ca0565535e3d5d3405d7e49f9d308505bb7773d21fb26a4c008c2"},
+    {file = "triton-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4b99ca3c6844066e516658541d876c28a5f6e3a852286bbc97ad57134827fd"},
+    {file = "triton-2.0.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b4d70dc92fb40af553b4460492c31dc7d3a114a979ffb7a5cdedb7eb546c08"},
+    {file = "triton-2.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fedce6a381901b1547e0e7e1f2546e4f65dca6d91e2d8a7305a2d1f5551895be"},
+    {file = "triton-2.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75834f27926eab6c7f00ce73aaf1ab5bfb9bec6eb57ab7c0bfc0a23fac803b4c"},
+    {file = "triton-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0117722f8c2b579cd429e0bee80f7731ae05f63fe8e9414acd9a679885fcbf42"},
+    {file = "triton-2.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcd9be5d0c2e45d2b7e6ddc6da20112b6862d69741576f9c3dbaf941d745ecae"},
+    {file = "triton-2.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42a0d2c3fc2eab4ba71384f2e785fbfd47aa41ae05fa58bf12cb31dcbd0aeceb"},
+    {file = "triton-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c47b72c72693198163ece9d90a721299e4fb3b8e24fd13141e384ad952724f"},
+]
+
+[package.dependencies]
+cmake = "*"
+filelock = "*"
+lit = "*"
+torch = "*"
+
+[package.extras]
+tests = ["autopep8", "flake8", "isort", "numpy", "pytest", "scipy (>=1.7.1)"]
+tutorials = ["matplotlib", "pandas", "tabulate"]
+
 [[package]]
 name = "typer"
-version = "0.7.0"
+version = "0.9.0"
 description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"},
-    {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"},
+    {file = "typer-0.9.0-py3-none-any.whl", hash = "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee"},
+    {file = "typer-0.9.0.tar.gz", hash = "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2"},
 ]
 
 [package.dependencies]
 click = ">=7.1.1,<9.0.0"
+typing-extensions = ">=3.7.4.3"
 
 [package.extras]
-all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
 dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
 doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
-test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
+test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<14.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
 
 [[package]]
 name = "types-attrs"
 version = "19.1.0"
 description = "Typing stubs for attrs"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
     {file = "types_attrs-19.1.0-py2.py3-none-any.whl", hash = "sha256:d11acf7a2531a7c52a740c30fa3eb8d01d3066c10d34c01ff5e59502caac5352"},
 ]
 
+[[package]]
+name = "types-pyyaml"
+version = "6.0.12.10"
+description = "Typing stubs for PyYAML"
+optional = false
+python-versions = "*"
+files = [
+    {file = "types-PyYAML-6.0.12.10.tar.gz", hash = "sha256:ebab3d0700b946553724ae6ca636ea932c1b0868701d4af121630e78d695fc97"},
+    {file = "types_PyYAML-6.0.12.10-py3-none-any.whl", hash = "sha256:662fa444963eff9b68120d70cda1af5a5f2aa57900003c2006d7626450eaae5f"},
+]
+
 [[package]]
 name = "typing-extensions"
-version = "4.5.0"
+version = "4.6.2"
 description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
-    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+    {file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"},
+    {file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2023.3"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+    {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
+    {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
 ]
 
 [[package]]
 name = "urllib3"
-version = "1.26.15"
+version = "1.26.16"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
-    {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
-    {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
+    {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"},
+    {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"},
 ]
 
 [package.extras]
@@ -2603,14 +2912,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 
 [[package]]
 name = "wandb"
-version = "0.13.11"
+version = "0.15.3"
 description = "A CLI and library for interacting with the Weights and Biases API."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "wandb-0.13.11-py3-none-any.whl", hash = "sha256:e584ac7fb0097fba8a030d5aedc5fea157286aba69a2e6e6e689b794155fcc8f"},
-    {file = "wandb-0.13.11.tar.gz", hash = "sha256:f2b065d1c732e8e1828fabe720dc82f12e6109d0d2bbfbcccc17eaefae0fd7c9"},
+    {file = "wandb-0.15.3-py3-none-any.whl", hash = "sha256:f9e0b8cd53a3564dc8e54967c251860bd023f0ee2a450c4407fe97f771dcfa9e"},
+    {file = "wandb-0.15.3.tar.gz", hash = "sha256:f9325901e0dff8e9a471d4a3e2102aa7bc26c7171afaf21a978ffb2cc7df4b3c"},
 ]
 
 [package.dependencies]
@@ -2621,8 +2929,8 @@ GitPython = ">=1.0.0,<3.1.29 || >3.1.29"
 pathtools = "*"
 protobuf = [
     {version = ">=3.12.0,<4.21.0 || >4.21.0,<5", markers = "python_version < \"3.9\" and sys_platform == \"linux\""},
-    {version = ">=3.15.0,<4.21.0 || >4.21.0,<5", markers = "python_version == \"3.9\" and sys_platform == \"linux\""},
     {version = ">=3.19.0,<4.21.0 || >4.21.0,<5", markers = "python_version > \"3.9\" or sys_platform != \"linux\""},
+    {version = ">=3.15.0,<4.21.0 || >4.21.0,<5", markers = "python_version == \"3.9\" and sys_platform == \"linux\""},
 ]
 psutil = ">=5.0.0"
 PyYAML = "*"
@@ -2633,8 +2941,9 @@ setuptools = "*"
 typing-extensions = {version = "*", markers = "python_version < \"3.10\""}
 
 [package.extras]
+async = ["httpx (>=0.22.0)"]
 aws = ["boto3"]
-azure = ["azure-storage-blob"]
+azure = ["azure-identity", "azure-storage-blob"]
 gcp = ["google-cloud-storage"]
 grpc = ["grpcio (>=1.27.2)"]
 kubeflow = ["google-cloud-storage", "kubernetes", "minio", "sh"]
@@ -2647,7 +2956,6 @@ sweeps = ["sweeps (>=0.2.0)"]
 name = "wcwidth"
 version = "0.2.6"
 description = "Measures the displayed width of unicode strings in a terminal"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -2657,101 +2965,99 @@ files = [
 
 [[package]]
 name = "wheel"
-version = "0.38.4"
+version = "0.40.0"
 description = "A built-package format for Python"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"},
-    {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"},
+    {file = "wheel-0.40.0-py3-none-any.whl", hash = "sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247"},
+    {file = "wheel-0.40.0.tar.gz", hash = "sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873"},
 ]
 
 [package.extras]
-test = ["pytest (>=3.0.0)"]
+test = ["pytest (>=6.0.0)"]
 
 [[package]]
 name = "yarl"
-version = "1.8.2"
+version = "1.9.2"
 description = "Yet another URL library"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5"},
-    {file = "yarl-1.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863"},
-    {file = "yarl-1.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe"},
-    {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e"},
-    {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996"},
-    {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef"},
-    {file = "yarl-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8"},
-    {file = "yarl-1.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6"},
-    {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146"},
-    {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f"},
-    {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826"},
-    {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3"},
-    {file = "yarl-1.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80"},
-    {file = "yarl-1.8.2-cp310-cp310-win32.whl", hash = "sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42"},
-    {file = "yarl-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574"},
-    {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634"},
-    {file = "yarl-1.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd"},
-    {file = "yarl-1.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581"},
-    {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a"},
-    {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf"},
-    {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee"},
-    {file = "yarl-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76"},
-    {file = "yarl-1.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b"},
-    {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a"},
-    {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6"},
-    {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4"},
-    {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c"},
-    {file = "yarl-1.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2"},
-    {file = "yarl-1.8.2-cp311-cp311-win32.whl", hash = "sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b"},
-    {file = "yarl-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c"},
-    {file = "yarl-1.8.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef"},
-    {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1"},
-    {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077"},
-    {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2"},
-    {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89"},
-    {file = "yarl-1.8.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c"},
-    {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0"},
-    {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739"},
-    {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b"},
-    {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7"},
-    {file = "yarl-1.8.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37"},
-    {file = "yarl-1.8.2-cp37-cp37m-win32.whl", hash = "sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89"},
-    {file = "yarl-1.8.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5"},
-    {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1"},
-    {file = "yarl-1.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918"},
-    {file = "yarl-1.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229"},
-    {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e"},
-    {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e"},
-    {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1"},
-    {file = "yarl-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3"},
-    {file = "yarl-1.8.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901"},
-    {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0"},
-    {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05"},
-    {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d"},
-    {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c"},
-    {file = "yarl-1.8.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946"},
-    {file = "yarl-1.8.2-cp38-cp38-win32.whl", hash = "sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165"},
-    {file = "yarl-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f"},
-    {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8"},
-    {file = "yarl-1.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf"},
-    {file = "yarl-1.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77"},
-    {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87"},
-    {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0"},
-    {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2"},
-    {file = "yarl-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08"},
-    {file = "yarl-1.8.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d"},
-    {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951"},
-    {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220"},
-    {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06"},
-    {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516"},
-    {file = "yarl-1.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588"},
-    {file = "yarl-1.8.2-cp39-cp39-win32.whl", hash = "sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83"},
-    {file = "yarl-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778"},
-    {file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"},
+    {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"},
+    {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"},
+    {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"},
+    {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"},
+    {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"},
+    {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"},
+    {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"},
+    {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"},
+    {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"},
+    {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"},
+    {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"},
+    {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"},
+    {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"},
+    {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"},
+    {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"},
+    {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"},
+    {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"},
+    {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"},
+    {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"},
+    {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"},
+    {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"},
+    {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"},
+    {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"},
+    {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"},
+    {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"},
+    {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"},
+    {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"},
+    {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"},
+    {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"},
+    {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"},
+    {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"},
+    {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"},
+    {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"},
+    {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"},
+    {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"},
+    {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"},
+    {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"},
+    {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"},
+    {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"},
+    {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"},
+    {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"},
+    {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"},
+    {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"},
+    {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"},
+    {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"},
+    {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"},
+    {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"},
+    {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"},
+    {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"},
+    {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"},
+    {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"},
+    {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"},
+    {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"},
+    {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"},
+    {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"},
+    {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"},
+    {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"},
+    {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"},
+    {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"},
+    {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"},
+    {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"},
+    {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"},
+    {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"},
+    {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"},
+    {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"},
+    {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"},
+    {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"},
+    {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"},
+    {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"},
+    {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"},
+    {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"},
+    {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"},
+    {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"},
+    {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"},
 ]
 
 [package.dependencies]
@@ -2762,7 +3068,6 @@ multidict = ">=4.0"
 name = "zipp"
 version = "3.15.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -2776,5 +3081,5 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 
 [metadata]
 lock-version = "2.0"
-python-versions = ">=3.8,<3.11"
-content-hash = "6a31bb2d055323362c83d4434285495a8dbb1728efba8ac91bd928053fabea61"
+python-versions = "^3.8"
+content-hash = "ea71feb77117543eaa30ebdf80553c5ffaf0a593c8c1a55aeaa751f76b8bb53b"
diff --git a/pyproject.toml b/pyproject.toml
index 03ad265a..e2fa5817 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "kwja"
-version = "2.0.0"
+version = "2.1.0"
 description = "A unified language analyzer for Japanese"
 authors = [
     "Hirokazu Kiyomaru <kiyomaru@i.kyoto-u.ac.jp>",
@@ -30,6 +30,7 @@ classifiers = [
     "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
     "Topic :: Scientific/Engineering",
     "Topic :: Text Processing",
 ]
@@ -40,34 +41,48 @@ packages = [
 include = [
     "kwja/resource/**/*"
 ]
+exclude = [
+    "src/kwja/metrics/**/*"
+]
 
 [tool.poetry.dependencies]
-python = ">=3.8,<3.11"
-torch = "^1.11"
-transformers = {version = "~4.25.1", extras = ["sentencepiece"]}
+python = "^3.8"
+torch = ">=1.11,!=2.0.1"  # https://github.com/pytorch/pytorch/issues/100974
+transformers = { version = "~4.28.1", extras = ["sentencepiece"]}
 tokenizers = "^0.13.2"
-pytorch-lightning = "~1.9.0"
-torchmetrics = "^0.11.3"
+pytorch-lightning = "~2.0.0"  # As "lightning" comes with many extra dependencies, we stay with "pytorch-lightning"
 omegaconf = "^2.1"
 hydra-core = "^1.2"
 rhoknp = "^1.1.0"
-typer = ">=0.6.1,<0.8.0"
-seqeval = "^1.2"
-python-dotenv = "^1.0.0"
-pandas = "^1.4"
+typer = ">=0.7.0,<0.10.0"
 jinf = "^1.0.4"
-importlib-resources = "^5.10"
-jaconv = "^0.3"
 pure-cdb = "^4.0"
 rich = ">=12.4"
-Levenshtein = "^0.20.8"
+pyyaml = "^6.0"
+importlib-resources = { version = "^5.10", python = "<3.9" }
 
 [tool.poetry.group.dev.dependencies]
-ipdb = "^0.13.9"
-types-attrs = "^19.1.0"
-wandb = "^0.13.3"
+torchmetrics = "^0.11.4"
+seqeval = "^1.2"
+Levenshtein = "^0.21.0"
+pandas = "^2.0"
+python-dotenv = "^1.0"
+jaconv = "^0.3.4"
+ipython = [
+    { version = "~8.12.1", python = "<3.9" },
+    { version = "^8.13", python = ">=3.9" },
+]
+ipdb = "^0.13.13"
+types-attrs = "^19.1"
+types-pyyaml = "^6.0.12.9"
+wandb = "^0.15.0"
 
 [tool.poetry.group.test.dependencies]
+torchmetrics = "^0.11.4"
+seqeval = "^1.2"
+Levenshtein = "^0.21.0"
+pandas = "^2.0"
+jaconv = "^0.3.4"
 pytest = "^7.1"
 pytest-cov = "^4.0"
 pytest-profiling = "^1.7"
@@ -77,7 +92,7 @@ kwja = "kwja.cli.cli:app"
 
 [tool.black]
 line-length = 120
-target-version = ["py38", "py39", "py310"]
+target-version = ["py38", "py39", "py310", "py311"]
 
 [tool.isort]
 line_length = 120
diff --git a/scripts/analyze.py b/scripts/analyze.py
index a4155812..2cce302d 100644
--- a/scripts/analyze.py
+++ b/scripts/analyze.py
@@ -5,30 +5,31 @@
 
 import hydra
 import pytorch_lightning as pl
+import torch
 from dotenv import load_dotenv
 from omegaconf import DictConfig, ListConfig, OmegaConf
 from pytorch_lightning.callbacks import Callback
 from pytorch_lightning.trainer.states import TrainerFn
 
-from kwja.cli.utils import suppress_debug_info
 from kwja.datamodule.datamodule import DataModule
+from kwja.utils.logging_util import filter_logs
 
-suppress_debug_info()
+filter_logs(environment="production")
 os.environ["TOKENIZERS_PARALLELISM"] = "false"
-OmegaConf.register_new_resolver("concat", lambda x, y: x + y)
 
 
 @hydra.main(version_base=None, config_path="../configs", config_name="eval")
 def main(eval_cfg: DictConfig):
     load_dotenv()
     if isinstance(eval_cfg.devices, str):
-        try:
-            eval_cfg.devices = [int(x) for x in eval_cfg.devices.split(",")]
-        except ValueError:
-            eval_cfg.devices = None
+        eval_cfg.devices = (
+            list(map(int, eval_cfg.devices.split(","))) if "," in eval_cfg.devices else int(eval_cfg.devices)
+        )
 
     # Load saved model and config
     model: pl.LightningModule = hydra.utils.call(eval_cfg.module.load_from_checkpoint, _recursive_=False)
+    if eval_cfg.compile is True:
+        model = torch.compile(model)  # type: ignore
 
     train_cfg: DictConfig = model.hparams
     OmegaConf.set_struct(train_cfg, False)  # enable to add new key-value pairs
@@ -46,7 +47,7 @@ def main(eval_cfg: DictConfig):
 
     trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         callbacks=callbacks,
         devices=cfg.devices,
     )
@@ -55,14 +56,14 @@ def main(eval_cfg: DictConfig):
         cfg.datamodule.predict.knp_file = Path(cfg.datamodule.predict.knp_file)
     elif getattr(cfg.datamodule.predict, "juman_file", None):
         cfg.datamodule.predict.juman_file = Path(cfg.datamodule.predict.juman_file)
-    elif not cfg.datamodule.predict.texts:
-        cfg.datamodule.predict.texts = sys.stdin.read().splitlines()
+    elif getattr(cfg.datamodule.predict, "senter_file", None):
+        cfg.datamodule.predict.senter_file = Path(cfg.datamodule.predict.senter_file)
+    elif getattr(cfg.datamodule.predict, "texts", None) is not None:
+        cfg.datamodule.predict.texts = sys.stdin.readlines()
     datamodule = DataModule(cfg=cfg.datamodule)
     datamodule.setup(stage=TrainerFn.PREDICTING)
 
-    if getattr(cfg, "load_only", False):
-        sys.exit(0)
-    trainer.predict(model=model, dataloaders=datamodule.predict_dataloader(), return_predictions=False)
+    trainer.predict(model=model, dataloaders=[datamodule.predict_dataloader()], return_predictions=False)
 
 
 if __name__ == "__main__":
diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh
index bdd56610..dcf7d6d8 100755
--- a/scripts/benchmark.sh
+++ b/scripts/benchmark.sh
@@ -2,21 +2,40 @@
 
 DEVICE="[0]"
 TYPO_BATCH_SIZE=1
+SENTER_BATCH_SIZE=1
 CHAR_BATCH_SIZE=1
 WORD_BATCH_SIZE=1
 
 usage() {
-  echo "Usage: ./scripts/benchmark.sh --input=\${INPUT} --work-dir=\${WORK_DIR} --typo-module=\${TYPO_MODULE} --char-module=\${CHAR_MODULE} --word-module=\${WORD_MODULE} --device=${DEVICE} --typo-batch-size=${TYPO_BATCH_SIZE} --char-batch-size=${CHAR_BATCH_SIZE} --word-batch-size=${WORD_BATCH_SIZE}"
-  echo "*** NOTE: specify arguments with \"=\" ***"
-  echo "  --input              path to input text"
-  echo "  --work-dir           path to working directory"
-  echo "  --typo-module        path to fine-tuned typo module"
-  echo "  --char-module        path to fine-tuned char module"
-  echo "  --word-module        path to fine-tuned word module"
-  echo "  --device             device (default=[0])"
-  echo "  --typo-batch-size    max_batches_per_device of typo module (default=1)"
-  echo "  --char-batch-size    max_batches_per_device of char module (default=1)"
-  echo "  --word-batch-size    max_batches_per_device of word module (default=1)"
+  cat << _EOT_
+Usage:
+  ./scripts/benchmark.sh
+    --input=<INPUT>
+    --work-dir=<WORK_DIR>
+    --typo-module=<TYPO_MODULE>
+    --senter-module=<SENTER_MODULE>
+    --char-module=<CHAR_MODULE>
+    --word-module=<WORD_MODULE>
+    [--device=<DEVICE>]
+    [--typo-batch-size=<TYPO_BATCH_SIZE>]
+    [--senter-batch-size=<SENTER_BATCH_SIZE>]
+    [--char-batch-size=<CHAR_BATCH_SIZE>]
+    [--word-batch-size=<WORD_BATCH_SIZE>]
+  *** NOTE: specify arguments with \"=\" ***
+
+Options:
+  --input                path to input text"
+  --work-dir             path to working directory"
+  --typo-module          path to fine-tuned typo module"
+  --senter-module        path to fine-tuned senter module"
+  --char-module          path to fine-tuned char module"
+  --word-module          path to fine-tuned word module"
+  --device               device (default=[0])"
+  --typo-batch-size      max_batches_per_device of typo module (default=1)"
+  --senter-batch-size    max_batches_per_device of senter module (default=1)"
+  --char-batch-size      max_batches_per_device of char module (default=1)"
+  --word-batch-size      max_batches_per_device of word module (default=1)"
+_EOT_
 }
 
 while getopts h-: opt; do
@@ -35,6 +54,9 @@ while getopts h-: opt; do
   typo-module)
     TYPO_MODULE=$OPTARG
     ;;
+  senter-module)
+    SENTER_MODULE=$OPTARG
+    ;;
   char-module)
     CHAR_MODULE=$OPTARG
     ;;
@@ -47,6 +69,9 @@ while getopts h-: opt; do
   typo-batch-size)
     TYPO_BATCH_SIZE=$OPTARG
     ;;
+  senter-batch-size)
+    SENTER_BATCH_SIZE=$OPTARG
+    ;;
   char-batch-size)
     CHAR_BATCH_SIZE=$OPTARG
     ;;
@@ -64,7 +89,7 @@ while getopts h-: opt; do
   esac
 done
 
-if [[ -z "$INPUT" ]] || [[ -z "$WORK_DIR" ]] || [[ -z "$TYPO_MODULE" ]] || [[ -z "$CHAR_MODULE" ]] || [[ -z "$WORD_MODULE" ]]; then
+if [[ -z "$INPUT" ]] || [[ -z "$WORK_DIR" ]] || [[ -z "$TYPO_MODULE" ]] || [[ -z "$SENTER_MODULE" ]] || [[ -z "$CHAR_MODULE" ]] || [[ -z "$WORD_MODULE" ]]; then
   echo "missing required arguments"
   usage
   exit 1
@@ -91,18 +116,18 @@ echo "Juman++ & KNP (NER + Dependency parsing + PAS analysis)"
 grep "# S-ID:" "$WORK_DIR/benchmark.knp_ne_anaphora.knp" | grep -cv "ERROR:" >> "$WORK_DIR/count.txt"
 
 echo "KWJA (typo_module)"
-(time -p cat "$INPUT" | poetry run python scripts/analyze.py module=typo checkpoint_path="$TYPO_MODULE" devices="$DEVICE" max_batches_per_device="$TYPO_BATCH_SIZE" +load_only=true) 2>> "$WORK_DIR/benchmark.stderr"
-(time -p cat "$INPUT" | poetry run python scripts/analyze.py module=typo checkpoint_path="$TYPO_MODULE" devices="$DEVICE" max_batches_per_device="$TYPO_BATCH_SIZE" > "$WORK_DIR/benchmark.kwja.txt") 2>> "$WORK_DIR/benchmark.stderr"
-grep "# S-ID:" "$WORK_DIR/benchmark.kwja.txt" | cut -f -3 -d "-" | uniq | wc -l >> "$WORK_DIR/count.txt"
+(time -p cat "$INPUT" | poetry run python ./scripts/analyze.py module=typo checkpoint_path="$TYPO_MODULE" devices="$DEVICE" max_batches_per_device="$TYPO_BATCH_SIZE" > "$WORK_DIR/benchmark.kwja.txt") 2>> "$WORK_DIR/benchmark.stderr"
+
+echo "KWJA (senter_module)"
+(time -p cat "$INPUT" | poetry run python ./scripts/analyze.py module=senter checkpoint_path="$SENTER_MODULE" devices="$DEVICE" max_batches_per_device="$SENTER_BATCH_SIZE" > "$WORK_DIR/benchmark.kwja.senter") 2>> "$WORK_DIR/benchmark.stderr"
+grep "# S-ID:" "$WORK_DIR/benchmark.kwja.senter" | cut -f -3 -d "-" | uniq | wc -l >> "$WORK_DIR/count.txt"
 
 echo "KWJA (char_module)"
-(time -p cat "$INPUT" | poetry run python scripts/analyze.py module=char checkpoint_path="$CHAR_MODULE" devices="$DEVICE" max_batches_per_device="$CHAR_BATCH_SIZE" +load_only=true) 2>> "$WORK_DIR/benchmark.stderr"
-(time -p cat "$INPUT" | poetry run python scripts/analyze.py module=char checkpoint_path="$CHAR_MODULE" devices="$DEVICE" max_batches_per_device="$CHAR_BATCH_SIZE" > "$WORK_DIR/benchmark.kwja.juman") 2>> "$WORK_DIR/benchmark.stderr"
+(time -p poetry run python ./scripts/analyze.py module=char checkpoint_path="$CHAR_MODULE" devices="$DEVICE" max_batches_per_device="$CHAR_BATCH_SIZE" +datamodule.predict.senter_file="$WORK_DIR/benchmark.kwja.senter" > "$WORK_DIR/benchmark.kwja.juman") 2>> "$WORK_DIR/benchmark.stderr"
 grep "# S-ID:" "$WORK_DIR/benchmark.kwja.juman" | cut -f -3 -d "-" | uniq | wc -l >> "$WORK_DIR/count.txt"
 
 echo "KWJA (word_module)"
-(time -p poetry run python scripts/analyze.py module=word checkpoint_path="$WORD_MODULE" devices="$DEVICE" max_batches_per_device="$WORD_BATCH_SIZE" +datamodule.predict.juman_file="$WORK_DIR/benchmark.kwja.juman" +load_only=true) 2>> "$WORK_DIR/benchmark.stderr"
-(time -p poetry run python scripts/analyze.py module=word checkpoint_path="$WORD_MODULE" devices="$DEVICE" max_batches_per_device="$WORD_BATCH_SIZE" +datamodule.predict.juman_file="$WORK_DIR/benchmark.kwja.juman" > "$WORK_DIR/benchmark.kwja.knp") 2>> "$WORK_DIR/benchmark.stderr"
+(time -p poetry run python ./scripts/analyze.py module=word checkpoint_path="$WORD_MODULE" devices="$DEVICE" max_batches_per_device="$WORD_BATCH_SIZE" +datamodule.predict.juman_file="$WORK_DIR/benchmark.kwja.juman" > "$WORK_DIR/benchmark.kwja.knp") 2>> "$WORK_DIR/benchmark.stderr"
 grep "# S-ID:" "$WORK_DIR/benchmark.kwja.knp" | cut -f -3 -d "-" | uniq | wc -l >> "$WORK_DIR/count.txt"
 
 grep '^real' "$WORK_DIR/benchmark.stderr" > "$WORK_DIR/time.txt"
diff --git a/scripts/build_dataset.py b/scripts/build_dataset.py
index e29fb5e5..5b5e9f16 100644
--- a/scripts/build_dataset.py
+++ b/scripts/build_dataset.py
@@ -13,7 +13,7 @@
 from rhoknp.utils.reader import chunk_by_document, chunk_by_sentence
 
 from kwja.utils.constants import BASE_PHRASE_FEATURES, IGNORE_VALUE_FEATURE_PAT, SUB_WORD_FEATURES
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 
 class JumanppAugmenter:
@@ -65,13 +65,21 @@ def _create_partial_input(sentence: Sentence) -> str:
     def _postprocess_sentence(
         original_sentence: Sentence, augmented_sentence: Sentence, update_original: bool = True
     ) -> None:
-        for original_morpheme, augmented_morpheme in zip(original_sentence.morphemes, augmented_sentence.morphemes):
-            # Jumanpp may override reading
-            augmented_morpheme.reading = original_morpheme.reading
-            if update_original and not original_sentence.need_knp:
-                # add Semantics
-                for k, v in augmented_morpheme.semantics.items():
-                    original_morpheme.semantics[k] = v
+        alignment = align_morphemes(original_sentence.morphemes, augmented_sentence.morphemes)
+        if alignment is None:
+            return None
+        keys = []
+        for original_morpheme in original_sentence.morphemes:
+            keys.append(str(original_morpheme.index))
+            if "-".join(keys) in alignment:
+                aligned = alignment["-".join(keys)]
+                if len(keys) == 1 and len(aligned) == 1:
+                    augmented_morpheme = aligned[0]
+                    # Jumanpp may override reading
+                    augmented_morpheme.reading = original_morpheme.reading
+                    if update_original and not original_sentence.need_knp:
+                        original_morpheme.semantics.update(augmented_morpheme.semantics)
+                keys = []
 
 
 def align_morphemes(morphemes1: List[Morpheme], morphemes2: List[Morpheme]) -> Optional[Dict[str, List[Morpheme]]]:
@@ -428,14 +436,17 @@ def main():
 
     output_root = Path(args.OUTPUT)
     doc_id2split = {}
-    for input_file in Path(args.id).glob("*.id"):
-        if input_file.stem not in {"train", "dev", "test"}:
-            continue
-        split = "valid" if input_file.stem == "dev" else input_file.stem
+    for id_file in Path(args.id).glob("*.id"):
+        if output_root.parts[-1] == "kyoto_ed":
+            if id_file.stem != "all":
+                continue
+        else:
+            if id_file.stem not in {"train", "dev", "test"}:
+                continue
+        split = "valid" if id_file.stem == "dev" else id_file.stem
         output_root.joinpath(split).mkdir(parents=True, exist_ok=True)
-        with input_file.open(mode="r") as f:
-            for line in f:
-                doc_id2split[line.strip()] = split
+        for doc_id in id_file.read_text().splitlines():
+            doc_id2split[doc_id] = split
 
     chunk_size = len(knp_texts) // args.j + int(len(knp_texts) % args.j > 0)
     iterable = [
diff --git a/scripts/build_datasets.sh b/scripts/build_datasets.sh
index dc90f32d..5caa8c15 100755
--- a/scripts/build_datasets.sh
+++ b/scripts/build_datasets.sh
@@ -3,11 +3,15 @@
 JOBS=1
 
 usage() {
-  echo "Usage: ./scripts/build_dataset.sh --work-dir=\${WORK_DIR} --out-dir=\${OUT_DIR} --jobs=${JOBS}"
-  echo "*** NOTE: specify arguments with \"=\" ***"
-  echo "  --work-dir    path to working directory"
-  echo "  --out-dir     path to output directory"
-  echo "  --jobs        number of jobs (default=1)"
+  cat << _EOT_
+Usage:
+  ./scripts/build_dataset.sh --out-dir=<OUT_DIR> [--jobs=<JOBS>]
+  *** NOTE: specify arguments with "=" ***
+
+Options:
+  --out-dir     path to output directory
+  --jobs        number of jobs (default=1)
+_EOT_
 }
 
 while getopts h-: opt; do
@@ -17,9 +21,6 @@ while getopts h-: opt; do
   fi
 
   case "$opt" in
-  work-dir)
-    WORK_DIR=$OPTARG
-    ;;
   out-dir)
     OUT_DIR=$OPTARG
     ;;
@@ -38,18 +39,22 @@ while getopts h-: opt; do
   esac
 done
 
-if [[ -z "$WORK_DIR" ]] || [[ -z "$OUT_DIR" ]]; then
+if [[ -z "$OUT_DIR" ]]; then
   echo "missing required arguments"
   usage
   exit 1
 fi
 
+WORK_DIR="$(mktemp -d)"
+
 mkdir -p "$WORK_DIR" "$OUT_DIR"/{kwdlc,fuman}
-git clone git@github.com:ku-nlp/KWDLC.git "$WORK_DIR"/KWDLC
-git clone git@github.com:ku-nlp/AnnotatedFKCCorpus.git "$WORK_DIR"/AnnotatedFKCCorpus
+git clone --depth 1 git@github.com:ku-nlp/KWDLC.git "$WORK_DIR"/KWDLC
+git clone --depth 1 git@github.com:ku-nlp/AnnotatedFKCCorpus.git "$WORK_DIR"/AnnotatedFKCCorpus
 poetry run python ./scripts/build_dataset.py "$WORK_DIR"/KWDLC/knp "$OUT_DIR"/kwdlc \
   --id "$WORK_DIR"/KWDLC/id/split_for_pas \
   -j "$JOBS"
 poetry run python ./scripts/build_dataset.py "$WORK_DIR"/AnnotatedFKCCorpus/knp "$OUT_DIR"/fuman \
   --id "$WORK_DIR"/AnnotatedFKCCorpus/id/split_for_pas \
   -j "$JOBS"
+
+rm -rf "$WORK_DIR"
diff --git a/scripts/deploy_dev_checkpoints.sh b/scripts/deploy_dev_checkpoints.sh
new file mode 100755
index 00000000..8b786f3c
--- /dev/null
+++ b/scripts/deploy_dev_checkpoints.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+
+set -euxo pipefail
+
+usage() {
+  cat << _EOT_
+Usage:
+  $0 remote_host remote_path
+
+Description:
+  Train and deploy checkpoints for development.
+_EOT_
+  exit 1
+}
+
+find_latest_checkpoint() {
+  fd --extension ckpt --full-path . "$1" | sort -r | head -n 1
+  return 0
+}
+
+if [[ $# -ne 2 ]]; then
+  usage
+fi
+
+for task in typo senter char seq2seq word; do
+  train_extra_args=("ignore_hparams_on_save=true" "trainer=cpu.debug" "do_predict_after_train=false")
+  module="${task}"
+  checkpoint_dir="result/${module}_module.debug-ignore_hparams_on_save_true-trainer_cpu.debug"
+
+  poetry run python scripts/train.py -cn ${module}_module.debug "${train_extra_args[@]}"
+
+  checkpoint_path="$(find_latest_checkpoint "${checkpoint_dir}")"
+  if [[ -z "${checkpoint_path}" ]]; then
+    echo "Checkpoint not found in ${checkpoint_dir}" >&2
+    exit 1
+  fi
+
+  if [[ "${module}" == "typo" ]]; then
+    pretrained_model_name="deberta-v2-tiny-wwm"
+  elif [[ "${module}" == "senter" ]]; then
+    pretrained_model_name="deberta-v2-base-wwm"
+  elif [[ "${module}" == "char" ]]; then
+    pretrained_model_name="deberta-v2-tiny-wwm"
+  elif [[ "${module}" == "seq2seq" ]]; then
+    pretrained_model_name="mt5-small"
+  elif [[ "${module}" == "word" ]]; then
+    pretrained_model_name="deberta-v2-tiny"
+  else
+    echo "Unknown module: ${module}" >&2
+    exit 1
+  fi
+  rsync -ahPmv "${checkpoint_path}" "$1:$2/dev/${task}_${pretrained_model_name}.ckpt"
+done
diff --git a/scripts/eval_jumanpp_knp.py b/scripts/eval_jumanpp_knp.py
new file mode 100644
index 00000000..1abb3792
--- /dev/null
+++ b/scripts/eval_jumanpp_knp.py
@@ -0,0 +1,109 @@
+import sys
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+from rhoknp import KNP, Document, Sentence
+from rhoknp.props import MemoTag
+
+from kwja.metrics.word import WordModuleMetric
+
+CORPORA: Tuple[str, ...] = ("kwdlc", "kyoto", "fuman", "wac")
+
+
+def evaluate_docs(documents: List[Document]):
+    word_module_metric = WordModuleMetric()
+    knp = KNP(options=["-tab", "-dpnd"])
+    (
+        gold_documents,
+        partly_gold_documents1,
+        partly_gold_documents2,
+        predicted_documents,
+    ) = (
+        [],
+        [],
+        [],
+        [],
+    )
+    for document in documents:
+        parsed_sentences: List[Sentence] = []
+        for sentence in document.sentences:
+            parsed_sentence = knp.apply_to_sentence(sentence.text)
+            parsed_sentence.sid = sentence.sid
+            parsed_sentence.did = sentence.did
+            parsed_sentences.append(parsed_sentence)
+        try:
+            predicted_document = Document.from_sentences(parsed_sentences)
+        except AttributeError:
+            continue
+
+        partly_gold_document1 = document.reparse()
+        partly_gold_document1.did = document.did
+        refresh(partly_gold_document1, level=1)
+        try:
+            partly_gold_document1 = knp.apply_to_document(partly_gold_document1)
+        except ValueError:
+            continue
+
+        partly_gold_document2 = document.reparse()
+        partly_gold_document2.did = document.did
+        refresh(partly_gold_document2, level=2)
+        partly_gold_document2 = knp.apply_to_document(partly_gold_document2)
+
+        gold_documents.append(document)
+        partly_gold_documents1.append(partly_gold_document1)
+        partly_gold_documents2.append(partly_gold_document2)
+        predicted_documents.append(predicted_document)
+
+    metrics: Dict[str, float] = {}
+    metrics.update(word_module_metric.compute_reading_prediction_metrics(predicted_documents, gold_documents))
+    # metrics.update(word_module_metric.compute_morphological_analysis_metrics(predicted_documents, gold_documents))
+    # metrics.update(word_module_metric.compute_word_feature_tagging_metrics(predicted_documents, gold_documents))
+    metrics.update(word_module_metric.compute_ner_metrics(partly_gold_documents1, gold_documents))
+    metrics.update(
+        word_module_metric.compute_base_phrase_feature_tagging_metrics(partly_gold_documents1, gold_documents)
+    )
+    metrics.update(word_module_metric.compute_dependency_parsing_metrics(partly_gold_documents1, gold_documents))
+    # metrics.update(word_module_metric.compute_cohesion_analysis_metrics(partly_gold_documents2, gold_documents))
+    return metrics, len(gold_documents)
+
+
+def refresh(document: Document, level: int = 1) -> None:
+    """Refresh document
+
+    NOTE:
+        level1: clear discourse relations, rel tags, dependencies, and base phrase features.
+        level2: clear discourse relations and rel tags.
+    """
+    assert level in (1, 2), f"invalid level: {level}"
+    try:
+        for clause in document.clauses:
+            clause.discourse_relations.clear()
+    except AttributeError:
+        pass
+
+    for base_phrase in document.base_phrases:
+        base_phrase.rel_tags.clear()
+        base_phrase.memo_tag = MemoTag()
+        if level == 1:
+            base_phrase.features.clear()
+            base_phrase.parent_index = None
+            base_phrase.dep_type = None
+
+    for phrase in document.phrases:
+        if level == 1:
+            phrase.features.clear()
+            phrase.parent_index = None
+            phrase.dep_type = None
+
+
+def main():
+    for corpus in CORPORA:
+        knp_dir = Path(sys.argv[1]) / corpus / "test"
+        metrics, num_docs = evaluate_docs(
+            [Document.from_knp(knp_path.read_text()) for knp_path in knp_dir.glob("*.knp")]
+        )
+        print(corpus, num_docs, metrics)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/predict.py b/scripts/predict.py
index d1e154d0..e602aa06 100644
--- a/scripts/predict.py
+++ b/scripts/predict.py
@@ -1,40 +1,31 @@
-import logging
-import warnings
 from typing import List
 
 import hydra
 import pytorch_lightning as pl
-import transformers.utils.logging as hf_logging
+import torch
 from dotenv import load_dotenv
-from lightning_fabric.utilities.warnings import PossibleUserWarning
 from omegaconf import DictConfig, ListConfig, OmegaConf
 from pytorch_lightning.callbacks import Callback
 from pytorch_lightning.trainer.states import TrainerFn
 
 from kwja.datamodule.datamodule import DataModule
+from kwja.utils.logging_util import filter_logs
 
-hf_logging.set_verbosity(hf_logging.ERROR)
-logging.getLogger("rhoknp").setLevel(logging.ERROR)
-warnings.filterwarnings(
-    "ignore",
-    message=r"It is recommended to use .+ when logging on epoch level in distributed setting to accumulate the metric"
-    r" across devices",
-    category=PossibleUserWarning,
-)
-OmegaConf.register_new_resolver("concat", lambda x, y: x + y)
+filter_logs(environment="development")
 
 
 @hydra.main(version_base=None, config_path="../configs", config_name="eval")
 def main(eval_cfg: DictConfig):
     load_dotenv()
     if isinstance(eval_cfg.devices, str):
-        try:
-            eval_cfg.devices = [int(x) for x in eval_cfg.devices.split(",")]
-        except ValueError:
-            eval_cfg.devices = None
+        eval_cfg.devices = (
+            list(map(int, eval_cfg.devices.split(","))) if "," in eval_cfg.devices else int(eval_cfg.devices)
+        )
 
     # Load saved model and config
     model: pl.LightningModule = hydra.utils.call(eval_cfg.module.load_from_checkpoint, _recursive_=False)
+    if eval_cfg.compile is True:
+        model = torch.compile(model)  # type: ignore
 
     train_cfg: DictConfig = model.hparams
     OmegaConf.set_struct(train_cfg, False)  # enable to add new key-value pairs
@@ -52,7 +43,7 @@ def main(eval_cfg: DictConfig):
 
     trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         callbacks=callbacks,
         devices=cfg.devices,
     )
diff --git a/scripts/preprocessors/preprocess_jumandic.py b/scripts/preprocessors/preprocess_jumandic.py
index 0035a60d..bb8cd44c 100644
--- a/scripts/preprocessors/preprocess_jumandic.py
+++ b/scripts/preprocessors/preprocess_jumandic.py
@@ -5,7 +5,7 @@
 from pathlib import Path
 
 from kwja.utils.jumandic import JumanDic
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 logger = logging.getLogger(__name__)
 logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s: %(message)s", level=logging.DEBUG)
diff --git a/scripts/preprocessors/preprocess_norm.py b/scripts/preprocessors/preprocess_norm.py
new file mode 100644
index 00000000..b9259ab5
--- /dev/null
+++ b/scripts/preprocessors/preprocess_norm.py
@@ -0,0 +1,97 @@
+import random
+import re
+import shutil
+from argparse import ArgumentParser
+from pathlib import Path
+from typing import Dict, List
+
+from jinf import Jinf
+from rhoknp import KNP, Document, Sentence
+from tqdm import tqdm
+
+
+def is_hiragana(value):
+    return re.match(r"^[\u3040-\u309F]+$", value) is not None
+
+
+def main():
+    parser = ArgumentParser()
+    parser.add_argument("-i", "--input-path", type=str, required=True)
+    parser.add_argument("-o", "--output-dir", type=str, required=True)
+    args = parser.parse_args()
+
+    random.seed(42)
+
+    knp = KNP()
+    jinf = Jinf()
+
+    sid2knp_str: Dict[str, str] = {}
+    num_errors: int = 0
+    with Path(args.input_path).open() as f:
+        document: Document = Document.from_jumanpp(f.read())
+        for sentence in tqdm(document.sentences):
+            for morpheme in sentence.morphemes:
+                if "代表表記" not in morpheme.semantics:
+                    if morpheme.conjtype == "*":
+                        canon: str = f"{morpheme.lemma}/{morpheme.reading}"
+                    elif is_hiragana(morpheme.lemma):
+                        canon = f"{morpheme.lemma}/{morpheme.lemma}"
+                    else:
+                        canon_right: str = jinf(morpheme.reading, morpheme.conjtype, morpheme.conjform, "基本形")
+                        canon = f"{morpheme.lemma}/{canon_right}"
+                    morpheme.semantics["代表表記"] = canon
+            try:
+                knp_applied_sentence: Sentence = knp.apply_to_sentence(sentence)
+            except ValueError:
+                num_errors += 1
+                continue
+            try:
+                sid2knp_str[knp_applied_sentence.sid] = knp_applied_sentence.to_knp()
+            except AttributeError:
+                num_errors += 1
+                continue
+
+    sid2knp_str_list: List[tuple[str, str]] = list(sid2knp_str.items())
+    random.shuffle(sid2knp_str_list)
+    train_size: int = int(len(sid2knp_str_list) * 0.9)
+    valid_size: int = int(len(sid2knp_str_list) * 0.05)
+    train_list: List[tuple[str, str]] = sid2knp_str_list[:train_size]
+    valid_list: List[tuple[str, str]] = sid2knp_str_list[train_size : train_size + valid_size]
+    test_list: List[tuple[str, str]] = sid2knp_str_list[train_size + valid_size :]
+
+    output_dir: Path = Path(args.output_dir)
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    train_dir: Path = output_dir / "train"
+    if train_dir.exists():
+        shutil.rmtree(str(train_dir))
+    train_dir.mkdir(exist_ok=True)
+    for name, knp_str in train_list:
+        with (train_dir / f"{name}.knp").open("w") as f:
+            f.write(f"{knp_str}\n")
+
+    valid_dir: Path = output_dir / "valid"
+    if valid_dir.exists():
+        shutil.rmtree(str(valid_dir))
+    valid_dir.mkdir(exist_ok=True)
+    for name, knp_str in valid_list:
+        with (valid_dir / f"{name}.knp").open("w") as f:
+            f.write(f"{knp_str}\n")
+
+    test_dir: Path = output_dir / "test"
+    if test_dir.exists():
+        shutil.rmtree(str(test_dir))
+    test_dir.mkdir(exist_ok=True)
+    for name, knp_str in test_list:
+        with (test_dir / f"{name}.knp").open("w") as f:
+            f.write(f"{knp_str}\n")
+
+    print(f"train: {len(train_list)}")
+    print(f"valid: {len(valid_list)}")
+    print(f"test: {len(test_list)}")
+    print(f"total: {len(train_list) + len(valid_list) + len(test_list)}")
+    print(f"num_errors: {num_errors}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/train.py b/scripts/train.py
index 9b149b65..261af839 100644
--- a/scripts/train.py
+++ b/scripts/train.py
@@ -1,46 +1,33 @@
-import logging
 import math
-import warnings
-from typing import List, Optional
+from typing import List, Union
 
 import hydra
 import pytorch_lightning as pl
-import transformers.utils.logging as hf_logging
+import torch
 import wandb
 from dotenv import load_dotenv
-from omegaconf import DictConfig, ListConfig, OmegaConf
+from omegaconf import DictConfig, ListConfig
 from pytorch_lightning.callbacks import Callback
 from pytorch_lightning.loggers import Logger
-from pytorch_lightning.utilities.warnings import PossibleUserWarning
 
 from kwja.datamodule.datamodule import DataModule
+from kwja.utils.logging_util import filter_logs
 
-hf_logging.set_verbosity(hf_logging.ERROR)
-logging.getLogger("rhoknp").setLevel(logging.ERROR)
-warnings.filterwarnings(
-    "ignore",
-    message=r"It is recommended to use .+ when logging on epoch level in distributed setting to accumulate the metric"
-    r" across devices",
-    category=PossibleUserWarning,
-)
-OmegaConf.register_new_resolver("concat", lambda x, y: x + y)
+filter_logs(environment="development")
 
 
 @hydra.main(version_base=None, config_path="../configs")
 def main(cfg: DictConfig):
     load_dotenv()
     if isinstance(cfg.devices, str):
-        try:
-            cfg.devices = [int(x) for x in cfg.devices.split(",")]
-        except ValueError:
-            cfg.devices = None
+        cfg.devices = list(map(int, cfg.devices.split(","))) if "," in cfg.devices else int(cfg.devices)
     if isinstance(cfg.max_batches_per_device, str):
         cfg.max_batches_per_device = int(cfg.max_batches_per_device)
     if isinstance(cfg.num_workers, str):
         cfg.num_workers = int(cfg.num_workers)
     cfg.seed = pl.seed_everything(seed=cfg.seed, workers=True)
 
-    logger: Optional[Logger] = cfg.get("logger") and hydra.utils.instantiate(cfg.get("logger"))
+    logger: Union[Logger, bool] = cfg.get("logger", False) and hydra.utils.instantiate(cfg.get("logger"))
     callbacks: List[Callback] = list(map(hydra.utils.instantiate, cfg.get("callbacks", {}).values()))
 
     # Calculate gradient_accumulation_steps assuming DDP
@@ -69,6 +56,8 @@ def main(cfg: DictConfig):
     datamodule = DataModule(cfg=cfg.datamodule)
 
     model: pl.LightningModule = hydra.utils.instantiate(cfg.module.cls, hparams=cfg, _recursive_=False)
+    if cfg.compile is True:
+        model = torch.compile(model)  # type: ignore
 
     trainer.fit(model=model, datamodule=datamodule)
     trainer.test(model=model, datamodule=datamodule, ckpt_path="best" if not trainer.fast_dev_run else None)
diff --git a/scripts/view_seq2seq_results.py b/scripts/view_seq2seq_results.py
index de0c579c..6bee1adf 100644
--- a/scripts/view_seq2seq_results.py
+++ b/scripts/view_seq2seq_results.py
@@ -1,14 +1,13 @@
-import copy
 import difflib
 import logging
 import re
 from argparse import ArgumentParser
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Dict, List, Tuple, cast
+from typing import Dict, List, Set, Tuple, cast
 
 import jaconv
-from rhoknp import Document, Sentence
+from rhoknp import Document, Jumanpp, Morpheme, Sentence
 
 logging.getLogger("rhoknp").setLevel(logging.ERROR)
 
@@ -18,13 +17,11 @@
 
 @dataclass(frozen=True)
 class DiffType:
-    # equal, surf, {reading, lemma, pos, subpos}は排他的．
+    # equal, surf, {reading, lemma, canon}は排他的．
     equal: bool = False
     surf: bool = False
     reading: bool = False
     lemma: bool = False
-    pos: bool = False
-    subpos: bool = False
     canon: bool = False
 
 
@@ -35,12 +32,12 @@ def __init__(self, diff_text: str):
         self.diff_part = self._separate_parts(diff_text)
 
     def __repr__(self):
-        return f"{self.surf}_{self.reading}_{self.lemma}_{self.pos}_{self.subpos}_{self.canon}"
+        return f"{self.surf}_{self.reading}_{self.lemma}_{self.canon}"
 
-    def _separate_parts(self, text: str) -> Tuple[str, str, str, str, str, str]:
+    def _separate_parts(self, text: str) -> Tuple[str, str, str, str]:
         if text.startswith("! ") or text.startswith("+ ") or text.startswith("- "):
             self.has_diff = True
-        return cast(Tuple[str, str, str, str, str, str], tuple(text[2:].split("_")))
+        return cast(Tuple[str, str, str, str], tuple(text[2:].split("_")))
 
     @property
     def surf(self) -> str:
@@ -54,17 +51,9 @@ def reading(self) -> str:
     def lemma(self) -> str:
         return self.diff_part[2]
 
-    @property
-    def pos(self) -> str:
-        return self.diff_part[3]
-
-    @property
-    def subpos(self) -> str:
-        return self.diff_part[4]
-
     @property
     def canon(self) -> str:
-        return self.diff_part[5]
+        return self.diff_part[3]
 
 
 class Diff(object):
@@ -91,28 +80,32 @@ def append(self, diff_type: DiffType, sys_parts, gold_parts) -> None:
 
 
 class MorphologicalAnalysisScorer:
-    def __init__(self, sys_sentences: List[Sentence], gold_sentences: List[Sentence]) -> None:
-        self.tp: Dict[str, int] = dict(surf=0, reading=0, lemma=0, pos=0, subpos=0, canon=0)
-        self.fp: Dict[str, int] = dict(surf=0, reading=0, lemma=0, pos=0, subpos=0, canon=0)
-        self.fn: Dict[str, int] = dict(surf=0, reading=0, lemma=0, pos=0, subpos=0, canon=0)
-
-        self.num_same_texts: int = 0
+    def __init__(self, sys_sentences: List[Sentence], gold_sentences: List[Sentence], eval_norm: bool = False) -> None:
+        self.eval_norm: bool = eval_norm
+        self.tp: Dict[str, int] = dict(surf=0, reading=0, lemma=0, canon=0)
+        self.fp: Dict[str, int] = dict(surf=0, reading=0, lemma=0, canon=0)
+        self.fn: Dict[str, int] = dict(surf=0, reading=0, lemma=0, canon=0)
+        self.sys_sentences: List[Sentence] = sys_sentences
+        self.gold_sentences: List[Sentence] = gold_sentences
+
+        self.num_diff_texts: int = 0
+        self.norm_types: Set[str] = set()
         self.diffs: List[Diff] = self._search_diffs(sys_sentences, gold_sentences)
 
-    @staticmethod
-    def _convert(sentence: Sentence) -> List[str]:
+    def _convert(self, sentence: Sentence, norm_morphemes: List[Morpheme]) -> List[str]:
         converteds: List[str] = []
+        norm_surfs: Set[str] = set(mrph.surf for mrph in norm_morphemes)
         for mrph in sentence.morphemes:
             surf: str = jaconv.h2z(mrph.surf.replace("<unk>", "$"), ascii=True, digit=True)
+            if self.eval_norm and surf not in norm_surfs:
+                continue
             reading: str = jaconv.h2z(mrph.reading.replace("<unk>", "$"), ascii=True, digit=True)
             lemma: str = jaconv.h2z(mrph.lemma.replace("<unk>", "$"), ascii=True, digit=True)
-            pos: str = jaconv.h2z(mrph.pos.replace("<unk>", "$"), ascii=True, digit=True)
-            subpos: str = jaconv.h2z(mrph.subpos.replace("<unk>", "$"), ascii=True, digit=True)
             if mrph.canon is None or mrph.canon == "None":
                 canon: str = f"{lemma}／{reading}"
             else:
                 canon = jaconv.h2z(mrph.canon.replace("<unk>", "$"), ascii=True, digit=True)
-            converteds.append(f"{surf}_{reading}_{lemma}_{pos}_{subpos}_{canon}")
+            converteds.append(f"{surf}_{reading}_{lemma}_{canon}")
         return converteds
 
     def _search_diff(self, pred: List[str], gold: List[str]) -> Diff:
@@ -138,9 +131,18 @@ def _search_diff(self, pred: List[str], gold: List[str]) -> Diff:
 
         sys_idx: int = 0
         gold_idx: int = 0
-        sys_len = len(pred)
+        sys_len: int = len(pred)
         gold_len: int = len(gold)
 
+        if sys_len == 0:
+            for g in gold:
+                diff.append(DiffType(surf=True), [], [g])
+            return diff
+        elif gold_len == 0:
+            for p in pred:
+                diff.append(DiffType(surf=True), [p], [])
+            return diff
+
         while sys_idx < sys_len and gold_idx < gold_len:
             if len(pred_with_diff) == 0:
                 if gold_with_diff[gold_idx].has_diff:
@@ -183,8 +185,6 @@ def _search_diff(self, pred: List[str], gold: List[str]) -> Diff:
                             "surf": False,
                             "reading": False,
                             "lemma": False,
-                            "pos": False,
-                            "subpos": False,
                             "canon": False,
                         }
                         if pred_diff_part.reading != gold_diff_part.reading:
@@ -194,12 +194,6 @@ def _search_diff(self, pred: List[str], gold: List[str]) -> Diff:
                         if pred_diff_part.canon != gold_diff_part.canon:
                             diff_types["canon"] = True
 
-                        if pred_diff_part.pos == "未定義語" and gold_diff_part.pos == "名詞":
-                            diff_types["subpos"] = True
-                        elif pred_diff_part.pos != gold_diff_part.pos:
-                            diff_types["pos"] = True
-                        elif pred_diff_part.subpos != gold_diff_part.subpos:
-                            diff_types["subpos"] = True
                         pred_diff_idx += 1
                         gold_diff_idx += 1
                         diff.append(DiffType(**diff_types), [pred_diff_part], [gold_diff_part])
@@ -238,59 +232,47 @@ def _search_diff(self, pred: List[str], gold: List[str]) -> Diff:
 
         pred_text: str = "".join(x.split("_")[0] for x in pred)
         gold_text: str = "".join(x.split("_")[0] for x in gold)
-        if pred_text == gold_text:
-            self.num_same_texts += 1
-        # else:
-        #     print(f"{pred_text = }")
-        #     print(f"{gold_text = }")
+        if pred_text != gold_text:
+            self.num_diff_texts += 1
+            # print(f"{pred_text = }")
+            # print(f"{gold_text = }")
         return diff
 
     def _search_diffs(self, sys_sentences: List[Sentence], gold_sentences: List[Sentence]) -> List[Diff]:
         diffs = []
         for sys_sentence, gold_sentence in zip(sys_sentences, gold_sentences):
-            diff: Diff = self._search_diff(self._convert(sys_sentence), self._convert(gold_sentence))
+            norm_morphemes: List[Morpheme] = []
+            if self.eval_norm:
+                for gold_morpheme in gold_sentence.morphemes:
+                    if "非標準表記" in gold_morpheme.semantics:
+                        norm_morphemes.append(gold_morpheme)
+            sys_converted: List[str] = self._convert(sys_sentence, norm_morphemes)
+            gold_converted: List[str] = self._convert(gold_sentence, norm_morphemes)
+            diff: Diff = self._search_diff(sys_converted, gold_converted)
             diffs.append(diff)
         return diffs
 
     def compute_score(self, is_simple_output: bool = True) -> None:
         for diff in self.diffs:
             for p in diff:
+                correct_keys: Set[str] = set()
                 if p["diff_type"].equal:
-                    true_keys: List[str] = ["surf", "pos", "subpos", "reading", "lemma", "canon"]
-                    false_keys: List[str] = []
-                elif p["diff_type"].surf:
-                    true_keys = []
-                    false_keys = ["surf", "pos", "subpos", "reading", "lemma", "canon"]
-                else:
-                    true_keys = ["surf"]
-                    false_keys = []
-                    if p["diff_type"].reading:
-                        false_keys.append("reading")
-                    else:
-                        true_keys.append("reading")
-                    if p["diff_type"].lemma:
-                        false_keys.append("lemma")
+                    correct_keys.update({"surf", "reading", "lemma", "canon"})
+                elif not p["diff_type"].surf:
+                    correct_keys.add("surf")
+                    if not p["diff_type"].reading:
+                        correct_keys.add("reading")
+                    if not p["diff_type"].lemma:
+                        correct_keys.add("lemma")
+                    if not p["diff_type"].canon:
+                        correct_keys.add("canon")
+
+                for key in ["surf", "reading", "lemma", "canon"]:
+                    if key in correct_keys:
+                        self.tp[key] += len(p["sys_parts"])
                     else:
-                        true_keys.append("lemma")
-                    if p["diff_type"].canon:
-                        false_keys.append("canon")
-                    else:
-                        true_keys.append("canon")
-                    if p["diff_type"].pos:
-                        false_keys.append("pos")
-                        false_keys.append("subpos")
-                    elif p["diff_type"].subpos:
-                        true_keys.append("pos")
-                        false_keys.append("subpos")
-                    else:
-                        true_keys.append("pos")
-                        true_keys.append("subpos")
-                    assert len(true_keys) + len(false_keys) == 6
-                for key in true_keys:
-                    self.tp[key] += len(p["sys_parts"])
-                for key in false_keys:
-                    self.fp[key] += len(p["sys_parts"])
-                    self.fn[key] += len(p["gold_parts"])
+                        self.fp[key] += len(p["sys_parts"])
+                        self.fn[key] += len(p["gold_parts"])
 
         keys: List[str] = ["surf", "reading", "lemma", "canon"]
         outputs: List[str] = [" / ".join(keys)]
@@ -307,154 +289,80 @@ def compute_score(self, is_simple_output: bool = True) -> None:
         print("    " + " = ".join(outputs))
 
 
-def format_juman(input_text: str) -> Tuple[str, bool]:
-    lines: List[str] = input_text.split("\n")
-    mrph_placeholder: List[str] = ["@", "@", "@", "未定義語", "15", "その他", "1", "*", "0", "*", "0", "NIL"]
-    output_text: str = ""
-    contain_dummy: bool = False
-    for line in lines:
-        if not line:
-            continue
-        if line == "EOS" or line.startswith("*") or line.startswith("+"):
-            output_text += line + "\n"
-        else:
-            preds: List[str] = line.split(" ")
-            if len(preds) == 4:
-                mrphs: List[str] = copy.deepcopy(mrph_placeholder)
-                for idx in range(3):
-                    mrphs[idx] = preds[idx]
-                mrphs[-1] = f'"代表表記:{preds[3]}"' if preds[3] is not None else "NIL"
-                output_text += " ".join(mrphs) + "\n"
-            elif line in ["!!!!/!", "????/?", ",,,,/,"]:
-                mrphs = copy.deepcopy(mrph_placeholder)
-                for idx in range(3):
-                    mrphs[idx] = line[idx]
-                mrphs[-1] = f'"代表表記:{line[-1]}/{line[-1]}"'
-                output_text += " ".join(mrphs) + "\n"
-            elif line == "............/...":
-                mrphs = copy.deepcopy(mrph_placeholder)
-                for idx in range(3):
-                    mrphs[idx] = "…"
-                mrphs[-1] = '"代表表記:…/…"'
-                output_text += " ".join(mrphs) + "\n"
-            else:
-                contain_dummy = True
-                output_text += " ".join(mrph_placeholder) + "\n"
-    if "*" not in output_text:
-        output_text = "*\n+\n" + output_text
-    if not output_text.endswith("EOS\n"):
-        output_text += "EOS\n"
-    return output_text, contain_dummy
-
-
 def main():
     parser = ArgumentParser()
-    parser.add_argument("-d", "--dataset-dir", type=str, required=True)
-    parser.add_argument("-i", "--input-file", type=str, required=True)
-    parser.add_argument("-ck", "--compare-kwja", action="store_true")
+    parser.add_argument("--dataset-dir", type=str, required=True)
+    parser.add_argument("--seq2seq-file", type=str, required=True)
+    parser.add_argument("--output-dir", type=str, default="./outputs")
     args = parser.parse_args()
 
-    pred_dicts: Dict[str, List[Dict]] = dict()
-    corpusid2text = {"0": "kyoto", "1": "kwdlc", "2": "fuman"}
-    input_text: str = ""
-    generated_text: str = ""
-    corpus: str = ""
-    with Path(args.input_file).open() as f:
-        for line in f:
-            if line[:3] == "src":
-                corpus: str = corpusid2text[line[3]]
-                input_text: str = line.strip()[6:]
-            elif line == "EOS\n":
-                generated_text += line
-                if corpus not in pred_dicts:
-                    pred_dicts[corpus] = []
-                pred_dicts[corpus].append({"text": input_text, "generated": generated_text})
-                input_text = ""
-                generated_text = ""
-            else:
-                generated_text += line
-
-    kwja_results: Dict[str, Dict[str, Sentence]] = dict()
-    if args.compare_kwja:
-        for corpus in ["kyoto", "kwdlc", "fuman"]:
-            kwja_results[corpus] = dict()
-            with Path(f"{OUTPUT_DIR}/predict_kwja/{corpus}_test.knp").open() as f:
-                lines = [line for line in f]
-            buffer: str = ""
-            for line in lines:
-                buffer += line
-                if line == "EOS\n":
-                    kwja_sentence: Sentence = Sentence.from_knp(buffer)
-                    if kwja_sentence.text not in kwja_results[corpus]:
-                        kwja_results[corpus][kwja_sentence.text] = kwja_sentence
-                    buffer = ""
-
-    for corpus in ["kyoto", "kwdlc", "fuman"]:
-        print(f"{corpus}: (# of sentences = {len(pred_dicts[corpus])})")
-        src_text2gold_sent: Dict[str, Sentence] = dict()
-        for path in Path(f"{args.dataset_dir}/{corpus}/test").glob("*.knp"):
-            with path.open() as f:
-                document: Document = Document.from_knp(f.read())
-            for gold_sent in document.sentences:
-                src_text2gold_sent[gold_sent.text] = gold_sent
-        print(f"    # of sentences in gold: {len(src_text2gold_sent)}")
-
-        src_text2juman_sent: Dict[str, Sentence] = dict()
-        for path in Path(f"{OUTPUT_DIR}/predict_juman_knp/{corpus}").glob("*.knp"):
-            with path.open() as f:
-                document: Document = Document.from_knp(f.read())
-            for juman_sent in document.sentences:
-                src_text2juman_sent[juman_sent.text] = juman_sent
-        print(f"    # of sentences in juman: {len(src_text2juman_sent)}")
+    jumanpp = Jumanpp()
+
+    sid_to_seq2seq_sent: Dict[str, Sentence] = dict()
+    with Path(args.seq2seq_file).open() as f:
+        seq2seq_document: Document = Document.from_jumanpp(f.read())
+        for sentence in seq2seq_document.sentences:
+            sid_to_seq2seq_sent[sentence.sid] = sentence
+
+    output_dir: Path = Path(args.output_dir)
+    for corpus in ["kyoto", "kwdlc", "fuman", "wac", "norm"]:
+        sid_to_gold_sent: Dict[str, Sentence] = dict()
+        sid_to_juman_sent: Dict[str, Sentence] = dict()
+        juman_dir: Path = output_dir / "juman" / corpus
+        juman_dir.mkdir(parents=True, exist_ok=True)
+
+        gold_paths: List[Path] = list(Path(f"{args.dataset_dir}/{corpus}/test").glob("*.knp"))
+        for gold_path in gold_paths:
+            with gold_path.open() as f:
+                gold_document: Document = Document.from_knp(f.read())
+            for gold_sentence in gold_document.sentences:
+                sid_to_gold_sent[gold_sentence.sid] = gold_sentence
+
+                juman_path: Path = juman_dir / f"{gold_path.stem}_{gold_sentence.sid}.jumanpp"
+                if not juman_path.exists():
+                    juman_sentence: Sentence = jumanpp.apply_to_sentence(gold_sentence)
+                    with juman_path.open("w") as f:
+                        f.write(juman_sentence.to_jumanpp())
+                with juman_path.open() as f:
+                    juman_sentence = Sentence.from_jumanpp(f.read())
+                    sid_to_juman_sent[gold_sentence.sid] = juman_sentence
+
+        print(f"{corpus} (# of sents in juman/gold = {len(sid_to_juman_sent)}/{len(sid_to_gold_sent)})")
 
         jumans: List[Sentence] = []
-        generators: List[Sentence] = []
-        kwjas: List[Sentence] = []
+        seq2seqs: List[Sentence] = []
         golds: List[Sentence] = []
-        num_dummies: int = 0
-        processed_generateds: set[str] = set()
-        if args.compare_kwja:
-            for pred in pred_dicts[corpus]:
-                if pred["text"] not in kwja_results[corpus]:
-                    continue
-                if pred["generated"] in processed_generateds:
-                    continue
-                formatted, contain_dummy = format_juman(pred["generated"])
-                num_dummies += 1 if contain_dummy else 0
-                kwjas.append(kwja_results[corpus][pred["text"]])
-                generators.append(Sentence.from_knp(formatted))
-                jumans.append(src_text2juman_sent[pred["text"]])
-                golds.append(src_text2gold_sent[pred["text"]])
-                processed_generateds.add(pred["generated"])
-            print(f"    # of sentences in kwja: {len(kwjas)}")
-            assert len(jumans) == len(generators) == len(kwjas) == len(golds)
+        for sid, gold_sent in sid_to_gold_sent.items():
+            if sid not in sid_to_juman_sent or sid not in sid_to_seq2seq_sent:
+                continue
+            jumans.append(sid_to_juman_sent[sid])
+            seq2seqs.append(sid_to_seq2seq_sent[sid])
+            golds.append(gold_sent)
+            assert len(jumans) == len(seq2seqs) == len(golds)
+
+        if corpus != "norm":
+            print("  jumanpp")
+            juman_scorer = MorphologicalAnalysisScorer(jumans, golds)
+            juman_scorer.compute_score()
+
+            print("  seq2seq")
+            system_scorer = MorphologicalAnalysisScorer(seq2seqs, golds)
+            system_scorer.compute_score()
+            print(f"  # of different texts for seq2seq: {system_scorer.num_diff_texts}")
+            print(
+                f"  Ratio of same texts for seq2seq = {(len(seq2seqs) - system_scorer.num_diff_texts) / len(seq2seqs) * 100:.2f}\n"
+            )
         else:
-            for pred in pred_dicts[corpus]:
-                if pred["generated"] in processed_generateds:
-                    continue
-                formatted, contain_dummy = format_juman(pred["generated"])
-                num_dummies += 1 if contain_dummy else 0
-                generators.append(Sentence.from_knp(formatted))
-                jumans.append(src_text2juman_sent[pred["text"]])
-                golds.append(src_text2gold_sent[pred["text"]])
-                processed_generateds.add(pred["generated"])
-            assert len(jumans) == len(generators) == len(golds)
-
-        print("  jumanpp")
-        juman_scorer = MorphologicalAnalysisScorer(jumans, golds)
-        juman_scorer.compute_score()
-
-        if args.compare_kwja:
-            print("  kwja")
-            kwja_scorer = MorphologicalAnalysisScorer(kwjas, golds)
-            kwja_scorer.compute_score()
-
-        print("  seq2seq")
-        system_scorer = MorphologicalAnalysisScorer(generators, golds)
-        system_scorer.compute_score()
-
-        print(f"# of same texts for seq2seq: {system_scorer.num_same_texts}")
-        print(f"Ratio of same texts for seq2seq = {system_scorer.num_same_texts / len(generators) * 100:.2f}\n")
+            print("  seq2seq")
+            system_scorer = MorphologicalAnalysisScorer(seq2seqs, golds)
+            system_scorer.compute_score()
+            print(f"  # of different texts for seq2seq: {system_scorer.num_diff_texts}")
+            print(
+                f"  Ratio of same texts for seq2seq = {(len(seq2seqs) - system_scorer.num_diff_texts) / len(seq2seqs) * 100:.2f}"
+            )
+            print("  seq2seq (only target morpheme)")
+            system_scorer = MorphologicalAnalysisScorer(seq2seqs, golds, eval_norm=True)
+            system_scorer.compute_score()
 
 
 if __name__ == "__main__":
diff --git a/src/kwja/callbacks/char_module_writer.py b/src/kwja/callbacks/char_module_writer.py
index ff5b2465..4c748583 100644
--- a/src/kwja/callbacks/char_module_writer.py
+++ b/src/kwja/callbacks/char_module_writer.py
@@ -35,6 +35,8 @@ def write_on_batch_end(
         dataloader_idx: int,
     ) -> None:
         dataloaders = trainer.predict_dataloaders
+        if isinstance(trainer.predict_dataloaders, dict):
+            dataloaders = list(trainer.predict_dataloaders.values())
         dataset: Union[CharDataset, CharInferenceDataset] = dataloaders[dataloader_idx].dataset
 
         special_ids = set(dataset.tokenizer.all_special_ids) - {dataset.tokenizer.unk_token_id}
@@ -45,22 +47,14 @@ def write_on_batch_end(
         ):
             example: Union[CharExample, CharInferenceExample] = dataset.examples[example_id]
             assert example.doc_id is not None, "doc_id isn't set"
-            document = dataset.doc_id2document[example.doc_id]
-            # メモリリーク対策
-            predicted_document = document.reparse()
-            predicted_document.doc_id = document.doc_id
-            for predicted_sentence, sentence in zip(predicted_document.sentences, document.sentences):
-                predicted_sentence.comment = sentence.comment
+            document = dataset.doc_id2document.pop(example.doc_id)
 
-            assert (
-                len(example.encoding.input_ids) == len(word_segmentation_predictions) == len(word_norm_op_predictions)
-            )
             word_segmentation_tags, word_norm_op_tags = convert_predictions_into_tags(
                 word_segmentation_predictions, word_norm_op_predictions, example.encoding.input_ids, special_ids
             )
-            set_morphemes(predicted_document, word_segmentation_tags, word_norm_op_tags)
+            set_morphemes(document, word_segmentation_tags, word_norm_op_tags)
 
-            output_string = "".join(s.to_jumanpp() for s in extract_target_sentences(predicted_document))
+            output_string = "".join(s.to_jumanpp() for s in extract_target_sentences(document))
             if isinstance(self.destination, Path):
                 with self.destination.open(mode="a") as f:
                     f.write(output_string)
diff --git a/src/kwja/callbacks/senter_module_writer.py b/src/kwja/callbacks/senter_module_writer.py
index cf766ee4..0bf973bf 100644
--- a/src/kwja/callbacks/senter_module_writer.py
+++ b/src/kwja/callbacks/senter_module_writer.py
@@ -6,6 +6,7 @@
 import pytorch_lightning as pl
 from pytorch_lightning.callbacks import BasePredictionWriter
 
+import kwja
 from kwja.callbacks.utils import convert_senter_predictions_into_tags
 from kwja.datamodule.datasets.senter import SenterDataset
 from kwja.datamodule.datasets.senter_inference import SenterInferenceDataset, SenterInferenceExample
@@ -25,7 +26,7 @@ def __init__(self, destination: Optional[Union[str, Path]] = None) -> None:
             self.destination.parent.mkdir(exist_ok=True, parents=True)
             self.destination.unlink(missing_ok=True)
 
-        self.prev_did: Optional[str] = None
+        self.prev_doc_id: Optional[str] = None
         self.prev_sid: int = 0
 
     def write_on_batch_end(
@@ -39,6 +40,8 @@ def write_on_batch_end(
         dataloader_idx: int,
     ) -> None:
         dataloaders = trainer.predict_dataloaders
+        if isinstance(trainer.predict_dataloaders, dict):
+            dataloaders = list(trainer.predict_dataloaders.values())
         dataset: Union[SenterDataset, SenterInferenceDataset] = dataloaders[dataloader_idx].dataset
 
         special_ids = set(dataset.tokenizer.all_special_ids) - {dataset.tokenizer.unk_token_id}
@@ -50,28 +53,35 @@ def write_on_batch_end(
         ):
             example: Union[SenterExample, SenterInferenceExample] = dataset.examples[example_id]
             assert example.doc_id is not None, "doc_id isn't set"
-            document = dataset.doc_id2document[example.doc_id]
-
-            assert len(example.encoding.input_ids) == len(sent_segmentation_predictions)
+            document = dataset.doc_id2document.pop(example.doc_id)
 
             sent_segmentation_tags = convert_senter_predictions_into_tags(
                 sent_segmentation_predictions, example.encoding.input_ids, special_ids
             )
-            current_did = to_orig_doc_id(document.did)
-            is_new_doc = self.prev_did is None or self.prev_did != current_did
-            if is_new_doc:
-                self.prev_did = current_did
+
+            orig_doc_id = to_orig_doc_id(document.doc_id)
+
+            is_new_document = orig_doc_id != self.prev_doc_id
+            is_first_document = self.prev_doc_id is None
+            if is_new_document:
+                self.prev_doc_id = orig_doc_id
                 self.prev_sid = 0
-            for char, sent_segmentation_tag in zip(document.text, sent_segmentation_tags):
-                if is_new_doc or sent_segmentation_tag == "B":
+                if not is_first_document:
                     output_string += "\n"
-                    output_string += f"# S-ID:{current_did}-{self.prev_sid + 1}"
+
+            for char_index, (char, sent_segmentation_tag) in enumerate(zip(document.text, sent_segmentation_tags)):
+                if is_new_document and char_index == 0:
+                    # The first character of the document is always the start of a sentence
+                    output_string += f"# S-ID:{orig_doc_id}-{self.prev_sid + 1} kwja:{kwja.__version__}\n"
+                    self.prev_sid += 1
+                elif sent_segmentation_tag == "B":
                     output_string += "\n"
+                    output_string += f"# S-ID:{orig_doc_id}-{self.prev_sid + 1} kwja:{kwja.__version__}\n"
                     self.prev_sid += 1
                 output_string += char
 
         if isinstance(self.destination, Path):
-            with self.destination.open("a") as f:
+            with self.destination.open(mode="a") as f:
                 f.write(output_string)
         elif isinstance(self.destination, TextIOBase):
             self.destination.write(output_string)
diff --git a/src/kwja/callbacks/seq2seq_module_writer.py b/src/kwja/callbacks/seq2seq_module_writer.py
index 576331f6..394c7321 100644
--- a/src/kwja/callbacks/seq2seq_module_writer.py
+++ b/src/kwja/callbacks/seq2seq_module_writer.py
@@ -47,6 +47,8 @@ def write_on_batch_end(
         dataloader_idx: int,
     ) -> None:
         dataloaders = trainer.predict_dataloaders
+        if isinstance(trainer.predict_dataloaders, dict):
+            dataloaders = list(trainer.predict_dataloaders.values())
         dataset: Union[Seq2SeqDataset, Seq2SeqInferenceDataset] = dataloaders[dataloader_idx].dataset
 
         outputs: List[str] = []
diff --git a/src/kwja/callbacks/typo_module_writer.py b/src/kwja/callbacks/typo_module_writer.py
index 6981db53..c691146c 100644
--- a/src/kwja/callbacks/typo_module_writer.py
+++ b/src/kwja/callbacks/typo_module_writer.py
@@ -48,6 +48,8 @@ def write_on_batch_end(
         dataloader_idx: int,
     ) -> None:
         dataloaders = trainer.predict_dataloaders
+        if isinstance(trainer.predict_dataloaders, dict):
+            dataloaders = list(trainer.predict_dataloaders.values())
         dataset: Union[TypoDataset, TypoInferenceDataset] = dataloaders[dataloader_idx].dataset
 
         post_texts = []
diff --git a/src/kwja/callbacks/utils.py b/src/kwja/callbacks/utils.py
index 6a6658aa..fee1eaba 100644
--- a/src/kwja/callbacks/utils.py
+++ b/src/kwja/callbacks/utils.py
@@ -1,4 +1,5 @@
 from logging import getLogger
+from pathlib import Path
 from typing import Dict, List, Literal, Optional, Set, Tuple
 
 import numpy as np
@@ -8,6 +9,7 @@
 from rhoknp.props import DepType, NamedEntity, NamedEntityCategory
 from transformers import PreTrainedTokenizerBase
 
+from kwja.datamodule.examples import SpecialTokenIndexer
 from kwja.utils.cohesion_analysis import CohesionUtils
 from kwja.utils.constants import (
     BASE_PHRASE_FEATURES,
@@ -38,9 +40,9 @@
 
 
 # ---------- typo module writer ----------
-def get_maps(tokenizer: PreTrainedTokenizerBase, extended_vocab_path: str) -> Tuple[Dict[str, int], Dict[int, str]]:
+def get_maps(tokenizer: PreTrainedTokenizerBase, extended_vocab_path: Path) -> Tuple[Dict[str, int], Dict[int, str]]:
     token2token_id = tokenizer.get_vocab()
-    with open(extended_vocab_path, mode="r") as f:
+    with extended_vocab_path.open() as f:
         for line in f:
             if line := line.strip():
                 token2token_id[line] = len(token2token_id.keys())
@@ -174,48 +176,41 @@ def get_word_reading_predictions(
 
 
 def get_morpheme_attribute_predictions(
-    pos_logits: torch.Tensor,
-    subpos_logits: torch.Tensor,
-    conjtype_logits: torch.Tensor,
-    conjform_logits: torch.Tensor,
+    pos_logits: List[List[float]],
+    subpos_logits: List[List[float]],
+    conjtype_logits: List[List[float]],
+    conjform_logits: List[List[float]],
 ) -> Tuple[List[int], List[int], List[int], List[int]]:
-    pos_predictions: List[int] = pos_logits.argmax(dim=1).tolist()
+    pos_predictions: List[int] = np.array(pos_logits).argmax(axis=1).tolist()
     subpos_predictions: List[int] = []
-    for pos_index, subpos_logit_list in zip(pos_predictions, subpos_logits.tolist()):
-        subpos_tag2subpos_id = POS_TAG_SUBPOS_TAG2SUBPOS_ID[POS_TAGS[pos_index]]
-        possible_subpos_indices: Set[int] = {
-            SUBPOS_TAGS.index(subpos_tag) for subpos_tag in subpos_tag2subpos_id.keys()
-        }
-        max_subpos_logit = MASKED
-        subpos_prediction: int = 0
-        for subpos_index, subpos_logit in enumerate(subpos_logit_list):
-            if subpos_index in possible_subpos_indices and subpos_logit > max_subpos_logit:
-                max_subpos_logit = subpos_logit
-                subpos_prediction = subpos_index
-        subpos_predictions.append(subpos_prediction)
-
-    conjtype_predictions: List[int] = conjtype_logits.argmax(dim=1).tolist()
+    conjtype_predictions: List[int] = np.array(conjtype_logits).argmax(axis=1).tolist()
     conjform_predictions: List[int] = []
-    for i, (pos_index, subpos_index, conjtype_index, conjform_logit_list) in enumerate(
-        zip(pos_predictions, subpos_predictions, conjtype_predictions, conjform_logits.tolist())
+    for i, (pos_index, subpos_logit_list, conjtype_index, conjform_logit_list) in enumerate(
+        zip(pos_predictions, subpos_logits, conjtype_predictions, conjform_logits)
     ):
-        pos_tag: str = POS_TAGS[pos_index]
-        subpos_tag: str = SUBPOS_TAGS[subpos_index]
+        pos_tag = POS_TAGS[pos_index]
+        possible_subpos_tags: Set[str] = set(POS_TAG_SUBPOS_TAG2SUBPOS_ID[pos_tag].keys())
+        for subpos_index, subpos_tag in enumerate(SUBPOS_TAGS):
+            if subpos_tag not in possible_subpos_tags:
+                subpos_logit_list[subpos_index] = MASKED
+        subpos_index = np.array(subpos_logit_list).argmax().item()
+        subpos_tag = SUBPOS_TAGS[subpos_index]
+        assert subpos_tag in possible_subpos_tags
+        subpos_predictions.append(subpos_index)
+
+        conjtype_tag = CONJTYPE_TAGS[conjtype_index]
         if (pos_tag, subpos_tag) in INFLECTABLE:
-            conjform_tag2conjform_id = CONJTYPE_TAG_CONJFORM_TAG2CONJFORM_ID[CONJTYPE_TAGS[conjtype_index]]
-            possible_conjform_indices: Set[int] = {
-                CONJFORM_TAGS.index(conjform_tag) for conjform_tag in conjform_tag2conjform_id.keys()
-            }
-            max_conjform_logit = MASKED
-            conjform_prediction: int = 0
-            for conjform_index, conjform_logit in enumerate(conjform_logit_list):
-                if conjform_index in possible_conjform_indices and conjform_logit > max_conjform_logit:
-                    max_conjform_logit = conjform_logit
-                    conjform_prediction = conjform_index
+            possible_conjform_tags: Set[str] = set(CONJTYPE_TAG_CONJFORM_TAG2CONJFORM_ID[conjtype_tag].keys())
+            for conjform_index, conjform_tag in enumerate(CONJFORM_TAGS):
+                if conjform_tag not in possible_conjform_tags:
+                    conjform_logit_list[conjform_index] = MASKED
+            conjform_index = np.array(conjform_logit_list).argmax().item()
+            conjform_tag = CONJFORM_TAGS[conjform_index]
+            assert conjform_tag in possible_conjform_tags
         else:
             conjtype_predictions[i] = CONJTYPE_TAGS.index("*")
-            conjform_prediction = CONJTYPE_TAG_CONJFORM_TAG2CONJFORM_ID["*"]["*"]
-        conjform_predictions.append(conjform_prediction)
+            conjform_index = CONJTYPE_TAG_CONJFORM_TAG2CONJFORM_ID["*"]["*"]
+        conjform_predictions.append(conjform_index)
 
     return pos_predictions, subpos_predictions, conjtype_predictions, conjform_predictions
 
@@ -224,20 +219,11 @@ def build_morphemes(
     surfs: List[str],
     lemmas: List[str],
     reading_predictions: List[str],
-    pos_predictions: List[int],
-    subpos_predictions: List[int],
-    conjtype_predictions: List[int],
-    conjform_predictions: List[int],
+    morpheme_attribute_predictions: Tuple[List[int], List[int], List[int], List[int]],
 ) -> List[Morpheme]:
     morphemes = []
     for surf, lemma, reading, pos_index, subpos_index, conjtype_index, conjform_index in zip(
-        surfs,
-        lemmas,
-        reading_predictions,
-        pos_predictions,
-        subpos_predictions,
-        conjtype_predictions,
-        conjform_predictions,
+        surfs, lemmas, reading_predictions, *morpheme_attribute_predictions
     ):
         pos = POS_TAGS[pos_index]
         pos_id = POS_TAG2POS_ID[pos]
@@ -270,9 +256,9 @@ def chunk_morphemes(
 ) -> Document:
     predicted_sentences = []
     for sentence in document.sentences:
-        morpheme_buffer = []
-        base_phrase_buffer = []
-        phrase_buffer = []
+        morpheme_buffer: List[Morpheme] = []
+        base_phrase_buffer: List[BasePhrase] = []
+        phrase_buffer: List[Phrase] = []
         for i in [m.global_index for m in sentence.morphemes]:
             morpheme = morphemes[i]
 
@@ -378,11 +364,11 @@ def add_dependency(
     sentence: Sentence,
     dependency_predictions: List[List[int]],
     dependency_type_predictions: List[List[int]],
-    special_token2index: Dict[str, int],
+    special_token_indexer: SpecialTokenIndexer,
 ) -> None:
     base_phrases = sentence.base_phrases
     morpheme_global_index2base_phrase_index = {m.global_index: bp.index for bp in base_phrases for m in bp.morphemes}
-    morpheme_global_index2base_phrase_index[special_token2index["[ROOT]"]] = -1
+    morpheme_global_index2base_phrase_index[special_token_indexer.get_morpheme_level_index("[ROOT]")] = -1
     dependency_manager = DependencyManager()
     for base_phrase in base_phrases:
         for parent_morpheme_global_index, dependency_type_index in zip(
@@ -443,24 +429,30 @@ def add_cohesion(
     document: Document,
     cohesion_logits: List[List[List[float]]],  # (rel, seq, seq)
     cohesion_task2utils: Dict[CohesionTask, CohesionUtils],
-    index2special_token: Dict[int, str],
+    special_token_indexer: SpecialTokenIndexer,
 ) -> None:
-    flatten_rels = [r for cohesion_utils in cohesion_task2utils.values() for r in cohesion_utils.rels]
+    rel2logits = dict(
+        zip(
+            [r for cohesion_utils in cohesion_task2utils.values() for r in cohesion_utils.rels],
+            cohesion_logits,
+        )
+    )
     base_phrases = document.base_phrases
     for base_phrase in base_phrases:
         rel_tags = RelTagList()
         for cohesion_utils in cohesion_task2utils.values():
-            if cohesion_utils.is_target(base_phrase):
-                for rel in cohesion_utils.rels:
-                    rel_tag = _to_rel_tag(
-                        rel,
-                        cohesion_logits[flatten_rels.index(rel)][base_phrase.head.global_index],  # (seq, )
-                        base_phrases,
-                        index2special_token,
-                        cohesion_utils.exophora_referents,
-                    )
-                    if rel_tag is not None:
-                        rel_tags.append(rel_tag)
+            if cohesion_utils.is_target(base_phrase) is False:
+                continue
+            for rel in cohesion_utils.rels:
+                rel_tag = _to_rel_tag(
+                    rel,
+                    rel2logits[rel][base_phrase.head.global_index],  # (seq, )
+                    base_phrases,
+                    special_token_indexer,
+                    cohesion_utils.exophora_referents,
+                )
+                if rel_tag is not None:
+                    rel_tags.append(rel_tag)
         base_phrase.rel_tags = rel_tags
 
 
@@ -468,14 +460,15 @@ def _to_rel_tag(
     rel: str,
     rel_logits: List[float],  # (seq, )
     base_phrases: List[BasePhrase],
-    index2special_token: Dict[int, str],
+    special_token_indexer: SpecialTokenIndexer,
     exophora_referents: List[ExophoraReferent],
 ) -> Optional[RelTag]:
-    logits = [rel_logits[bp.head.global_index] for bp in base_phrases] + [rel_logits[i] for i in index2special_token]
-    predicted_antecedent_index: int = np.argmax(logits).item()
-    if 0 <= predicted_antecedent_index < len(base_phrases):
+    logits = [rel_logits[bp.head.global_index] for bp in base_phrases]
+    logits += [rel_logits[i] for i in special_token_indexer.get_morpheme_level_indices()]
+    predicted_base_phrase_global_index: int = np.argmax(logits).item()
+    if 0 <= predicted_base_phrase_global_index < len(base_phrases):
         # endophora
-        predicted_antecedent = base_phrases[predicted_antecedent_index]
+        predicted_antecedent = base_phrases[predicted_base_phrase_global_index]
         return RelTag(
             type=rel,
             target=predicted_antecedent.head.text,
@@ -485,17 +478,17 @@ def _to_rel_tag(
         )
     else:
         # exophora
-        special_token = list(index2special_token.values())[predicted_antecedent_index - len(base_phrases)]
-        if special_token in [str(e) for e in exophora_referents]:  # exclude [NULL], [NA], and [ROOT]
+        special_token = special_token_indexer.special_tokens[predicted_base_phrase_global_index - len(base_phrases)]
+        stripped_special_token = special_token[1:-1]  # strip '[' and ']'
+        if stripped_special_token in [str(er) for er in exophora_referents]:  # exclude [NULL], [NA], and [ROOT]
             return RelTag(
                 type=rel,
-                target=special_token,
+                target=stripped_special_token,
                 sid=None,
                 base_phrase_index=None,
                 mode=None,
             )
-        else:
-            return None
+    return None
 
 
 def add_discourse(document: Document, discourse_predictions: List[List[int]]) -> None:
diff --git a/src/kwja/callbacks/word_discourse_module_writer.py b/src/kwja/callbacks/word_discourse_module_writer.py
deleted file mode 100644
index 9e763682..00000000
--- a/src/kwja/callbacks/word_discourse_module_writer.py
+++ /dev/null
@@ -1,71 +0,0 @@
-import logging
-import sys
-from io import TextIOBase
-from pathlib import Path
-from typing import Any, Optional, Sequence, TextIO, Union
-
-import pytorch_lightning as pl
-from pytorch_lightning.callbacks import BasePredictionWriter
-
-from kwja.callbacks.utils import add_discourse
-from kwja.datamodule.datasets import WordDataset, WordInferenceDataset
-from kwja.datamodule.examples import WordExample, WordInferenceExample
-from kwja.utils.sub_document import extract_target_sentences
-
-logger = logging.getLogger(__name__)
-
-
-class WordDiscourseModuleWriter(BasePredictionWriter):
-    def __init__(self, destination: Optional[Union[str, Path]] = None) -> None:
-        super().__init__(write_interval="batch")
-        if destination is None:
-            self.destination: Union[Path, TextIO] = sys.stdout
-        else:
-            if isinstance(destination, str):
-                destination = Path(destination)
-            self.destination = destination
-            self.destination.parent.mkdir(exist_ok=True, parents=True)
-            self.destination.unlink(missing_ok=True)
-
-    def write_on_batch_end(
-        self,
-        trainer: pl.Trainer,
-        pl_module: pl.LightningModule,
-        prediction: Any,
-        batch_indices: Optional[Sequence[int]],
-        batch: Any,
-        batch_idx: int,
-        dataloader_idx: int,
-    ) -> None:
-        dataloaders = trainer.predict_dataloaders
-        dataset: Union[WordDataset, WordInferenceDataset] = dataloaders[dataloader_idx].dataset
-
-        for example_id, discourse_predictions in zip(
-            prediction["example_ids"].tolist(), prediction["discourse_predictions"].tolist()
-        ):
-            example: Union[WordExample, WordInferenceExample] = dataset.examples[example_id]
-            assert example.doc_id is not None, "doc_id isn't set"
-            document = dataset.doc_id2document[example.doc_id]
-            # メモリリーク対策
-            predicted_document = document.reparse()
-            predicted_document.doc_id = document.doc_id
-            for predicted_sentence, sentence in zip(predicted_document.sentences, document.sentences):
-                predicted_sentence.comment = sentence.comment
-
-            add_discourse(predicted_document, discourse_predictions)
-
-            output_string = "".join(s.to_knp() for s in extract_target_sentences(predicted_document))
-            if isinstance(self.destination, Path):
-                with self.destination.open(mode="a") as f:
-                    f.write(output_string)
-            elif isinstance(self.destination, TextIOBase):
-                self.destination.write(output_string)
-
-    def write_on_epoch_end(
-        self,
-        trainer: pl.Trainer,
-        pl_module: pl.LightningModule,
-        predictions: Sequence[Any],
-        batch_indices: Optional[Sequence[Any]] = None,
-    ) -> None:
-        pass  # pragma: no cover
diff --git a/src/kwja/callbacks/word_module_writer.py b/src/kwja/callbacks/word_module_writer.py
index 1001d27f..f3b434de 100644
--- a/src/kwja/callbacks/word_module_writer.py
+++ b/src/kwja/callbacks/word_module_writer.py
@@ -4,7 +4,7 @@
 from io import TextIOBase
 from itertools import product
 from pathlib import Path
-from typing import Any, Dict, List, Optional, Sequence, TextIO, Union
+from typing import Any, Dict, List, Optional, Sequence, TextIO, Tuple, Union
 
 import pytorch_lightning as pl
 from jinf import Jinf
@@ -33,7 +33,6 @@
     POS_TAGS,
     RESOURCE_PATH,
     SUBPOS_TAGS,
-    WordTask,
 )
 from kwja.utils.jumandic import JumanDic
 from kwja.utils.reading_prediction import get_reading2reading_id
@@ -75,16 +74,15 @@ def write_on_batch_end(
         dataloader_idx: int,
     ) -> None:
         dataloaders = trainer.predict_dataloaders
+        if isinstance(trainer.predict_dataloaders, dict):
+            dataloaders = list(trainer.predict_dataloaders.values())
         dataset: Union[WordDataset, WordInferenceDataset] = dataloaders[dataloader_idx].dataset
 
         for (
             example_id,
             reading_predictions,
             reading_subword_map,
-            pos_logits,
-            subpos_logits,
-            conjtype_logits,
-            conjform_logits,
+            *morpheme_attribute_logits,  # pos_logits, subpos_logits, conjtype_logits, conjform_logits
             word_feature_probabilities,
             ne_predictions,
             base_phrase_feature_probabilities,
@@ -96,22 +94,23 @@ def write_on_batch_end(
             prediction["example_ids"].tolist(),
             prediction["reading_predictions"].tolist(),
             prediction["reading_subword_map"].tolist(),
-            prediction["pos_logits"],
-            prediction["subpos_logits"],
-            prediction["conjtype_logits"],
-            prediction["conjform_logits"],
+            prediction["pos_logits"].tolist(),
+            prediction["subpos_logits"].tolist(),
+            prediction["conjtype_logits"].tolist(),
+            prediction["conjform_logits"].tolist(),
             prediction["word_feature_probabilities"].tolist(),
-            prediction["ne_predictions"],
+            prediction["ne_predictions"].tolist(),
             prediction["base_phrase_feature_probabilities"].tolist(),
             prediction["dependency_predictions"].tolist(),
             prediction["dependency_type_predictions"].tolist(),
-            prediction["cohesion_logits"].tolist(),  # (b, rel, seq, seq)
+            prediction["cohesion_logits"].tolist(),
             prediction["discourse_predictions"].tolist(),
         ):
             example: Union[WordExample, WordInferenceExample] = dataset.examples[example_id]
             assert example.doc_id is not None, "doc_id isn't set"
-            document = dataset.doc_id2document[example.doc_id]
-            if dataset.from_seq2seq:
+            document = dataset.doc_id2document.pop(example.doc_id)
+            num_morphemes = len(document.morphemes)
+            if dataset.from_seq2seq is True:
                 word_reading_predictions = [m.reading for m in document.morphemes]
                 canons = [m.canon for m in document.morphemes]
             else:
@@ -123,20 +122,14 @@ def write_on_batch_end(
                     reading_subword_map,
                 )
                 canons = ["" for _ in document.morphemes]
-            (
-                pos_predictions,
-                subpos_predictions,
-                conjtype_predictions,
-                conjform_predictions,
-            ) = get_morpheme_attribute_predictions(pos_logits, subpos_logits, conjtype_logits, conjform_logits)
+            morpheme_attribute_predictions = get_morpheme_attribute_predictions(
+                *(logits[:num_morphemes] for logits in morpheme_attribute_logits)
+            )
             morphemes = self._build_morphemes(
                 [m.surf for m in document.morphemes],
                 [m.lemma for m in document.morphemes],
                 word_reading_predictions,
-                pos_predictions,
-                subpos_predictions,
-                conjtype_predictions,
-                conjform_predictions,
+                morpheme_attribute_predictions,
                 canons,
                 dataset.from_seq2seq,
             )
@@ -146,7 +139,7 @@ def write_on_batch_end(
                 add_named_entities(sentence, ne_predictions)
                 add_base_phrase_features(sentence, base_phrase_feature_probabilities)
                 add_dependency(
-                    sentence, dependency_predictions, dependency_type_predictions, dataset.special_token2index
+                    sentence, dependency_predictions, dependency_type_predictions, example.special_token_indexer
                 )
             orig_doc_id = to_orig_doc_id(document.doc_id)
             # 解析済みの文とマージ
@@ -159,10 +152,9 @@ def write_on_batch_end(
                 predicted_document,
                 cohesion_logits,
                 dataset.cohesion_task2utils,
-                dataset.index2special_token,
+                example.special_token_indexer,
             )
-            if WordTask.DISCOURSE_PARSING in pl_module.training_tasks:
-                add_discourse(predicted_document, discourse_predictions)
+            add_discourse(predicted_document, discourse_predictions)
 
             for predicted_sentence, sentence in zip(
                 extract_target_sentences(predicted_document),
@@ -186,24 +178,14 @@ def _build_morphemes(
         surfs: List[str],
         norms: List[str],
         reading_predictions: List[str],
-        pos_predictions: List[int],
-        subpos_predictions: List[int],
-        conjtype_predictions: List[int],
-        conjform_predictions: List[int],
+        morpheme_attribute_predictions: Tuple[List[int], List[int], List[int], List[int]],
         canons: List[Optional[str]],
         from_seq2seq: bool,
     ) -> List[Morpheme]:
         assert len(surfs) == len(norms) == len(reading_predictions)
         morphemes = []
         for surf, norm, reading, pos_index, subpos_index, conjtype_index, conjform_index, canon in zip(
-            surfs,
-            norms,
-            reading_predictions,
-            pos_predictions,
-            subpos_predictions,
-            conjtype_predictions,
-            conjform_predictions,
-            canons,
+            surfs, norms, reading_predictions, *morpheme_attribute_predictions, canons
         ):
             pos = POS_TAGS[pos_index]
             pos_id = POS_TAG2POS_ID[pos]
diff --git a/src/kwja/cli/__init__.py b/src/kwja/cli/__init__.py
index e69de29b..c3870fc8 100644
--- a/src/kwja/cli/__init__.py
+++ b/src/kwja/cli/__init__.py
@@ -0,0 +1,5 @@
+import os
+import sys
+
+if "pytest" not in sys.modules:
+    os.environ["KWJA_CLI_MODE"] = "1"
diff --git a/src/kwja/cli/cli.py b/src/kwja/cli/cli.py
index 34ea1cd5..23e02688 100644
--- a/src/kwja/cli/cli.py
+++ b/src/kwja/cli/cli.py
@@ -1,61 +1,59 @@
 import os
 import re
+import sys
 from abc import ABC
-from enum import Enum
+from datetime import datetime
 from pathlib import Path
-from tempfile import TemporaryDirectory
+from tempfile import NamedTemporaryFile
 from typing import Dict, List, Optional, Set, Tuple
+from unicodedata import normalize
 
 import hydra
 import pytorch_lightning as pl
+import torch
 import typer
-from omegaconf import OmegaConf
 from pytorch_lightning.trainer.states import TrainerFn
-from rhoknp import Document
-from rhoknp.utils.reader import chunk_by_document
+from rhoknp import RegexSenter, Sentence
+from rhoknp.utils.reader import chunk_by_sentence
 
 import kwja
-from kwja.cli.utils import download_checkpoint, prepare_device, suppress_debug_info
+from kwja.cli.config import CLIConfig, Device, ModelSize, get_kwja_config_file
+from kwja.cli.utils import download_checkpoint, prepare_device
 from kwja.datamodule.datamodule import DataModule
-from kwja.modules import CharModule, Seq2SeqModule, TypoModule, WordModule
+from kwja.modules import CharModule, SenterModule, Seq2SeqModule, TypoModule, WordModule
+from kwja.utils.constants import TRANSLATION_TABLE
+from kwja.utils.logging_util import filter_logs
 
-suppress_debug_info()
+filter_logs(environment="production")
 os.environ["TOKENIZERS_PARALLELISM"] = "false"
-OmegaConf.register_new_resolver("concat", lambda x, y: x + y)
 OMEGACONF_VARIABLE_INTERPOLATION = re.compile(r"\$(?P<variable>\{.+?})")
 
 app = typer.Typer(pretty_exceptions_show_locals=False)
 
 
-class Device(str, Enum):
-    auto = "auto"
-    cpu = "cpu"
-    gpu = "gpu"
-
-
 class BaseModuleProcessor(ABC):
-    def __init__(
-        self,
-        specified_device: str,
-        model_size: str,
-        batch_size: int,
-        destination: Path,
-    ) -> None:
-        self.device_name, self.device = prepare_device(specified_device)
-        self.model_size: str = model_size
+    def __init__(self, config: CLIConfig, batch_size: int) -> None:
+        self.config: CLIConfig = config
+        self.device_name, self.device = prepare_device(config.device.value)
+        self.model_size: ModelSize = config.model_size
         self.batch_size: int = batch_size
-        self.destination: Path = destination
+        self.destination = Path(NamedTemporaryFile().name)
         self.module: Optional[pl.LightningModule] = None
         self.trainer: Optional[pl.Trainer] = None
 
     def load(self):
         self.module = self._load_module()
+        if self.config.torch_compile is True:
+            self.module = torch.compile(self.module)  # type: ignore
         self.module.hparams.datamodule.batch_size = self.batch_size
+        self.module.hparams.datamodule.num_workers = self.config.num_workers
 
-        writer = hydra.utils.instantiate(self.module.hparams.callbacks.prediction_writer, destination=self.destination)
         self.trainer = pl.Trainer(
             logger=False,
-            callbacks=[writer, hydra.utils.instantiate(self.module.hparams.callbacks.progress_bar)],
+            callbacks=[
+                hydra.utils.instantiate(self.module.hparams.callbacks.prediction_writer, destination=self.destination),
+                hydra.utils.instantiate(self.module.hparams.callbacks.progress_bar),
+            ],
             accelerator=self.device_name,
             devices=1,
         )
@@ -66,16 +64,15 @@ def _load_module(self) -> pl.LightningModule:
     def delete_module_and_trainer(self) -> None:
         del self.module, self.trainer
 
-    def apply_module(self, input_file: Path) -> Path:
+    def apply_module(self, input_file: Path) -> None:
         datamodule = self._load_datamodule(input_file)
         assert self.trainer is not None
-        self.trainer.predict(model=self.module, dataloaders=datamodule.predict_dataloader(), return_predictions=False)
-        return self.destination
+        self.trainer.predict(model=self.module, dataloaders=[datamodule.predict_dataloader()], return_predictions=False)
 
     def _load_datamodule(self, input_file: Path) -> DataModule:
         raise NotImplementedError
 
-    def output_prediction(self) -> None:
+    def export_prediction(self) -> str:
         raise NotImplementedError
 
 
@@ -83,73 +80,108 @@ class TypoModuleProcessor(BaseModuleProcessor):
     def _load_module(self) -> pl.LightningModule:
         typer.echo("Loading typo module", err=True)
         checkpoint_path: Path = download_checkpoint(module="typo", model_size=self.model_size)
-        return TypoModule.load_from_checkpoint(checkpoint_path, map_location=self.device)
+        return TypoModule.fast_load_from_checkpoint(checkpoint_path, map_location=self.device)
 
     def _load_datamodule(self, input_file: Path) -> DataModule:
         assert self.module is not None
-        self.module.hparams.datamodule.predict.texts = input_file.read_text().splitlines()
+        self.module.hparams.datamodule.predict.texts = _split_into_documents(input_file.read_text())
         datamodule = DataModule(cfg=self.module.hparams.datamodule)
         datamodule.setup(stage=TrainerFn.PREDICTING)
         return datamodule
 
-    def output_prediction(self) -> None:
-        post_texts: List[str] = []
-        with self.destination.open() as f:
-            for line in f:
-                if line.strip() != "EOD":
-                    post_texts.append(line.strip())
-        print("\n".join(post_texts))
+    def export_prediction(self) -> str:
+        return self.destination.read_text()
+
+
+class SenterModuleProcessor(BaseModuleProcessor):
+    doc_idx = 0
+
+    def load(self):
+        if self.model_size != ModelSize.tiny:
+            super().load()
+
+    def _load_module(self) -> pl.LightningModule:
+        if self.model_size != ModelSize.tiny:
+            typer.echo("Loading senter module", err=True)
+            checkpoint_path: Path = download_checkpoint(module="senter", model_size=self.model_size)
+            return SenterModule.fast_load_from_checkpoint(checkpoint_path, map_location=self.device)
+        return  # type: ignore
+
+    def _load_datamodule(self, input_file: Path) -> DataModule:
+        assert self.module is not None
+        self.module.hparams.datamodule.predict.texts = _split_into_documents(input_file.read_text())
+        datamodule = DataModule(cfg=self.module.hparams.datamodule)
+        datamodule.setup(stage=TrainerFn.PREDICTING)
+        return datamodule
+
+    def apply_module(self, input_file: Path) -> None:
+        if self.model_size != ModelSize.tiny:
+            super().apply_module(input_file)
+            return
+
+        senter = RegexSenter()
+        output_string = ""
+        doc_id_prefix = datetime.now().strftime("%Y%m%d%H%M")
+        for document_text in _split_into_documents(input_file.read_text()):
+            document = senter.apply_to_document(document_text)
+            document.doc_id = f"{doc_id_prefix}-{self.doc_idx}"
+            self.doc_idx += 1
+            for sent_idx, sentence in enumerate(document.sentences):
+                sentence.sid = f"{document.doc_id}-{sent_idx}"
+                sentence.misc_comment = f"kwja:{kwja.__version__}"
+            output_string += document.to_raw_text().strip()
+        self.destination.write_text(output_string)
+
+    def export_prediction(self) -> str:
+        return self.destination.read_text()
 
 
 class Seq2SeqModuleProcessor(BaseModuleProcessor):
     def _load_module(self):
         typer.echo("Loading seq2seq module", err=True)
         checkpoint_path: Path = download_checkpoint(module="seq2seq", model_size=self.model_size)
-        return Seq2SeqModule.load_from_checkpoint(checkpoint_path, map_location=self.device)
+        return Seq2SeqModule.fast_load_from_checkpoint(checkpoint_path, map_location=self.device)
 
     def _load_datamodule(self, input_file: Path) -> DataModule:
         assert self.module is not None
-        self.module.hparams.datamodule.predict.texts = input_file.read_text().splitlines()
+        self.module.hparams.datamodule.predict.senter_file = input_file
         datamodule = DataModule(cfg=self.module.hparams.datamodule)
         datamodule.setup(stage=TrainerFn.PREDICTING)
         return datamodule
 
-    def output_prediction(self) -> None:
-        seq2seq_texts: List[str] = []
-        with self.destination.open() as f:
-            for line in f:
-                if line.strip() != "EOD":
-                    seq2seq_texts.append(line.strip())
-        print("\n".join(seq2seq_texts))
+    def export_prediction(self) -> str:
+        return self.destination.read_text()
 
 
 class CharModuleProcessor(BaseModuleProcessor):
     def _load_module(self) -> pl.LightningModule:
         typer.echo("Loading char module", err=True)
         checkpoint_path: Path = download_checkpoint(module="char", model_size=self.model_size)
-        return CharModule.load_from_checkpoint(checkpoint_path, map_location=self.device)
+        return CharModule.fast_load_from_checkpoint(checkpoint_path, map_location=self.device)
 
     def _load_datamodule(self, input_file: Path) -> DataModule:
         assert self.module is not None
-        self.module.hparams.datamodule.predict.texts = input_file.read_text().splitlines()
+        self.module.hparams.datamodule.predict.senter_file = input_file
         datamodule = DataModule(cfg=self.module.hparams.datamodule)
         datamodule.setup(stage=TrainerFn.PREDICTING)
         return datamodule
 
-    def output_prediction(self) -> None:
-        word_segmented_texts: List[str] = []
+    def export_prediction(self) -> str:
+        export_text = ""
         with self.destination.open() as f:
-            for juman_text in chunk_by_document(f):
-                document = Document.from_jumanpp(juman_text)
-                word_segmented_texts.append(" ".join(m.text for m in document.morphemes))
-        print("\n".join(word_segmented_texts))
+            for juman_text in chunk_by_sentence(f):
+                sentence = Sentence.from_jumanpp(juman_text)
+                if sentence.comment != "":
+                    export_text += sentence.comment + "\n"
+                export_text += " ".join(m.text for m in sentence.morphemes) + "\n"
+        return export_text
 
 
 class WordModuleProcessor(BaseModuleProcessor):
     def _load_module(self) -> pl.LightningModule:
         typer.echo("Loading word module", err=True)
         checkpoint_path: Path = download_checkpoint(module="word", model_size=self.model_size)
-        return WordModule.load_from_checkpoint(checkpoint_path, map_location=self.device)
+        return WordModule.fast_load_from_checkpoint(checkpoint_path, map_location=self.device)
 
     def _load_datamodule(self, input_file: Path) -> DataModule:
         assert self.module is not None
@@ -158,56 +190,19 @@ def _load_datamodule(self, input_file: Path) -> DataModule:
         datamodule.setup(stage=TrainerFn.PREDICTING)
         return datamodule
 
-    def output_prediction(self) -> None:
-        print(self.destination.read_text(), end="")
-
-
-class WordDiscourseModuleProcessor(BaseModuleProcessor):
-    def _load_module(self) -> pl.LightningModule:
-        typer.echo("Loading word_discourse module", err=True)
-        checkpoint_path: Path = download_checkpoint(module="word_discourse", model_size=self.model_size)
-        module = WordModule.load_from_checkpoint(checkpoint_path, map_location=self.device)
-        module.hparams.callbacks.prediction_writer = {
-            "_target_": "kwja.callbacks.word_discourse_module_writer.WordDiscourseModuleWriter"
-        }
-        return module
-
-    def _load_datamodule(self, input_file: Path) -> DataModule:
-        assert self.module is not None
-        self.module.hparams.datamodule.predict.knp_file = input_file
-        datamodule = DataModule(cfg=self.module.hparams.datamodule)
-        datamodule.setup(stage=TrainerFn.PREDICTING)
-        return datamodule
-
-    def output_prediction(self) -> None:
-        print(self.destination.read_text(), end="")
+    def export_prediction(self) -> str:
+        return self.destination.read_text()
 
 
 class CLIProcessor:
-    def __init__(
-        self,
-        specified_device: str,
-        model_size: str,
-        typo_batch_size: int,
-        seq2seq_batch_size: int,
-        char_batch_size: int,
-        word_batch_size: int,
-    ) -> None:
-        self.tmp_dir = TemporaryDirectory()
-        self.raw_destination = self.tmp_dir.name / Path("raw_text.txt")
-        typo_destination = self.tmp_dir.name / Path("typo_prediction.txt")
-        seq2seq_destination = self.tmp_dir.name / Path("seq2seq_prediction.seq2seq")
-        char_destination = self.tmp_dir.name / Path("char_prediction.juman")
-        word_destination = self.tmp_dir.name / Path("word_prediction.knp")
-        word_discourse_destination = self.tmp_dir.name / Path("word_discourse_prediction.knp")
+    def __init__(self, config: CLIConfig) -> None:
+        self.raw_destination = Path(NamedTemporaryFile(suffix=".txt", delete=False).name)
         self.processors: Dict[str, BaseModuleProcessor] = {
-            "typo": TypoModuleProcessor(specified_device, model_size, typo_batch_size, typo_destination),
-            "seq2seq": Seq2SeqModuleProcessor(specified_device, model_size, seq2seq_batch_size, seq2seq_destination),
-            "char": CharModuleProcessor(specified_device, model_size, char_batch_size, char_destination),
-            "word": WordModuleProcessor(specified_device, model_size, word_batch_size, word_destination),
-            "word_discourse": WordDiscourseModuleProcessor(
-                specified_device, model_size, word_batch_size, word_discourse_destination
-            ),
+            "typo": TypoModuleProcessor(config, config.typo_batch_size),
+            "senter": SenterModuleProcessor(config, config.senter_batch_size),
+            "seq2seq": Seq2SeqModuleProcessor(config, config.seq2seq_batch_size),
+            "char": CharModuleProcessor(config, config.char_batch_size),
+            "word": WordModuleProcessor(config, config.word_batch_size),
         }
 
     def load_modules(self, tasks: List[str]) -> None:
@@ -215,43 +210,52 @@ def load_modules(self, tasks: List[str]) -> None:
             self.processors[task].load()
 
     def refresh(self) -> None:
+        self.raw_destination.unlink(missing_ok=True)
         for processor in self.processors.values():
             processor.destination.unlink(missing_ok=True)
 
-    def run(self, input_text: str, specified_tasks: List[str], interactive: bool = False) -> None:
-        input_texts = _split_input_texts([input_text])
-        self.raw_destination.write_text("\n".join(input_texts))  # TODO: consider using pickle
+    def run(self, input_documents: List[str], tasks: List[str], interactive: bool = False) -> None:
+        self.raw_destination.write_text(
+            "".join(_normalize_text(input_document) + "\nEOD\n" for input_document in input_documents)
+        )
         input_file = self.raw_destination
-        for specified_task in specified_tasks:
-            processor = self.processors[specified_task]
+        for task in tasks:
+            processor = self.processors[task]
             if interactive is False:
                 processor.load()
-            if specified_task in {"char", "seq2seq"} and "typo" in specified_tasks:
-                # char and seq2seq module after typo module
-                input_texts = _split_input_texts([input_file.read_text()])
-                input_file.write_text("\n".join(input_texts))  # TODO: consider using pickle
-            input_file = processor.apply_module(input_file)
+            processor.apply_module(input_file)
+            input_file = processor.destination
             if interactive is False:
                 processor.delete_module_and_trainer()
-        self.processors[specified_tasks[-1]].output_prediction()
-
-
-def _split_input_texts(input_texts: List[str]) -> List[str]:
-    split_texts: List[str] = []
-    split_text: str = ""
-    for input_text in input_texts:
-        input_text_with_eod: str = input_text.strip() + "\nEOD"
-        for text in input_text_with_eod.split("\n"):
-            if text == "EOD":
-                # hydra.utils.instantiateを実行する際に文字列${...}を補間しようとするのを防ぐ
-                normalized = OMEGACONF_VARIABLE_INTERPOLATION.sub(r"$␣\g<variable>", split_text)
-                # "#"で始まる行がコメント行と誤認識されることを防ぐ
-                normalized = normalized.replace("#", "♯").replace("＃", "♯")  # TODO: use "＃" instead of "♯"
-                split_texts.append(normalized.rstrip())
-                split_text = ""
-            else:
-                split_text += f"{text}\n"
-    return split_texts
+        print(self.processors[tasks[-1]].export_prediction(), end="")
+
+
+def _normalize_text(text: str) -> str:
+    # Tokenizers (BertJapaneseTokenizer, DebertaV2Tokenizer, etc.) apply NFKC normalization internally, so
+    # there may be inconsistency in number of characters if not applying NFKC normalization in advance
+    normalized = normalize("NFKC", text)
+    # escape several symbols and delete control characters
+    normalized = normalized.translate(TRANSLATION_TABLE)
+    # prevent hydra.utils.instantiate from interpolating the string "${...}"
+    normalized = OMEGACONF_VARIABLE_INTERPOLATION.sub(r"$␣\g<variable>", normalized)
+    # if normalized != text:
+    #     typer.echo(f"apply normalization ({text} -> {normalized})", err=True)
+    return normalized
+
+
+def _split_into_documents(input_text: str) -> List[str]:
+    documents: List[str] = []
+    document: str = ""
+    for line in input_text.split("\n"):
+        if line == "EOD":
+            documents.append(document.rstrip())
+            document = ""
+        else:
+            document += f"{line}\n"
+    else:
+        if document.rstrip() != "":
+            documents.append(document.rstrip())
+    return documents
 
 
 def version_callback(value: bool) -> None:
@@ -260,117 +264,115 @@ def version_callback(value: bool) -> None:
         raise typer.Exit()
 
 
-def model_size_callback(value: str) -> str:
-    if value not in ("tiny", "base", "large"):
-        raise typer.BadParameter("model size must be one of 'tiny', 'base', or 'large'")
-    return value
-
-
 def tasks_callback(value: str) -> str:
-    tasks: List[str] = value.split(",")
+    """sort and validate specified tasks"""
+    values: List[str] = [v for v in value.split(",") if v]
+    tasks: List[str] = []
+    for candidate_task in ("typo", "senter", "seq2seq", "char", "word"):
+        if candidate_task in values:
+            tasks.append(candidate_task)
+            values.remove(candidate_task)
+    if len(values) == 1:
+        raise typer.BadParameter(f"invalid task is specified: {repr(values[0])}")
+    if len(values) > 1:
+        raise typer.BadParameter(f"invalid tasks are specified: {', '.join(repr(v) for v in values)}")
     if len(tasks) == 0:
         raise typer.BadParameter("task must be specified")
-    for task in tasks:
-        if task not in ("typo", "seq2seq", "char", "word", "word_discourse"):
-            raise typer.BadParameter("invalid task name is contained")
     valid_task_combinations: Set[Tuple[str, ...]] = {
         ("typo",),
-        ("char",),
-        ("seq2seq",),
-        ("seq2seq", "typo"),
-        ("seq2seq", "word"),
-        ("seq2seq", "typo", "word"),
-        ("seq2seq", "word", "word_discourse"),
-        ("seq2seq", "typo", "word", "word_discourse"),
-        ("char", "typo"),
-        ("char", "word"),
-        ("char", "typo", "word"),
-        ("char", "word", "word_discourse"),
-        ("char", "typo", "word", "word_discourse"),
+        ("typo", "senter"),
+        ("typo", "senter", "char"),
+        ("typo", "senter", "char", "word"),
+        ("typo", "senter", "seq2seq"),
+        ("typo", "senter", "seq2seq", "word"),
+        ("senter",),
+        ("senter", "char"),
+        ("senter", "char", "word"),
+        ("senter", "seq2seq"),
+        ("senter", "seq2seq", "word"),
     }
-    sorted_task: Tuple[str, ...] = tuple(sorted(tasks))
-    if sorted_task not in valid_task_combinations:
+
+    if tuple(tasks) not in valid_task_combinations:
         raise typer.BadParameter(
             "task combination is invalid. "
-            "Please specify one of "
-            "'typo', "
-            "'char', "
-            "'seq2seq', "
-            "'typo,char', "
-            "'typo,seq2seq', "
-            "'seq2seq,word', "
-            "'char,word', "
-            "'typo,seq2seq,word', "
-            "'seq2seq,word,word_discourse', "
-            "'typo,seq2seq,word,word_discourse', "
-            "'typo,char,word', "
-            "'char,word,word_discourse' "
-            "or 'typo,char,word,word_discourse'."
+            f"Please specify one of {', '.join(repr(','.join(ts)) for ts in valid_task_combinations)}."
         )
-    return value
+    return ",".join(tasks)
 
 
 @app.command()
 def main(
     text: Optional[str] = typer.Option(None, help="Text to be analyzed."),
-    filename: Optional[Path] = typer.Option(None, help="File to be analyzed."),
-    device: Device = typer.Option(
-        Device.auto,
-        help="Device to be used. Please specify 'auto', 'cpu' or 'gpu'.",
-    ),
-    model_size: str = typer.Option(
-        "base", callback=model_size_callback, help="Model size to be used. Please specify 'tiny', 'base', or 'large'."
-    ),
-    typo_batch_size: int = typer.Option(1, help="Batch size for typo module."),
-    seq2seq_batch_size: int = typer.Option(1, help="Batch size for seq2seq module."),
-    char_batch_size: int = typer.Option(1, help="Batch size for char module."),
-    word_batch_size: int = typer.Option(1, help="Batch size for word module."),
-    tasks: str = typer.Option("typo,char,word,word_discourse", callback=tasks_callback, help="Tasks to be performed."),
+    filename: List[Path] = typer.Option([], dir_okay=False, help="Files to be analyzed."),
+    model_size: Optional[ModelSize] = typer.Option(None, help="Model size to be used."),
+    device: Optional[Device] = typer.Option(None, help="Device to be used."),
+    typo_batch_size: Optional[int] = typer.Option(None, help="Batch size for typo module."),
+    senter_batch_size: Optional[int] = typer.Option(None, help="Batch size for senter module."),
+    seq2seq_batch_size: Optional[int] = typer.Option(None, help="Batch size for seq2seq module."),
+    char_batch_size: Optional[int] = typer.Option(None, help="Batch size for char module."),
+    word_batch_size: Optional[int] = typer.Option(None, help="Batch size for word module."),
+    tasks: str = typer.Option("senter,char,word", callback=tasks_callback, help="Tasks to be performed."),
     _: Optional[bool] = typer.Option(None, "--version", callback=version_callback, is_eager=True),
+    config_file: Optional[Path] = typer.Option(None, help="Path to KWJA config file."),
 ) -> None:
     input_text: Optional[str] = None
-    if text is not None and filename is not None:
+    if text is not None and len(filename) > 0:
         typer.echo("ERROR: Please provide text or filename, not both", err=True)
         raise typer.Abort()
     elif text is not None:
         input_text = text
-    elif filename is not None:
-        input_text = Path(filename).read_text()
+    elif len(filename) > 0:
+        input_text = "".join(path.read_text().rstrip("\n") + "\nEOD\n" for path in filename)
+    elif sys.stdin.isatty() is False:
+        input_text = sys.stdin.read()
+    else:
+        pass  # interactive mode
 
-    if model_size == "large" and "seq2seq" in tasks:
-        typer.echo("ERROR: Large model does not support seq2seq module now", err=True)
-        raise typer.Abort()
+    specified_tasks: List[str] = tasks.split(",")
 
-    processor = CLIProcessor(
-        specified_device=device.value,
-        model_size=model_size,
-        typo_batch_size=typo_batch_size,
-        seq2seq_batch_size=seq2seq_batch_size,
-        char_batch_size=char_batch_size,
-        word_batch_size=word_batch_size,
-    )
-    specified_tasks: List[str] = [
-        task for task in ["typo", "seq2seq", "char", "word", "word_discourse"] if task in tasks.split(",")
-    ]
-
-    if input_text is None:
-        processor.load_modules(specified_tasks)
-
-        typer.echo('Please end your input with a new line and type "EOD"', err=True)
-        input_text = ""
-        while True:
-            input_ = input()
-            if input_ == "EOD":
-                processor.refresh()
-                processor.run(input_text, specified_tasks, interactive=True)
-                print("EOD")  # To indicate the end of the output.
-                input_text = ""
-            else:
-                input_text += input_ + "\n"
+    if config_file is None:
+        config_file = get_kwja_config_file()
+    if config_file.exists():
+        config = CLIConfig.from_yaml(config_file)
     else:
-        if input_text.strip() == "":
-            raise typer.Exit()
-        processor.run(input_text, specified_tasks)
+        config = CLIConfig()
+    if model_size is not None:
+        config.model_size = model_size
+    if device is not None:
+        config.device = device
+    if typo_batch_size is not None:
+        config.typo_batch_size = typo_batch_size
+    if senter_batch_size is not None:
+        config.senter_batch_size = senter_batch_size
+    if seq2seq_batch_size is not None:
+        config.seq2seq_batch_size = seq2seq_batch_size
+    if char_batch_size is not None:
+        config.char_batch_size = char_batch_size
+    if word_batch_size is not None:
+        config.word_batch_size = word_batch_size
+
+    processor = CLIProcessor(config)
+
+    # Batch mode
+    if input_text is not None:
+        if input_text.strip() != "":
+            processor.run(_split_into_documents(input_text), specified_tasks)
+        processor.refresh()
+        raise typer.Exit()
+
+    # Interactive mode
+    processor.load_modules(specified_tasks)
+    typer.echo('Please end your input with a new line and type "EOD"', err=True)
+    input_text = ""
+    while True:
+        input_ = input()
+        if input_ == "EOD":
+            processor.refresh()
+            processor.run([input_text], specified_tasks, interactive=True)
+            print("EOD")  # To indicate the end of the output.
+            input_text = ""
+        else:
+            input_text += input_ + "\n"
 
 
 if __name__ == "__main__":
diff --git a/src/kwja/cli/config.py b/src/kwja/cli/config.py
new file mode 100644
index 00000000..98eb4375
--- /dev/null
+++ b/src/kwja/cli/config.py
@@ -0,0 +1,59 @@
+import os
+from dataclasses import dataclass
+from enum import Enum
+from pathlib import Path
+
+import yaml
+
+ENV_KWJA_CONFIG_FILE = "KWJA_CONFIG_FILE"
+ENV_XDG_CONFIG_HOME = "XDG_CONFIG_HOME"
+DEFAULT_CONFIG_DIR = Path.home() / ".config"
+
+
+def get_kwja_config_file() -> Path:
+    if path := os.getenv(ENV_KWJA_CONFIG_FILE):
+        return Path(path)
+    config_dir = DEFAULT_CONFIG_DIR
+    if path := os.getenv(ENV_XDG_CONFIG_HOME):
+        config_dir = Path(path)
+    return config_dir / "kwja" / "config.yaml"
+
+
+class Device(str, Enum):
+    auto = "auto"
+    cpu = "cpu"
+    gpu = "gpu"
+
+
+class ModelSize(str, Enum):
+    tiny = "tiny"
+    base = "base"
+    large = "large"
+
+
+@dataclass
+class CLIConfig:
+    model_size: ModelSize = ModelSize.base
+    device: Device = Device.auto
+    num_workers: int = 0
+    torch_compile: bool = False
+    typo_batch_size: int = 1
+    senter_batch_size: int = 1
+    seq2seq_batch_size: int = 1
+    char_batch_size: int = 1
+    word_batch_size: int = 1
+
+    @classmethod
+    def from_yaml(cls, path: Path) -> "CLIConfig":
+        config = yaml.safe_load(path.read_text())
+        return cls(
+            model_size=ModelSize(config["model_size"]),
+            device=Device(config["device"]),
+            num_workers=config["num_workers"],
+            torch_compile=config["torch_compile"],
+            typo_batch_size=config["typo_batch_size"],
+            senter_batch_size=config["senter_batch_size"],
+            seq2seq_batch_size=config["seq2seq_batch_size"],
+            char_batch_size=config["char_batch_size"],
+            word_batch_size=config["word_batch_size"],
+        )
diff --git a/src/kwja/cli/utils.py b/src/kwja/cli/utils.py
index 64610c37..8e32f60c 100644
--- a/src/kwja/cli/utils.py
+++ b/src/kwja/cli/utils.py
@@ -1,15 +1,14 @@
 import logging
 import os
 import sys
-import warnings
 from pathlib import Path
 from typing import Dict, Optional, Tuple, Union
 
 import torch
-import transformers.utils.logging as hf_logging
 from torch.hub import download_url_to_file
 
 import kwja
+from kwja.cli.config import ModelSize
 
 logger = logging.getLogger(__name__)
 
@@ -18,43 +17,33 @@
 DEFAULT_CACHE_DIR = Path.home() / ".cache"
 
 _CHECKPOINT_BASE_URL = "https://lotus.kuee.kyoto-u.ac.jp"
-_CHECKPOINT_FILE_NAMES: Dict[str, Dict[str, str]] = {
-    "tiny": {
+_CHECKPOINT_FILE_NAMES: Dict[ModelSize, Dict[str, str]] = {
+    ModelSize.tiny: {
         "typo": "typo_deberta-v2-tiny-wwm.ckpt",
-        "seq2seq": "seq2seq_mt5-small.ckpt",
+        "seq2seq": "seq2seq_t5-small.ckpt",
         "char": "char_deberta-v2-tiny-wwm.ckpt",
         "word": "word_deberta-v2-tiny.ckpt",
-        "word_discourse": "disc_deberta-v2-tiny.ckpt",
     },
-    "base": {
+    ModelSize.base: {
         "typo": "typo_deberta-v2-base-wwm.ckpt",
-        "seq2seq": "seq2seq_mt5-base.ckpt",
+        "senter": "senter_deberta-v2-base-wwm.ckpt",
+        "seq2seq": "seq2seq_t5-base.ckpt",
         "char": "char_deberta-v2-base-wwm.ckpt",
         "word": "word_deberta-v2-base.ckpt",
-        "word_discourse": "disc_deberta-v2-base.ckpt",
     },
-    "large": {
+    ModelSize.large: {
         "typo": "typo_deberta-v2-large-wwm.ckpt",
-        "seq2seq": "seq2seq_mt5-large.ckpt",
+        "senter": "senter_deberta-v2-base-wwm.ckpt",  # We won't release the large model for now.
+        "seq2seq": "seq2seq_t5-large.ckpt",
         "char": "char_deberta-v2-large-wwm.ckpt",
         "word": "word_deberta-v2-large.ckpt",
-        "word_discourse": "disc_deberta-v2-large.ckpt",
     },
 }
 
 
-def suppress_debug_info() -> None:
-    warnings.filterwarnings("ignore")
-    logging.getLogger("kwja").setLevel(logging.ERROR)
-    logging.getLogger("rhoknp").setLevel(logging.ERROR)
-    logging.getLogger("torch").setLevel(logging.ERROR)
-    logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
-    hf_logging.set_verbosity(hf_logging.ERROR)
-
-
 def download_checkpoint(
     module: str,
-    model_size: str,
+    model_size: ModelSize,
     checkpoint_dir: Optional[Union[str, Path]] = None,
     progress: bool = True,
 ) -> Path:
@@ -62,8 +51,8 @@ def download_checkpoint(
     If the object is already present in `checkpoint_dir`, just return the path to the object.
 
     Args:
-        module: typo, char, word, or word_discourse
-        model_size: base or large
+        module: typo, char, or word
+        model_size: tiny, base or large
         checkpoint_dir: directory in which to save the object
         progress: whether to display a progress bar to stderr
     """
@@ -74,8 +63,7 @@ def download_checkpoint(
         checkpoint_dir = Path(checkpoint_dir)
     checkpoint_dir.mkdir(exist_ok=True, parents=True)
 
-    remote_checkpoint_path = Path("/kwja") / _get_model_version() / _CHECKPOINT_FILE_NAMES[model_size][module]
-    checkpoint_url = _CHECKPOINT_BASE_URL + str(remote_checkpoint_path)
+    checkpoint_url = f"{_CHECKPOINT_BASE_URL}/kwja/{_get_model_version()}/{_CHECKPOINT_FILE_NAMES[model_size][module]}"
     checkpoint_path = checkpoint_dir / _CHECKPOINT_FILE_NAMES[model_size][module]
     if checkpoint_path.exists() is False:
         sys.stderr.write(f'Downloading: "{checkpoint_url}" to {checkpoint_path}\n')
@@ -93,6 +81,8 @@ def _get_kwja_cache_dir() -> Path:
 
 
 def _get_model_version() -> str:
+    if "dev" in kwja.__version__:
+        return "dev"
     version_map = {
         ("1", "0"): "v1.0",
         ("1", "1"): "v1.0",
@@ -100,7 +90,7 @@ def _get_model_version() -> str:
         ("1", "3"): "v1.3",
         ("1", "4"): "v1.3",
         ("2", "0"): "v2.0",
-        # ("2", "1"): "v2.0",
+        ("2", "1"): "v2.1",
     }
     version_fields = kwja.__version__.split(".")
     return version_map[(version_fields[0], version_fields[1])]
diff --git a/src/kwja/datamodule/datamodule.py b/src/kwja/datamodule/datamodule.py
index 2cebf294..b8a43ea0 100644
--- a/src/kwja/datamodule/datamodule.py
+++ b/src/kwja/datamodule/datamodule.py
@@ -19,14 +19,13 @@
     WordDataset,
     WordInferenceDataset,
 )
+from kwja.utils.constants import IGNORE_INDEX
 
 
 class DataModule(pl.LightningDataModule):
     def __init__(self, cfg: DictConfig) -> None:
         super().__init__()
         self.cfg: DictConfig = cfg
-        self.batch_size: int = cfg.batch_size
-        self.num_workers: int = cfg.num_workers
 
         self.train_dataset: Optional[ConcatDataset] = None
         self.valid_datasets: Dict[str, Union[TypoDataset, Seq2SeqDataset, CharDataset, WordDataset]] = {}
@@ -59,37 +58,77 @@ def setup(self, stage: Optional[str] = None) -> None:
 
     def train_dataloader(self) -> DataLoader:
         assert self.train_dataset is not None
-        return self._get_dataloader(dataset=self.train_dataset, shuffle=True)
+        return self._get_dataloader(self.train_dataset, shuffle=True)
 
-    def val_dataloader(self) -> List[DataLoader]:
-        return [self._get_dataloader(dataset, shuffle=False) for dataset in self.valid_datasets.values()]
+    def val_dataloader(self) -> Dict[str, DataLoader]:
+        return {corpus: self._get_dataloader(dataset, shuffle=False) for corpus, dataset in self.valid_datasets.items()}
 
-    def test_dataloader(self) -> List[DataLoader]:
-        return [self._get_dataloader(dataset, shuffle=False) for dataset in self.test_datasets.values()]
+    def test_dataloader(self) -> Dict[str, DataLoader]:
+        return {corpus: self._get_dataloader(dataset, shuffle=False) for corpus, dataset in self.test_datasets.items()}
 
     def predict_dataloader(self) -> DataLoader:
         assert isinstance(self.predict_dataset, Dataset)
         return self._get_dataloader(self.predict_dataset, shuffle=False)
 
-    def _get_dataloader(self, dataset: Dataset, shuffle: bool) -> DataLoader:
+    def _get_dataloader(self, dataset: Dataset, shuffle: bool = False) -> DataLoader:
+        if self.cfg.dataset_type == "word":
+            collate_fn = word_dataclass_data_collator
+        else:
+            collate_fn = token_dataclass_data_collator
         return DataLoader(
             dataset=dataset,
-            batch_size=self.batch_size,
+            batch_size=self.cfg.batch_size,
             shuffle=shuffle,
-            num_workers=self.num_workers,
-            collate_fn=dataclass_data_collator,
+            num_workers=self.cfg.num_workers,
+            collate_fn=collate_fn,
             pin_memory=True,
         )
 
 
-def dataclass_data_collator(features: List[Any]) -> Dict[str, Union[Tensor, List[str]]]:
-    first: Any = features[0]
-    assert is_dataclass(first), "Data must be a dataclass"
+def token_dataclass_data_collator(batch_features: List[Any]) -> Dict[str, Union[Tensor, List[str]]]:
+    first_features: Any = batch_features[0]
+    assert is_dataclass(first_features), "Data must be a dataclass"
+
+    token_indices = torch.arange(max(sum(getattr(fs, "attention_mask")) for fs in batch_features))
+
+    batch: Dict[str, Union[Tensor, List[str]]] = {}
+    for field in fields(first_features):
+        features = [getattr(fs, field.name) for fs in batch_features]
+        if field.name in {"src_text"}:
+            batch[field.name] = features
+        else:
+            value = torch.as_tensor(features)
+            if value.ndim == 1 or value.size(1) == 0:
+                pass
+            elif field.name in {"seq2seq_labels"}:
+                target_indices = torch.arange(value.ne(IGNORE_INDEX).sum(dim=1).max().item())
+                value = value[:, target_indices]
+            else:
+                value = value[:, token_indices]
+            batch[field.name] = value
+    return batch
+
+
+def word_dataclass_data_collator(batch_features: List[Any]) -> Dict[str, Union[Tensor, List[str]]]:
+    first_features: Any = batch_features[0]
+    assert is_dataclass(first_features), "Data must be a dataclass"
+
+    token_indices = torch.arange(max(sum(getattr(fs, "attention_mask")) for fs in batch_features))
+    word_indices = torch.arange(max(sum(any(row) for row in getattr(fs, "subword_map")) for fs in batch_features))
+
     batch: Dict[str, Union[Tensor, List[str]]] = {}
-    for field in fields(first):
-        feats = [getattr(f, field.name) for f in features]
-        if "text" in field.name:
-            batch[field.name] = feats
+    for field in fields(first_features):
+        features = [getattr(fs, field.name) for fs in batch_features]
+        value = torch.as_tensor(features)
+        if value.ndim == 1 or value.size(1) == 0:
+            pass
+        elif field.name in {"input_ids", "attention_mask", "reading_labels"}:
+            value = value[:, token_indices]
+        elif field.name in {"subword_map", "reading_subword_map"}:
+            value = value[:, word_indices.unsqueeze(1), token_indices]
+        elif field.name in {"dependency_mask", "cohesion_mask", "cohesion_labels", "discourse_labels"}:
+            value = value[..., word_indices.unsqueeze(1), word_indices]
         else:
-            batch[field.name] = torch.as_tensor(feats)
+            value = value[:, word_indices]
+        batch[field.name] = value
     return batch
diff --git a/src/kwja/datamodule/datasets/base.py b/src/kwja/datamodule/datasets/base.py
index 65803d60..14b9135d 100644
--- a/src/kwja/datamodule/datasets/base.py
+++ b/src/kwja/datamodule/datasets/base.py
@@ -1,6 +1,5 @@
 import logging
 from abc import ABC
-from functools import cached_property
 from pathlib import Path
 from typing import Dict, Generic, List, TypeVar, Union
 
@@ -8,7 +7,7 @@
 from torch.utils.data import Dataset
 from transformers import PreTrainedTokenizerBase
 
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 from kwja.utils.sub_document import SequenceSplitter, SpanCandidate, to_sub_doc_id
 
 logger = logging.getLogger(__name__)
@@ -69,10 +68,6 @@ def __init__(
                 }
             )
 
-    @cached_property
-    def documents(self) -> List[Document]:
-        return list(self.doc_id2document.values())
-
     @staticmethod
     def _load_documents(document_dir: Path, ext: str) -> List[Document]:
         documents = []
diff --git a/src/kwja/datamodule/datasets/char.py b/src/kwja/datamodule/datasets/char.py
index 409886bf..88285b56 100644
--- a/src/kwja/datamodule/datasets/char.py
+++ b/src/kwja/datamodule/datasets/char.py
@@ -1,7 +1,7 @@
 import logging
 from dataclasses import dataclass
 from pathlib import Path
-from typing import List
+from typing import Dict, List
 from unicodedata import normalize
 
 from rhoknp import Document
@@ -17,7 +17,7 @@
     WORD_NORM_OP_TAGS,
     WORD_SEGMENTATION_TAGS,
 )
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 from kwja.utils.word_normalization import SentenceDenormalizer
 
 logger = logging.getLogger(__name__)
@@ -44,14 +44,14 @@ def __init__(
         super(CharDataset, self).__init__(tokenizer, max_seq_length)
         self.path = Path(path)
         self.denormalizer = SentenceDenormalizer()
-        self.denormalize_probability: float = denormalize_probability
+        self.denormalize_probability: float = denormalize_probability if "train" in self.path.parts else 0.0
         super(BaseDataset, self).__init__(self.path, tokenizer, max_seq_length, document_split_stride)
-        self.examples: List[CharExample] = self._load_examples(self.documents)
+        self.examples: List[CharExample] = self._load_examples(self.doc_id2document)
 
-    def _load_examples(self, documents: List[Document]) -> List[CharExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[CharExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             encoding: BatchEncoding = self.tokenizer(
                 document.text,
                 padding=PaddingStrategy.MAX_LENGTH,
diff --git a/src/kwja/datamodule/datasets/char_inference.py b/src/kwja/datamodule/datasets/char_inference.py
index 139039e2..9bcf0e48 100644
--- a/src/kwja/datamodule/datasets/char_inference.py
+++ b/src/kwja/datamodule/datasets/char_inference.py
@@ -1,19 +1,16 @@
 import logging
-from datetime import datetime
-from typing import List, Optional
-from unicodedata import normalize
+from pathlib import Path
+from typing import Dict, List, Optional
 
-from omegaconf import ListConfig
-from rhoknp import Document, RegexSenter
+from rhoknp import Document
 from transformers import BatchEncoding, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
-import kwja
 from kwja.datamodule.datasets.base import BaseDataset, FullAnnotatedDocumentLoaderMixin
 from kwja.datamodule.datasets.char import CharModuleFeatures
 from kwja.datamodule.examples import CharInferenceExample
-from kwja.utils.constants import TRANSLATION_TABLE
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
+from kwja.utils.reader import chunk_by_document_for_line_by_line_text
 
 logger = logging.getLogger(__name__)
 
@@ -21,22 +18,28 @@
 class CharInferenceDataset(BaseDataset[CharInferenceExample, CharModuleFeatures], FullAnnotatedDocumentLoaderMixin):
     def __init__(
         self,
-        texts: ListConfig,
         tokenizer: PreTrainedTokenizerBase,
         max_seq_length: int,
         document_split_stride: int = -1,
-        doc_id_prefix: Optional[str] = None,
+        senter_file: Optional[Path] = None,
         **_,
     ) -> None:
         super(CharInferenceDataset, self).__init__(tokenizer, max_seq_length)
-        documents = self._build_documents_from_texts(list(texts), doc_id_prefix)
+        if senter_file is not None:
+            with senter_file.open() as f:
+                documents = [
+                    Document.from_line_by_line_text(c)
+                    for c in track(chunk_by_document_for_line_by_line_text(f), description="Loading documents")
+                ]
+        else:
+            documents = []
         super(BaseDataset, self).__init__(documents, tokenizer, max_seq_length, document_split_stride)
-        self.examples: List[CharInferenceExample] = self._load_examples(self.documents)
+        self.examples: List[CharInferenceExample] = self._load_examples(self.doc_id2document)
 
-    def _load_examples(self, documents: List[Document]) -> List[CharInferenceExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[CharInferenceExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             encoding: BatchEncoding = self.tokenizer(
                 document.text,
                 padding=PaddingStrategy.MAX_LENGTH,
@@ -61,30 +64,3 @@ def encode(self, example: CharInferenceExample) -> CharModuleFeatures:
             word_segmentation_labels=[],
             word_norm_op_labels=[],
         )
-
-    @staticmethod
-    def _build_documents_from_texts(texts: List[str], doc_id_prefix: Optional[str]) -> List[Document]:
-        senter = RegexSenter()
-        # split text into sentences
-        documents: List[Document] = [
-            senter.apply_to_document(text) for text in track(texts, description="Loading documents")
-        ]
-        if doc_id_prefix is None:
-            doc_id_prefix = datetime.now().strftime("%Y%m%d%H%M")
-        doc_id_width = len(str(len(documents)))
-        sent_id_width = max((len(str(len(doc.sentences))) for doc in documents), default=0)
-        for document_index, document in enumerate(documents):
-            document.doc_id = f"{doc_id_prefix}-{document_index:0{doc_id_width}}"
-            for sentence_index, sentence in enumerate(document.sentences):
-                sentence.sid = f"{document.doc_id}-{sentence_index:0{sent_id_width}}"
-                sentence.misc_comment = f"kwja:{kwja.__version__}"
-        return documents
-
-    def _postprocess_document(self, document):
-        for sentence in document.sentences:
-            normalized = normalize("NFKC", sentence.text).translate(TRANSLATION_TABLE)
-            if normalized != sentence.text:
-                logger.warning(f"apply normalization ({sentence.text} -> {normalized})")
-                sentence.text = normalized
-        document.text = "".join(sentence.text for sentence in document.sentences)
-        return document
diff --git a/src/kwja/datamodule/datasets/senter.py b/src/kwja/datamodule/datasets/senter.py
index b4bbf46e..777256e7 100644
--- a/src/kwja/datamodule/datasets/senter.py
+++ b/src/kwja/datamodule/datasets/senter.py
@@ -1,7 +1,8 @@
 import logging
 from dataclasses import dataclass
 from pathlib import Path
-from typing import List
+from typing import Dict, List
+from unicodedata import normalize
 
 from rhoknp import Document
 from transformers import BatchEncoding, PreTrainedTokenizerBase
@@ -9,8 +10,8 @@
 
 from kwja.datamodule.datasets.base import BaseDataset, FullAnnotatedDocumentLoaderMixin
 from kwja.datamodule.examples import SenterExample
-from kwja.utils.constants import IGNORE_INDEX, SENT_SEGMENTATION_TAGS
-from kwja.utils.progress_bar import track
+from kwja.utils.constants import IGNORE_INDEX, SENT_SEGMENTATION_TAGS, TRANSLATION_TABLE
+from kwja.utils.logging_util import track
 
 logger = logging.getLogger(__name__)
 
@@ -34,12 +35,12 @@ def __init__(
         super(SenterDataset, self).__init__(tokenizer, max_seq_length)
         self.path = Path(path)
         super(BaseDataset, self).__init__(self.path, tokenizer, max_seq_length, document_split_stride)
-        self.examples: List[SenterExample] = self._load_examples(self.documents)
+        self.examples: List[SenterExample] = self._load_examples(self.doc_id2document)
 
-    def _load_examples(self, documents: List[Document]) -> List[SenterExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[SenterExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             encoding: BatchEncoding = self.tokenizer(
                 document.text,
                 padding=PaddingStrategy.MAX_LENGTH,
@@ -78,4 +79,10 @@ def _postprocess_document(self, document: Document) -> Document:
         for i in reversed(range(len(document.sentences))):
             if "括弧位置" in document.sentences[i].comment:
                 del document.sentences[i]
-        return document
+            else:
+                for morpheme in document.sentences[i].morphemes:
+                    normalized = normalize("NFKC", morpheme.text).translate(TRANSLATION_TABLE)
+                    if normalized != morpheme.text:
+                        logger.warning(f"apply normalization ({morpheme.text} -> {normalized})")
+                        morpheme.text = normalized
+        return document.reparse()
diff --git a/src/kwja/datamodule/datasets/senter_inference.py b/src/kwja/datamodule/datasets/senter_inference.py
index c10cfdfa..fc016ea9 100644
--- a/src/kwja/datamodule/datasets/senter_inference.py
+++ b/src/kwja/datamodule/datasets/senter_inference.py
@@ -1,7 +1,6 @@
 import logging
 from datetime import datetime
-from typing import List, Optional
-from unicodedata import normalize
+from typing import Dict, List, Optional
 
 from omegaconf import ListConfig
 from rhoknp import Document, RegexSenter
@@ -12,8 +11,7 @@
 from kwja.datamodule.datasets.base import BaseDataset, FullAnnotatedDocumentLoaderMixin
 from kwja.datamodule.datasets.senter import SenterModuleFeatures
 from kwja.datamodule.examples import SenterInferenceExample
-from kwja.utils.constants import TRANSLATION_TABLE
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 logger = logging.getLogger(__name__)
 
@@ -33,12 +31,12 @@ def __init__(
         super(SenterInferenceDataset, self).__init__(tokenizer, max_seq_length)
         documents = self._build_documents_from_texts(list(texts), doc_id_prefix)
         super(BaseDataset, self).__init__(documents, tokenizer, max_seq_length, document_split_stride)
-        self.examples: List[SenterInferenceExample] = self._load_examples(self.documents)
+        self.examples: List[SenterInferenceExample] = self._load_examples(self.doc_id2document)
 
-    def _load_examples(self, documents: List[Document]) -> List[SenterInferenceExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[SenterInferenceExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             encoding: BatchEncoding = self.tokenizer(
                 document.text,
                 padding=PaddingStrategy.MAX_LENGTH,
@@ -81,12 +79,3 @@ def _build_documents_from_texts(texts: List[str], doc_id_prefix: Optional[str])
                 sentence.sid = f"{document.doc_id}-{sent_idx:0{sent_id_width}}"
                 sentence.misc_comment = f"kwja:{kwja.__version__}"
         return documents
-
-    def _postprocess_document(self, document: Document) -> Document:
-        for sentence in document.sentences:
-            normalized = normalize("NFKC", sentence.text).translate(TRANSLATION_TABLE)
-            if normalized != sentence.text:
-                logger.warning(f"apply normalization ({sentence.text} -> {normalized})")
-                sentence.text = normalized
-        document.text = "".join(sentence.text for sentence in document.sentences)
-        return document
diff --git a/src/kwja/datamodule/datasets/seq2seq.py b/src/kwja/datamodule/datasets/seq2seq.py
index 36299d65..a9e48e59 100644
--- a/src/kwja/datamodule/datasets/seq2seq.py
+++ b/src/kwja/datamodule/datasets/seq2seq.py
@@ -4,13 +4,13 @@
 from typing import List
 
 from rhoknp import Document
-from torch.utils.data import Dataset
 from transformers import BatchEncoding, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
+from kwja.datamodule.datasets.base import BaseDataset
 from kwja.datamodule.examples import Seq2SeqExample
-from kwja.utils.constants import IGNORE_INDEX, NEW_LINE_TOKEN
-from kwja.utils.progress_bar import track
+from kwja.utils.constants import FULL_SPACE_TOKEN, IGNORE_INDEX, NEW_LINE_TOKEN
+from kwja.utils.logging_util import track
 from kwja.utils.seq2seq_format import get_seq2seq_format
 
 logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ class Seq2SeqModuleFeatures:
     seq2seq_labels: List[int]
 
 
-class Seq2SeqDataset(Dataset[Seq2SeqModuleFeatures]):
+class Seq2SeqDataset(BaseDataset[Seq2SeqExample, Seq2SeqModuleFeatures]):
     def __init__(
         self,
         path: str,
@@ -34,21 +34,15 @@ def __init__(
         max_tgt_length: int,
         ext: str = "knp",
     ) -> None:
+        super().__init__(tokenizer, max_src_length)
         self.path = Path(path)
 
-        self.tokenizer: PreTrainedTokenizerBase = tokenizer
         self.max_src_length: int = max_src_length
         self.max_tgt_length: int = max_tgt_length
 
         self.documents: List[Document] = self._load_documents(self.path, ext)
         self.examples: List[Seq2SeqExample] = self._load_examples(self.documents)
 
-    def __len__(self) -> int:
-        return len(self.examples)
-
-    def __getitem__(self, index: int) -> Seq2SeqModuleFeatures:
-        return self.encode(self.examples[index])
-
     @staticmethod
     def _load_documents(document_dir: Path, ext: str = "knp") -> List[Document]:
         documents = []
@@ -86,7 +80,7 @@ def _load_examples(self, documents: List[Document]) -> List[Seq2SeqExample]:
                 examples.append(
                     Seq2SeqExample(
                         example_id=example_id,
-                        src_text=sentence.text,
+                        src_text=sentence.text.strip().replace("\u3000", FULL_SPACE_TOKEN),
                         src_encoding=src_encoding,
                         tgt_encoding=tgt_encoding,
                         sid=sentence.sid,
@@ -95,17 +89,16 @@ def _load_examples(self, documents: List[Document]) -> List[Seq2SeqExample]:
                 example_id += 1
         if len(examples) == 0:
             logger.error(
-                "No examples to process. "
-                f"Make sure there exist any documents in {self.path} and they are not too long."
+                f"No examples to process. Make sure there exist any documents in {self.path} and they are not too long."
             )
         return examples
 
     def encode(self, example: Seq2SeqExample) -> Seq2SeqModuleFeatures:
-        seq2seq_labels: List[int] = [IGNORE_INDEX for _ in range(self.max_tgt_length)]
-        for seq2seq_global_index, seq2seq_tag in enumerate(example.tgt_encoding.input_ids):
-            if seq2seq_tag == self.tokenizer.pad_token_id:
-                continue
-            seq2seq_labels[seq2seq_global_index] = seq2seq_tag
+        seq2seq_labels: List[int] = [
+            (seq2seq_tag if seq2seq_tag != self.tokenizer.pad_token_id else IGNORE_INDEX)
+            for seq2seq_tag in example.tgt_encoding.input_ids
+        ]
+        assert len(seq2seq_labels) == self.max_tgt_length
 
         return Seq2SeqModuleFeatures(
             example_ids=example.example_id,
diff --git a/src/kwja/datamodule/datasets/seq2seq_inference.py b/src/kwja/datamodule/datasets/seq2seq_inference.py
index f7ff188b..d6a65983 100644
--- a/src/kwja/datamodule/datasets/seq2seq_inference.py
+++ b/src/kwja/datamodule/datasets/seq2seq_inference.py
@@ -1,46 +1,44 @@
 import logging
-from datetime import datetime
+from pathlib import Path
 from typing import List, Optional
 
-from omegaconf import ListConfig
-from rhoknp import KNP, Document, Jumanpp, RegexSenter
-from torch.utils.data import Dataset
+from rhoknp import Document
 from transformers import BatchEncoding, PreTrainedTokenizerBase
 from transformers.utils import PaddingStrategy
 
-import kwja
+from kwja.datamodule.datasets.base import BaseDataset
 from kwja.datamodule.datasets.seq2seq import Seq2SeqModuleFeatures
 from kwja.datamodule.examples import Seq2SeqInferenceExample
-from kwja.utils.progress_bar import track
+from kwja.utils.constants import FULL_SPACE_TOKEN
+from kwja.utils.logging_util import track
+from kwja.utils.reader import chunk_by_document_for_line_by_line_text
 
 logger = logging.getLogger(__name__)
 
-jumanpp = Jumanpp()
-knp = KNP()
 
-
-class Seq2SeqInferenceDataset(Dataset[Seq2SeqModuleFeatures]):
+class Seq2SeqInferenceDataset(BaseDataset[Seq2SeqInferenceExample, Seq2SeqModuleFeatures]):
     def __init__(
         self,
-        texts: ListConfig,
         tokenizer: PreTrainedTokenizerBase,
         max_src_length: int,
         max_tgt_length: int,
-        doc_id_prefix: Optional[str] = None,
+        senter_file: Optional[Path] = None,
+        **_,
     ) -> None:
-        self.tokenizer: PreTrainedTokenizerBase = tokenizer
+        super().__init__(tokenizer, max_src_length)
         self.max_src_length: int = max_src_length
         self.max_tgt_length: int = max_tgt_length
 
-        documents: List[Document] = self._build_documents_from_texts(list(texts), doc_id_prefix)
+        if senter_file is not None:
+            with senter_file.open() as f:
+                documents = [
+                    Document.from_line_by_line_text(c)
+                    for c in track(chunk_by_document_for_line_by_line_text(f), description="Loading documents")
+                ]
+        else:
+            documents = []
         self.examples: List[Seq2SeqInferenceExample] = self._load_example(documents)
 
-    def __len__(self) -> int:
-        return len(self.examples)
-
-    def __getitem__(self, index: int) -> Seq2SeqModuleFeatures:
-        return self.encode(self.examples[index])
-
     def _load_example(self, documents: List[Document]) -> List[Seq2SeqInferenceExample]:
         examples = []
         example_id: int = 0
@@ -55,7 +53,7 @@ def _load_example(self, documents: List[Document]) -> List[Seq2SeqInferenceExamp
                 examples.append(
                     Seq2SeqInferenceExample(
                         example_id=example_id,
-                        src_text=sentence.text.strip(),
+                        src_text=sentence.text.strip().replace("\u3000", FULL_SPACE_TOKEN),
                         src_encoding=src_encoding,
                         sid=sentence.sid,
                     )
@@ -65,8 +63,7 @@ def _load_example(self, documents: List[Document]) -> List[Seq2SeqInferenceExamp
             logger.error("No examples to process. Make sure any texts are given and they are not too long.")
         return examples
 
-    @staticmethod
-    def encode(example: Seq2SeqInferenceExample) -> Seq2SeqModuleFeatures:
+    def encode(self, example: Seq2SeqInferenceExample) -> Seq2SeqModuleFeatures:
         return Seq2SeqModuleFeatures(
             example_ids=example.example_id,
             src_text=example.src_text,
@@ -74,21 +71,3 @@ def encode(example: Seq2SeqInferenceExample) -> Seq2SeqModuleFeatures:
             attention_mask=example.src_encoding.attention_mask,
             seq2seq_labels=[],
         )
-
-    @staticmethod
-    def _build_documents_from_texts(texts: List[str], doc_id_prefix: Optional[str]) -> List[Document]:
-        senter = RegexSenter()
-        # split text into sentences
-        documents: List[Document] = [
-            senter.apply_to_document(text) for text in track(texts, description="Loading documents")
-        ]
-        if doc_id_prefix is None:
-            doc_id_prefix = datetime.now().strftime("%Y%m%d%H%M")
-        doc_id_width = len(str(len(documents)))
-        sent_id_width = max((len(str(len(doc.sentences))) for doc in documents), default=0)
-        for document_index, document in enumerate(documents):
-            document.doc_id = f"{doc_id_prefix}-{document_index:0{doc_id_width}}"
-            for sentence_index, sentence in enumerate(document.sentences):
-                sentence.sid = f"{document.doc_id}-{sentence_index:0{sent_id_width}}"
-                sentence.misc_comment = f"kwja:{kwja.__version__}"
-        return documents
diff --git a/src/kwja/datamodule/datasets/typo.py b/src/kwja/datamodule/datasets/typo.py
index 6039a5f3..d9a0faed 100644
--- a/src/kwja/datamodule/datasets/typo.py
+++ b/src/kwja/datamodule/datasets/typo.py
@@ -11,7 +11,7 @@
 from kwja.datamodule.datasets.base import BaseDataset
 from kwja.datamodule.examples import TypoExample
 from kwja.utils.constants import DUMMY_TOKEN, IGNORE_INDEX, RESOURCE_PATH, TYPO_CORR_OP_TAG2TOKEN
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 
 @dataclass(frozen=True)
diff --git a/src/kwja/datamodule/datasets/typo_inference.py b/src/kwja/datamodule/datasets/typo_inference.py
index 935e382a..93dc72d9 100644
--- a/src/kwja/datamodule/datasets/typo_inference.py
+++ b/src/kwja/datamodule/datasets/typo_inference.py
@@ -9,7 +9,7 @@
 from kwja.datamodule.datasets.typo import TypoModuleFeatures
 from kwja.datamodule.examples import TypoInferenceExample
 from kwja.utils.constants import DUMMY_TOKEN
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 
 class TypoInferenceDataset(BaseDataset[TypoInferenceExample, TypoModuleFeatures]):
diff --git a/src/kwja/datamodule/datasets/word.py b/src/kwja/datamodule/datasets/word.py
index be0d99c8..10894ab8 100644
--- a/src/kwja/datamodule/datasets/word.py
+++ b/src/kwja/datamodule/datasets/word.py
@@ -1,7 +1,7 @@
 import logging
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Dict, List, Literal, Union
+from typing import Dict, List, Literal, Optional, Union
 
 from omegaconf import ListConfig
 from rhoknp import Document, Sentence
@@ -11,7 +11,7 @@
 from transformers.utils import PaddingStrategy
 
 from kwja.datamodule.datasets.base import BaseDataset, FullAnnotatedDocumentLoaderMixin
-from kwja.datamodule.examples import WordExample
+from kwja.datamodule.examples import SpecialTokenIndexer, WordExample
 from kwja.utils.cohesion_analysis import BridgingUtils, CohesionBasePhrase, CohesionUtils, CoreferenceUtils, PasUtils
 from kwja.utils.constants import (
     BASE_PHRASE_FEATURES,
@@ -29,7 +29,7 @@
     CohesionTask,
 )
 from kwja.utils.kanjidic import KanjiDic
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 from kwja.utils.reading_prediction import ReadingAligner, get_reading2reading_id
 from kwja.utils.sub_document import extract_target_sentences
 
@@ -41,7 +41,6 @@ class WordModuleFeatures:
     example_ids: int
     input_ids: List[int]
     attention_mask: List[int]
-    target_mask: List[int]
     subword_map: List[List[bool]]
     reading_labels: List[int]
     reading_subword_map: List[List[bool]]
@@ -51,6 +50,7 @@ class WordModuleFeatures:
     conjform_labels: List[int]
     word_feature_labels: List[List[int]]
     ne_labels: List[int]
+    ne_mask: List[bool]
     base_phrase_feature_labels: List[List[int]]
     dependency_labels: List[int]
     dependency_mask: List[List[bool]]  # True/False = keep/mask
@@ -81,6 +81,9 @@ def __init__(
         else:
             self.tokenizer_input_format = "text"
         super(BaseDataset, self).__init__(self.path, tokenizer, max_seq_length, document_split_stride)
+        # some tags are not annotated in editorial articles
+        self.skip_cohesion_ne_discourse = self.path.parts[-2] == "kyoto_ed"
+
         # ---------- seq2seq ----------
         self.from_seq2seq: bool = False
 
@@ -92,80 +95,62 @@ def __init__(
         )
 
         # ---------- cohesion analysis ----------
-        self.cohesion_tasks: List[CohesionTask] = [CohesionTask(t) for t in cohesion_tasks]
-        self.exophora_referents = [ExophoraReferent(s) for s in exophora_referents]
-        self.restrict_cohesion_target = restrict_cohesion_target
+        self.cohesion_tasks: List[CohesionTask] = [CohesionTask(ct) for ct in cohesion_tasks]
+        self.exophora_referents = [ExophoraReferent(er) for er in exophora_referents]
         self.pas_cases: List[str] = list(pas_cases)
         self.br_cases: List[str] = list(br_cases)
-        self.cohesion_task2utils: Dict[CohesionTask, CohesionUtils] = {}
-        for cohesion_task in self.cohesion_tasks:
-            if cohesion_task == CohesionTask.PAS_ANALYSIS:
-                self.cohesion_task2utils[cohesion_task] = PasUtils(
-                    self.pas_cases, "all", self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
-            elif cohesion_task == CohesionTask.BRIDGING_REFERENCE_RESOLUTION:
-                self.cohesion_task2utils[cohesion_task] = BridgingUtils(
-                    self.br_cases, self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
-            elif cohesion_task == CohesionTask.COREFERENCE_RESOLUTION:
-                self.cohesion_task2utils[cohesion_task] = CoreferenceUtils(
-                    self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
+        self.cohesion_task2utils: Dict[CohesionTask, CohesionUtils] = {
+            ct: self._build_cohesion_utils(ct, restrict_cohesion_target) for ct in self.cohesion_tasks
+        }
 
         # ---------- dependency parsing & cohesion analysis ----------
         self.special_tokens: List[str] = list(special_tokens)
-        self.special_token2index: Dict[str, int] = {
-            st: self.max_seq_length - len(self.special_tokens) + i for i, st in enumerate(self.special_tokens)
-        }
-        self.index2special_token: Dict[int, str] = {v: k for k, v in self.special_token2index.items()}
         self.special_encoding: Encoding = self.tokenizer(
             self.special_tokens,
-            is_split_into_words=True,
+            add_special_tokens=False,
             padding=PaddingStrategy.DO_NOT_PAD,
             truncation=False,
-            add_special_tokens=False,
+            is_split_into_words=True,
         ).encodings[0]
 
-        self.examples: List[WordExample] = self._load_examples(self.documents)
+        self.examples: List[WordExample] = self._load_examples(self.doc_id2document)
 
     def _get_tokenized_len(self, document_or_sentence: Union[Document, Sentence]) -> int:
         tokenizer_input: Union[List[str], str] = [m.text for m in document_or_sentence.morphemes]
         if self.tokenizer_input_format == "text":
             tokenizer_input = " ".join(tokenizer_input)
-        encoding = self.tokenizer(
-            tokenizer_input, add_special_tokens=False, is_split_into_words=self.tokenizer_input_format == "words"
-        ).encodings[0]
-        return len(encoding.ids)
+        return len(self.tokenizer.tokenize(tokenizer_input, is_split_into_words=self.tokenizer_input_format == "words"))
 
-    def _load_examples(self, documents: List[Document]) -> List[WordExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[WordExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             tokenizer_input: Union[List[str], str] = [m.text for m in document.morphemes]
             if self.tokenizer_input_format == "text":
                 tokenizer_input = " ".join(tokenizer_input)
             encoding: Encoding = self.tokenizer(
                 tokenizer_input,
-                padding=PaddingStrategy.MAX_LENGTH,
+                padding=PaddingStrategy.DO_NOT_PAD,
                 truncation=False,
-                max_length=self.max_seq_length - self.num_special_tokens,
                 is_split_into_words=self.tokenizer_input_format == "words",
             ).encodings[0]
-            if len(encoding.ids) > self.max_seq_length - self.num_special_tokens:
+            if len(encoding.ids) > self.max_seq_length - len(self.special_tokens):
                 continue
+            padding_encoding: Encoding = self.tokenizer(
+                "",
+                add_special_tokens=False,
+                padding=PaddingStrategy.MAX_LENGTH,
+                truncation=False,
+                max_length=self.max_seq_length - len(encoding.ids) - len(self.special_tokens),
+            ).encodings[0]
+            merged_encoding: Encoding = Encoding.merge([encoding, self.special_encoding, padding_encoding])
 
-            example = WordExample(example_id, encoding)
+            special_token_indexer = SpecialTokenIndexer(self.special_tokens, len(encoding.ids), len(document.morphemes))
+
+            example = WordExample(example_id, merged_encoding, special_token_indexer)
             example.load_document(document, self.reading_aligner, self.cohesion_task2utils)
-            discourse_path = self.path / "disc_expert" / f"{document.doc_id}.knp"
-            if not discourse_path.exists() and self.path.name == "train":
-                discourse_path = self.path / "disc_crowd" / f"{document.doc_id}.knp"
-            if discourse_path.exists():
-                try:
-                    discourse_document = Document.from_knp(discourse_path.read_text())
-                    if document == discourse_document:
-                        example.load_discourse_document(discourse_document)
-                except AssertionError:
-                    logger.warning(f"{discourse_path} is not a valid KNP file")
+            if discourse_document := self._find_discourse_document(document):
+                example.load_discourse_document(discourse_document)
 
             examples.append(example)
             example_id += 1
@@ -177,45 +162,43 @@ def _load_examples(self, documents: List[Document]) -> List[WordExample]:
         return examples
 
     def encode(self, example: WordExample) -> WordModuleFeatures:
-        merged_encoding: Encoding = Encoding.merge([example.encoding, self.special_encoding])
         assert example.doc_id is not None, "doc_id isn't set"
         document = self.doc_id2document[example.doc_id]
 
-        target_mask = [0 for _ in range(self.max_seq_length)]
+        target_mask = [False] * self.max_seq_length
         for sentence in extract_target_sentences(document):
             for morpheme in sentence.morphemes:
-                target_mask[morpheme.global_index] = 1
+                target_mask[morpheme.global_index] = True
 
         # ---------- reading prediction ----------
         reading_labels = [IGNORE_INDEX] * self.max_seq_length
         if example.readings is not None:
             token_indices = [
                 (token_index, word_id)
-                for token_index, word_id in enumerate(merged_encoding.word_ids)
-                if token_index not in self.index2special_token and word_id is not None
+                for token_index, word_id in enumerate(example.encoding.word_ids)
+                if token_index not in example.special_token_indexer.token_level_indices and word_id is not None
             ]
             for (token_index, word_id), reading in zip(token_indices, example.readings):
-                if target_mask[word_id] == 1:
-                    decoded_token = self.tokenizer.decode(merged_encoding.ids[token_index])
-                    if decoded_token == reading:
-                        reading_labels[token_index] = self.reading2reading_id["[ID]"]
-                    else:
-                        reading_labels[token_index] = self.reading2reading_id.get(
-                            reading, self.reading2reading_id["[UNK]"]
-                        )
+                if target_mask[word_id] is False:
+                    continue
+                decoded_token = self.tokenizer.decode(example.encoding.ids[token_index])
+                if decoded_token == reading:
+                    reading_labels[token_index] = self.reading2reading_id["[ID]"]
+                else:
+                    reading_labels[token_index] = self.reading2reading_id.get(reading, self.reading2reading_id["[UNK]"])
 
         # NOTE: hereafter, indices are given at the word level
 
         # ---------- morphological analysis ----------
-        morpheme_attribute_tags_list = (POS_TAGS, SUBPOS_TAGS, CONJTYPE_TAGS, CONJFORM_TAGS)
-        morpheme_attribute_labels = tuple([IGNORE_INDEX] * self.max_seq_length for _ in morpheme_attribute_tags_list)
+        morpheme_attribute_tags_set = (POS_TAGS, SUBPOS_TAGS, CONJTYPE_TAGS, CONJFORM_TAGS)
+        morpheme_attribute_labels_set = tuple([IGNORE_INDEX] * self.max_seq_length for _ in morpheme_attribute_tags_set)
         for morpheme_global_index, morpheme_attributes in example.morpheme_global_index2morpheme_attributes.items():
-            for i, (morpheme_attribute, morpheme_attribute_tags) in enumerate(
-                zip(morpheme_attributes, morpheme_attribute_tags_list)
+            for morpheme_attribute, morpheme_attribute_tags, morpheme_attribute_labels in zip(
+                morpheme_attributes, morpheme_attribute_tags_set, morpheme_attribute_labels_set
             ):
-                if morpheme_attribute in morpheme_attribute_tags:
-                    morpheme_attribute_index = morpheme_attribute_tags.index(morpheme_attribute)
-                    morpheme_attribute_labels[i][morpheme_global_index] = morpheme_attribute_index
+                if morpheme_attribute not in morpheme_attribute_tags:
+                    continue
+                morpheme_attribute_labels[morpheme_global_index] = morpheme_attribute_tags.index(morpheme_attribute)
 
         # ---------- word feature tagging ----------
         word_feature_labels = [[IGNORE_INDEX] * len(WORD_FEATURES) for _ in range(self.max_seq_length)]
@@ -224,17 +207,14 @@ def encode(self, example: WordExample) -> WordModuleFeatures:
                 word_feature_labels[morpheme_global_index][i] = int(word_feature in word_feature_set)
 
         # ---------- ner ----------
-        ne_labels: List[int] = [
-            NE_TAGS.index("O") if target_mask[morpheme_global_index] == 1 else IGNORE_INDEX
-            for morpheme_global_index in range(self.max_seq_length)
-        ]
+        ne_mask = [False] * self.max_seq_length if self.skip_cohesion_ne_discourse is True else target_mask
+        ne_labels: List[int] = [NE_TAGS.index("O")] * self.max_seq_length
         for named_entity in example.named_entities:
             category = named_entity.category.value
             for i, morpheme in enumerate(named_entity.morphemes):
                 bi = "B" if i == 0 else "I"
                 assert ne_labels[morpheme.global_index] == NE_TAGS.index("O"), f"nested NE found in {example.doc_id}"
-                ne_index = NE_TAGS.index(f"{bi}-{category}")
-                ne_labels[morpheme.global_index] = ne_index
+                ne_labels[morpheme.global_index] = NE_TAGS.index(f"{bi}-{category}")
 
         # ---------- base phrase feature tagging ----------
         base_phrase_feature_labels = [[IGNORE_INDEX] * len(BASE_PHRASE_FEATURES) for _ in range(self.max_seq_length)]
@@ -243,19 +223,18 @@ def encode(self, example: WordExample) -> WordModuleFeatures:
                 base_phrase_feature_labels[morpheme_global_index][i] = int(base_phrase_feature in feature_set)
 
         # ---------- dependency parsing ----------
-        dependency_labels: List[int] = [IGNORE_INDEX for _ in range(self.max_seq_length)]
-        root_index = self.special_token2index["[ROOT]"]
+        dependency_labels: List[int] = [IGNORE_INDEX] * self.max_seq_length
+        root_index = example.special_token_indexer.get_morpheme_level_index("[ROOT]")
         for morpheme_global_index, dependency in example.morpheme_global_index2dependency.items():
-            dependency_labels[morpheme_global_index] = dependency if dependency >= 0 else root_index
+            dependency_labels[morpheme_global_index] = root_index if dependency == -1 else dependency
         dependency_mask = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
         for morpheme_global_index, head_candidates in example.morpheme_global_index2head_candidates.items():
             for head_candidate in head_candidates:
                 dependency_mask[morpheme_global_index][head_candidate.global_index] = True
             dependency_mask[morpheme_global_index][root_index] = True
-        dependency_type_labels: List[int] = [IGNORE_INDEX for _ in range(self.max_seq_length)]
+        dependency_type_labels: List[int] = [IGNORE_INDEX] * self.max_seq_length
         for morpheme_global_index, dependency_type in example.morpheme_global_index2dependency_type.items():
-            dependency_type_index = DEPENDENCY_TYPES.index(dependency_type)
-            dependency_type_labels[morpheme_global_index] = dependency_type_index
+            dependency_type_labels[morpheme_global_index] = DEPENDENCY_TYPES.index(dependency_type)
 
         # ---------- cohesion analysis ----------
         cohesion_labels: List[List[List[int]]] = []  # (rel, seq, seq)
@@ -263,35 +242,44 @@ def encode(self, example: WordExample) -> WordModuleFeatures:
         for cohesion_task, cohesion_utils in self.cohesion_task2utils.items():
             cohesion_base_phrases = example.cohesion_task2base_phrases[cohesion_task]
             for rel in cohesion_utils.rels:
-                rel_labels = self._convert_cohesion_base_phrases_into_rel_labels(cohesion_base_phrases, rel)
+                rel_labels = self._convert_cohesion_base_phrases_into_rel_labels(
+                    cohesion_base_phrases, rel, example.special_token_indexer
+                )
                 cohesion_labels.append(rel_labels)
-            rel_mask = self._convert_cohesion_base_phrases_into_rel_mask(cohesion_base_phrases)
+            rel_mask = self._convert_cohesion_base_phrases_into_rel_mask(
+                cohesion_base_phrases, example.special_token_indexer
+            )
             cohesion_mask.extend([rel_mask] * len(cohesion_utils.rels))
 
         # ---------- discourse parsing ----------
-        discourse_labels = [[IGNORE_INDEX for _ in range(self.max_seq_length)] for _ in range(self.max_seq_length)]
-        for (
-            modifier_morpheme_global_index,
-            head_morpheme_global_index,
-        ), relation in example.morpheme_global_indices2discourse_relation.items():
-            if relation in DISCOURSE_RELATIONS:
-                discourse_index = DISCOURSE_RELATIONS.index(relation)
+        discourse_labels = [[IGNORE_INDEX] * self.max_seq_length for _ in range(self.max_seq_length)]
+        if self.skip_cohesion_ne_discourse is False:
+            for (
+                morpheme_global_indices,
+                discourse_relation,
+            ) in example.morpheme_global_indices2discourse_relation.items():
+                if discourse_relation not in DISCOURSE_RELATIONS:
+                    continue
+                modifier_morpheme_global_index, head_morpheme_global_index = morpheme_global_indices
+                discourse_index = DISCOURSE_RELATIONS.index(discourse_relation)
                 discourse_labels[modifier_morpheme_global_index][head_morpheme_global_index] = discourse_index
 
         return WordModuleFeatures(
             example_ids=example.example_id,
-            input_ids=merged_encoding.ids,
-            attention_mask=merged_encoding.attention_mask,
-            target_mask=target_mask,
-            subword_map=self._get_subword_map(merged_encoding),
+            input_ids=example.encoding.ids,
+            attention_mask=example.encoding.attention_mask,
+            subword_map=self._generate_subword_map(example.encoding.word_ids, example.special_token_indexer),
             reading_labels=reading_labels,
-            reading_subword_map=self._get_subword_map(merged_encoding, include_special_tokens=False),
-            pos_labels=morpheme_attribute_labels[0],
-            subpos_labels=morpheme_attribute_labels[1],
-            conjtype_labels=morpheme_attribute_labels[2],
-            conjform_labels=morpheme_attribute_labels[3],
+            reading_subword_map=self._generate_subword_map(
+                example.encoding.word_ids, example.special_token_indexer, include_special_tokens=False
+            ),
+            pos_labels=morpheme_attribute_labels_set[0],
+            subpos_labels=morpheme_attribute_labels_set[1],
+            conjtype_labels=morpheme_attribute_labels_set[2],
+            conjform_labels=morpheme_attribute_labels_set[3],
             word_feature_labels=word_feature_labels,
             ne_labels=ne_labels,
+            ne_mask=ne_mask,
             base_phrase_feature_labels=base_phrase_feature_labels,
             dependency_labels=dependency_labels,
             dependency_mask=dependency_mask,
@@ -301,42 +289,61 @@ def encode(self, example: WordExample) -> WordModuleFeatures:
             discourse_labels=discourse_labels,
         )
 
-    def _get_subword_map(self, encoding: Encoding, include_special_tokens: bool = True) -> List[List[bool]]:
+    def _generate_subword_map(
+        self,
+        word_ids: List[Union[int, None]],
+        special_token_indexer: SpecialTokenIndexer,
+        include_special_tokens: bool = True,
+    ) -> List[List[bool]]:
         subword_map = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
-        for token_id, word_id in enumerate(encoding.word_ids):
-            if word_id is None or token_id in self.special_indices:
+        for token_index, word_id in enumerate(word_ids):
+            if word_id is None or token_index in special_token_indexer.token_level_indices:
                 continue
-            subword_map[word_id][token_id] = True
-        if include_special_tokens:
-            for special_index in self.special_indices:
-                subword_map[special_index][special_index] = True
+            subword_map[word_id][token_index] = True
+        if include_special_tokens is True:
+            for token_index, morpheme_global_index in special_token_indexer.token_and_morpheme_level_indices:
+                subword_map[morpheme_global_index][token_index] = True
         return subword_map
 
-    @property
-    def cohesion_special_indices(self) -> List[int]:
-        return [i for st, i in self.special_token2index.items() if st != "[ROOT]"]
-
-    @property
-    def num_special_tokens(self) -> int:
-        return len(self.special_tokens)
-
-    @property
-    def special_indices(self) -> List[int]:
-        return list(self.special_token2index.values())
+    def _find_discourse_document(self, document: Document) -> Optional[Document]:
+        discourse_path = self.path / "disc_expert" / f"{document.doc_id}.knp"
+        if not discourse_path.exists() and self.path.name == "train":
+            discourse_path = self.path / "disc_crowd" / f"{document.doc_id}.knp"
+        if discourse_path.exists():
+            try:
+                discourse_document = Document.from_knp(discourse_path.read_text())
+                if document == discourse_document:
+                    return discourse_document
+            except AssertionError:
+                logger.warning(f"{discourse_path} is not a valid KNP file")
+        return None
+
+    def _build_cohesion_utils(self, cohesion_task: CohesionTask, restrict_cohesion_target: bool) -> CohesionUtils:
+        if cohesion_task == CohesionTask.PAS_ANALYSIS:
+            return PasUtils(self.pas_cases, "all", self.exophora_referents, restrict_cohesion_target)
+        elif cohesion_task == CohesionTask.BRIDGING_REFERENCE_RESOLUTION:
+            return BridgingUtils(self.br_cases, self.exophora_referents, restrict_cohesion_target)
+        elif cohesion_task == CohesionTask.COREFERENCE_RESOLUTION:
+            return CoreferenceUtils(self.exophora_referents, restrict_cohesion_target)
+        else:
+            raise ValueError("invalid cohesion task")
 
     def _convert_cohesion_base_phrases_into_rel_labels(
         self,
         cohesion_base_phrases: List[CohesionBasePhrase],
         rel: str,
+        special_token_indexer: SpecialTokenIndexer,
     ) -> List[List[int]]:
-        rel_labels: List[List[int]] = [[0] * self.max_seq_length for _ in range(self.max_seq_length)]
+        rel_labels = [[0] * self.max_seq_length for _ in range(self.max_seq_length)]
+        if self.skip_cohesion_ne_discourse is True:
+            return rel_labels
         for cohesion_base_phrase in cohesion_base_phrases:
             if cohesion_base_phrase.is_target is False:
                 continue
             assert cohesion_base_phrase.rel2tags is not None, "rel2tags isn't set"
             for tag in cohesion_base_phrase.rel2tags[rel]:
-                if tag in self.special_token2index:
-                    target_morpheme_global_index = self.special_token2index[tag]
+                if tag in self.special_tokens:
+                    target_morpheme_global_index = special_token_indexer.get_morpheme_level_index(tag)
                 else:
                     # int(tag) is the base phrase global index of an endophora argument
                     target_morpheme_global_index = cohesion_base_phrases[int(tag)].head.global_index
@@ -346,6 +353,7 @@ def _convert_cohesion_base_phrases_into_rel_labels(
     def _convert_cohesion_base_phrases_into_rel_mask(
         self,
         cohesion_base_phrases: List[CohesionBasePhrase],
+        special_token_indexer: SpecialTokenIndexer,
     ) -> List[List[bool]]:
         rel_mask = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
         for cohesion_base_phrase in cohesion_base_phrases:
@@ -355,6 +363,6 @@ def _convert_cohesion_base_phrases_into_rel_mask(
             for morpheme in cohesion_base_phrase.morphemes:
                 for antecedent_candidate in cohesion_base_phrase.antecedent_candidates:
                     rel_mask[morpheme.global_index][antecedent_candidate.head.global_index] = True
-                for cohesion_special_index in self.cohesion_special_indices:
-                    rel_mask[morpheme.global_index][cohesion_special_index] = True
+                for morpheme_global_index in special_token_indexer.get_morpheme_level_indices(only_cohesion=True):
+                    rel_mask[morpheme.global_index][morpheme_global_index] = True
         return rel_mask
diff --git a/src/kwja/datamodule/datasets/word_inference.py b/src/kwja/datamodule/datasets/word_inference.py
index b8fdf67c..c0de8be4 100644
--- a/src/kwja/datamodule/datasets/word_inference.py
+++ b/src/kwja/datamodule/datasets/word_inference.py
@@ -12,10 +12,10 @@
 
 from kwja.datamodule.datasets.base import BaseDataset, FullAnnotatedDocumentLoaderMixin
 from kwja.datamodule.datasets.word import WordModuleFeatures
-from kwja.datamodule.examples import WordInferenceExample
+from kwja.datamodule.examples import SpecialTokenIndexer, WordInferenceExample
 from kwja.utils.cohesion_analysis import BridgingUtils, CohesionUtils, CoreferenceUtils, PasUtils
 from kwja.utils.constants import SPLIT_INTO_WORDS_MODEL_NAMES, CohesionTask
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 from kwja.utils.sub_document import extract_target_sentences
 
 logger = logging.getLogger(__name__)
@@ -59,75 +59,63 @@ def __init__(
         self.from_seq2seq: bool = juman_file is not None and juman_file.suffix == ".seq2seq"
 
         # ---------- cohesion analysis ----------
-        self.cohesion_tasks = [CohesionTask(t) for t in cohesion_tasks]
-        self.exophora_referents = [ExophoraReferent(s) for s in exophora_referents]
+        self.cohesion_tasks = [CohesionTask(ct) for ct in cohesion_tasks]
+        self.exophora_referents = [ExophoraReferent(er) for er in exophora_referents]
         self.pas_cases: List[str] = list(pas_cases)
         self.br_cases: List[str] = list(br_cases)
-        self.cohesion_task2utils: Dict[CohesionTask, CohesionUtils] = {}
-        for cohesion_task in self.cohesion_tasks:
-            if cohesion_task == CohesionTask.PAS_ANALYSIS:
-                self.cohesion_task2utils[cohesion_task] = PasUtils(
-                    self.pas_cases, "all", self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
-            elif cohesion_task == CohesionTask.BRIDGING_REFERENCE_RESOLUTION:
-                self.cohesion_task2utils[cohesion_task] = BridgingUtils(
-                    self.br_cases, self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
-            elif cohesion_task == CohesionTask.COREFERENCE_RESOLUTION:
-                self.cohesion_task2utils[cohesion_task] = CoreferenceUtils(
-                    self.exophora_referents, restrict_target=restrict_cohesion_target
-                )
+        self.cohesion_task2utils: Dict[CohesionTask, CohesionUtils] = {
+            ct: self._build_cohesion_utils(ct, restrict_cohesion_target) for ct in self.cohesion_tasks
+        }
 
         # ---------- dependency parsing & cohesion analysis ----------
         self.special_tokens: List[str] = list(special_tokens)
-        self.special_token2index: Dict[str, int] = {
-            st: self.max_seq_length - len(self.special_tokens) + i for i, st in enumerate(self.special_tokens)
-        }
-        self.index2special_token: Dict[int, str] = {v: k for k, v in self.special_token2index.items()}
         self.special_encoding: Encoding = self.tokenizer(
             self.special_tokens,
-            is_split_into_words=True,
+            add_special_tokens=False,
             padding=PaddingStrategy.DO_NOT_PAD,
             truncation=False,
-            add_special_tokens=False,
+            is_split_into_words=True,
         ).encodings[0]
 
-        self.examples: List[WordInferenceExample] = self._load_examples(self.documents)
+        self.examples: List[WordInferenceExample] = self._load_examples(self.doc_id2document)
 
     def _get_tokenized_len(self, document_or_sentence: Union[Document, Sentence]) -> int:
         tokenizer_input: Union[List[str], str] = [m.text for m in document_or_sentence.morphemes]
         if self.tokenizer_input_format == "text":
             tokenizer_input = " ".join(tokenizer_input)
-        encoding = self.tokenizer(
-            tokenizer_input, add_special_tokens=False, is_split_into_words=self.tokenizer_input_format == "words"
-        ).encodings[0]
-        return len(encoding.ids)
+        return len(self.tokenizer.tokenize(tokenizer_input, is_split_into_words=self.tokenizer_input_format == "words"))
 
-    def _load_examples(self, documents: List[Document]) -> List[WordInferenceExample]:
+    def _load_examples(self, doc_id2document: Dict[str, Document]) -> List[WordInferenceExample]:
         examples = []
         example_id = 0
-        for document in track(documents, description="Loading examples"):
+        for document in track(doc_id2document.values(), description="Loading examples"):
             tokenizer_input: Union[List[str], str] = [m.text for m in document.morphemes]
             if self.tokenizer_input_format == "text":
                 tokenizer_input = " ".join(tokenizer_input)
             encoding: Encoding = self.tokenizer(
                 tokenizer_input,
-                padding=PaddingStrategy.MAX_LENGTH,
+                padding=PaddingStrategy.DO_NOT_PAD,
                 truncation=False,
-                max_length=self.max_seq_length - self.num_special_tokens,
                 is_split_into_words=self.tokenizer_input_format == "words",
             ).encodings[0]
-            if len(encoding.ids) > self.max_seq_length - self.num_special_tokens:
-                continue
-            num_tokenized_morphemes = len({word_id for word_id in encoding.word_ids if word_id is not None})
-            if len(document.morphemes) != num_tokenized_morphemes:
-                logger.warning(f"Document length and tokenized length mismatch: {document.text}")
+            if len(encoding.ids) > self.max_seq_length - len(self.special_tokens):
                 continue
+            padding_encoding: Encoding = self.tokenizer(
+                "",
+                add_special_tokens=False,
+                padding=PaddingStrategy.MAX_LENGTH,
+                truncation=False,
+                max_length=self.max_seq_length - len(encoding.ids) - len(self.special_tokens),
+            ).encodings[0]
+            merged_encoding: Encoding = Encoding.merge([encoding, self.special_encoding, padding_encoding])
+
+            special_token_indexer = SpecialTokenIndexer(self.special_tokens, len(encoding.ids), len(document.morphemes))
 
             examples.append(
                 WordInferenceExample(
                     example_id=example_id,
-                    encoding=encoding,
+                    encoding=merged_encoding,
+                    special_token_indexer=special_token_indexer,
                     doc_id=document.doc_id,
                 )
             )
@@ -139,22 +127,22 @@ def _load_examples(self, documents: List[Document]) -> List[WordInferenceExample
     def encode(self, example: WordInferenceExample) -> WordModuleFeatures:
         document = self.doc_id2document[example.doc_id]
 
-        target_mask = [0 for _ in range(self.max_seq_length)]
+        # ---------- ner ----------
+        # True/False = keep/mask
+        ne_mask = [False] * self.max_seq_length
         for sentence in extract_target_sentences(document):
             for morpheme in sentence.morphemes:
-                target_mask[morpheme.global_index] = 1
+                ne_mask[morpheme.global_index] = True
 
         # ---------- dependency parsing ----------
-        # True/False = keep/mask
         dependency_mask = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
+        root_index = example.special_token_indexer.get_morpheme_level_index("[ROOT]")
         for sentence in document.sentences:
-            morpheme_global_indices = [morpheme.global_index for morpheme in sentence.morphemes]
-            start, stop = min(morpheme_global_indices), max(morpheme_global_indices) + 1
-            for i in range(start, stop):
-                for j in range(start, stop):
-                    if i != j:
-                        dependency_mask[i][j] = True
-                dependency_mask[i][self.special_token2index["[ROOT]"]] = True
+            for morpheme_src in sentence.morphemes:
+                for morpheme_tgt in sentence.morphemes:
+                    if morpheme_src.global_index != morpheme_tgt.global_index:
+                        dependency_mask[morpheme_src.global_index][morpheme_tgt.global_index] = True
+                dependency_mask[morpheme_src.global_index][root_index] = True
 
         # ---------- cohesion analysis ----------
         cohesion_mask: List[List[List[bool]]] = []  # (rel, seq, seq)
@@ -166,26 +154,27 @@ def encode(self, example: WordInferenceExample) -> WordModuleFeatures:
                     morpheme, morphemes
                 ):
                     rel_mask[morpheme.global_index][antecedent_candidate_morpheme.global_index] = True
-                for cohesion_special_index in self.cohesion_special_indices:
-                    rel_mask[morpheme.global_index][cohesion_special_index] = True
+                for morpheme_global_index in example.special_token_indexer.get_morpheme_level_indices(
+                    only_cohesion=True
+                ):
+                    rel_mask[morpheme.global_index][morpheme_global_index] = True
             cohesion_mask.extend([rel_mask] * len(cohesion_utils.rels))
-
-        merged_encoding: Encoding = Encoding.merge([example.encoding, self.special_encoding])
-
         return WordModuleFeatures(
             example_ids=example.example_id,
-            input_ids=merged_encoding.ids,
-            attention_mask=merged_encoding.attention_mask,
-            target_mask=target_mask,
-            subword_map=self._get_subword_map(merged_encoding),
+            input_ids=example.encoding.ids,
+            attention_mask=example.encoding.attention_mask,
+            subword_map=self._generate_subword_map(example.encoding.word_ids, example.special_token_indexer),
             reading_labels=[],
-            reading_subword_map=self._get_subword_map(merged_encoding, include_special_tokens=False),
+            reading_subword_map=self._generate_subword_map(
+                example.encoding.word_ids, example.special_token_indexer, include_special_tokens=False
+            ),
             pos_labels=[],
             subpos_labels=[],
             conjtype_labels=[],
             conjform_labels=[],
             word_feature_labels=[],
             ne_labels=[],
+            ne_mask=ne_mask,
             base_phrase_feature_labels=[],
             dependency_labels=[],
             dependency_mask=dependency_mask,
@@ -195,25 +184,28 @@ def encode(self, example: WordInferenceExample) -> WordModuleFeatures:
             discourse_labels=[],
         )
 
-    def _get_subword_map(self, encoding: Encoding, include_special_tokens: bool = True) -> List[List[bool]]:
+    def _generate_subword_map(
+        self,
+        word_ids: List[Union[int, None]],
+        special_token_indexer: SpecialTokenIndexer,
+        include_special_tokens: bool = True,
+    ) -> List[List[bool]]:
         subword_map = [[False] * self.max_seq_length for _ in range(self.max_seq_length)]
-        for token_id, word_id in enumerate(encoding.word_ids):
-            if word_id is None or token_id in self.special_indices:
+        for token_index, word_id in enumerate(word_ids):
+            if word_id is None or token_index in special_token_indexer.token_level_indices:
                 continue
-            subword_map[word_id][token_id] = True
-        if include_special_tokens:
-            for special_index in self.special_indices:
-                subword_map[special_index][special_index] = True
+            subword_map[word_id][token_index] = True
+        if include_special_tokens is True:
+            for token_index, morpheme_global_index in special_token_indexer.token_and_morpheme_level_indices:
+                subword_map[morpheme_global_index][token_index] = True
         return subword_map
 
-    @property
-    def cohesion_special_indices(self) -> List[int]:
-        return [i for st, i in self.special_token2index.items() if st != "[ROOT]"]
-
-    @property
-    def num_special_tokens(self) -> int:
-        return len(self.special_tokens)
-
-    @property
-    def special_indices(self) -> List[int]:
-        return list(self.special_token2index.values())
+    def _build_cohesion_utils(self, cohesion_task: CohesionTask, restrict_cohesion_target: bool) -> CohesionUtils:
+        if cohesion_task == CohesionTask.PAS_ANALYSIS:
+            return PasUtils(self.pas_cases, "all", self.exophora_referents, restrict_cohesion_target)
+        elif cohesion_task == CohesionTask.BRIDGING_REFERENCE_RESOLUTION:
+            return BridgingUtils(self.br_cases, self.exophora_referents, restrict_cohesion_target)
+        elif cohesion_task == CohesionTask.COREFERENCE_RESOLUTION:
+            return CoreferenceUtils(self.exophora_referents, restrict_cohesion_target)
+        else:
+            raise ValueError("invalid cohesion task")
diff --git a/src/kwja/datamodule/examples/__init__.py b/src/kwja/datamodule/examples/__init__.py
index c76c8cb3..d542dca3 100644
--- a/src/kwja/datamodule/examples/__init__.py
+++ b/src/kwja/datamodule/examples/__init__.py
@@ -2,7 +2,7 @@
 from .senter import SenterExample, SenterInferenceExample
 from .seq2seq import Seq2SeqExample, Seq2SeqInferenceExample
 from .typo import TypoExample, TypoInferenceExample
-from .word import WordExample, WordInferenceExample
+from .word import SpecialTokenIndexer, WordExample, WordInferenceExample
 
 __all__ = [
     "TypoExample",
@@ -11,6 +11,7 @@
     "Seq2SeqInferenceExample",
     "CharExample",
     "CharInferenceExample",
+    "SpecialTokenIndexer",
     "WordExample",
     "WordInferenceExample",
     "SenterExample",
diff --git a/src/kwja/datamodule/examples/word.py b/src/kwja/datamodule/examples/word.py
index 15f5fa96..d84e9091 100644
--- a/src/kwja/datamodule/examples/word.py
+++ b/src/kwja/datamodule/examples/word.py
@@ -1,5 +1,6 @@
 import logging
 from dataclasses import dataclass
+from functools import cached_property
 from typing import Dict, List, Optional, Set, Tuple
 
 from rhoknp import BasePhrase, Clause, Document, Morpheme, Phrase
@@ -20,10 +21,40 @@
 logger = logging.getLogger(__name__)
 
 
+class SpecialTokenIndexer:
+    def __init__(self, special_tokens: List[str], num_tokens: int, num_morphemes: int) -> None:
+        self.special_tokens: List[str] = special_tokens
+        self._special_token2token_level_index: Dict[str, int] = {
+            st: num_tokens + i for i, st in enumerate(special_tokens)
+        }
+        self._special_token2morpheme_level_index: Dict[str, int] = {
+            st: num_morphemes + i for i, st in enumerate(special_tokens)
+        }
+
+    def get_morpheme_level_index(self, special_token: str) -> int:
+        return self._special_token2morpheme_level_index[special_token]
+
+    def get_morpheme_level_indices(self, only_cohesion: bool = False) -> List[int]:
+        return [
+            i for st, i in self._special_token2morpheme_level_index.items() if not (only_cohesion and st == "[ROOT]")
+        ]
+
+    @cached_property
+    def token_level_indices(self) -> List[int]:
+        return list(self._special_token2token_level_index.values())
+
+    @cached_property
+    def token_and_morpheme_level_indices(self) -> List[Tuple[int, int]]:
+        return list(
+            zip(self._special_token2token_level_index.values(), self._special_token2morpheme_level_index.values())
+        )
+
+
 class WordExample:
-    def __init__(self, example_id: int, encoding: Encoding) -> None:
+    def __init__(self, example_id: int, encoding: Encoding, special_token_indexer: SpecialTokenIndexer) -> None:
         self.example_id = example_id
         self.encoding = encoding
+        self.special_token_indexer = special_token_indexer
         self.doc_id: Optional[str] = None
 
         # ---------- reading prediction ----------
@@ -158,4 +189,5 @@ def _set_discourse_relation(self, clauses: List[Clause]) -> None:
 class WordInferenceExample:
     example_id: int
     encoding: Encoding
+    special_token_indexer: SpecialTokenIndexer
     doc_id: str
diff --git a/src/kwja/metrics/base.py b/src/kwja/metrics/base.py
index 99439f6b..ce0dc3c6 100644
--- a/src/kwja/metrics/base.py
+++ b/src/kwja/metrics/base.py
@@ -22,6 +22,15 @@ def update(self, kwargs: Dict[str, torch.Tensor]) -> None:
             if isinstance(value, torch.BoolTensor):
                 value = value.long()
             state.append(value)
+            # # The following code works for SingleDeviceStrategy, but not for DDPStrategy.
+            # if len(value.size()) == 0:
+            #     value = value.view(1)  # (1)
+            # if isinstance(state, torch.Tensor):
+            #     new_value = torch.cat([state, value], dim=0)
+            # else:
+            #     assert len(state) == 0, f"state {state_name} is not empty"
+            #     new_value = value
+            # setattr(self, state_name, new_value)
 
     def set_properties(self, kwargs: Dict[str, Any]) -> None:
         for key, value in kwargs.items():
diff --git a/src/kwja/metrics/char.py b/src/kwja/metrics/char.py
index 84b0742c..1bf657d1 100644
--- a/src/kwja/metrics/char.py
+++ b/src/kwja/metrics/char.py
@@ -30,6 +30,21 @@ def __init__(self) -> None:
         self.word_norm_op_predictions: torch.Tensor
         self.word_norm_op_labels: torch.Tensor
 
+    @staticmethod
+    def pad(kwargs: Dict[str, torch.Tensor], max_seq_length: int) -> None:
+        for key, value in kwargs.items():
+            if key in {"example_ids"}:
+                continue
+            else:
+                dims = [1]
+            for dim in dims:
+                size = [max_seq_length - s if i == dim else s for i, s in enumerate(value.size())]
+                if size[dim] == 0:
+                    continue
+                padding = torch.zeros(size, dtype=value.dtype, device=value.device)
+                value = torch.cat([value, padding], dim=dim)
+            kwargs[key] = value
+
     def compute(self) -> Dict[str, float]:
         sorted_indices = unique(self.example_ids)
         for state_name in self.STATE_NAMES:
@@ -88,12 +103,7 @@ def compute_word_segmentation_metrics(
         predictions = [self._convert_document_into_segmentation_tags(d) for d in predicted_documents]
         return {
             "word_segmentation_accuracy": accuracy_score(y_true=labels, y_pred=predictions),
-            "word_segmentation_f1": f1_score(
-                y_true=labels,
-                y_pred=predictions,
-                mode="strict",
-                scheme=IOB2,
-            ),
+            "word_segmentation_f1": f1_score(y_true=labels, y_pred=predictions, mode="strict", scheme=IOB2).item(),
         }
 
     @staticmethod
diff --git a/src/kwja/metrics/senter.py b/src/kwja/metrics/senter.py
index 4107a75a..5eef3a4f 100644
--- a/src/kwja/metrics/senter.py
+++ b/src/kwja/metrics/senter.py
@@ -25,6 +25,21 @@ def __init__(self) -> None:
         self.example_ids: torch.Tensor
         self.sent_segmentation_predictions: torch.Tensor
 
+    @staticmethod
+    def pad(kwargs: Dict[str, torch.Tensor], max_seq_length: int) -> None:
+        for key, value in kwargs.items():
+            if key in {"example_ids"}:
+                continue
+            else:
+                dims = [1]
+            for dim in dims:
+                size = [max_seq_length - s if i == dim else s for i, s in enumerate(value.size())]
+                if size[dim] == 0:
+                    continue
+                padding = torch.zeros(size, dtype=value.dtype, device=value.device)
+                value = torch.cat([value, padding], dim=dim)
+            kwargs[key] = value
+
     def compute(self) -> Dict[str, float]:
         sorted_indices = unique(self.example_ids)
         for state_name in self.STATE_NAMES:
@@ -75,12 +90,7 @@ def compute_sent_segmentation_metrics(
         predictions = [self._convert_document_into_segmentation_tags(d) for d in predicted_documents]
         return {
             "sent_segmentation_accuracy": accuracy_score(y_true=labels, y_pred=predictions),
-            "sent_segmentation_f1": f1_score(
-                y_true=labels,
-                y_pred=predictions,
-                mode="strict",
-                scheme=IOB2,
-            ),
+            "sent_segmentation_f1": f1_score(y_true=labels, y_pred=predictions, mode="strict", scheme=IOB2).item(),
         }
 
     @staticmethod
diff --git a/src/kwja/metrics/typo.py b/src/kwja/metrics/typo.py
index 4a1e6c86..bfd2fe30 100644
--- a/src/kwja/metrics/typo.py
+++ b/src/kwja/metrics/typo.py
@@ -28,6 +28,21 @@ def __init__(self, confidence_thresholds: Tuple[float, ...] = (0.0, 0.8, 0.9)) -
         self.ins_predictions: torch.Tensor
         self.ins_probabilities: torch.Tensor
 
+    @staticmethod
+    def pad(kwargs: Dict[str, torch.Tensor], max_seq_length: int) -> None:
+        for key, value in kwargs.items():
+            if key in {"example_ids"}:
+                continue
+            else:
+                dims = [1]
+            for dim in dims:
+                size = [max_seq_length - s if i == dim else s for i, s in enumerate(value.size())]
+                if size[dim] == 0:
+                    continue
+                padding = torch.zeros(size, dtype=value.dtype, device=value.device)
+                value = torch.cat([value, padding], dim=dim)
+            kwargs[key] = value
+
     def compute(self) -> Dict[str, float]:
         sorted_indices = unique(self.example_ids)
         for state_name in self.STATE_NAMES:
diff --git a/src/kwja/metrics/word.py b/src/kwja/metrics/word.py
index eb0149ea..afbc4c6c 100644
--- a/src/kwja/metrics/word.py
+++ b/src/kwja/metrics/word.py
@@ -31,6 +31,7 @@
     CONJTYPE_TAGS,
     DISCOURSE_RELATIONS,
     IGNORE_INDEX,
+    MASKED,
     POS_TAGS,
     SUBPOS_TAGS,
     WORD_FEATURES,
@@ -81,6 +82,27 @@ def __init__(self) -> None:
         self.discourse_predictions: torch.Tensor
         self.discourse_labels: torch.Tensor
 
+    @staticmethod
+    def pad(kwargs: Dict[str, torch.Tensor], max_seq_length: int) -> None:
+        for key, value in kwargs.items():
+            if key in {"example_ids"}:
+                continue
+            elif key in {"reading_subword_map", "cohesion_logits", "discourse_predictions", "discourse_labels"}:
+                dims = [value.ndim - 2, value.ndim - 1]
+            else:
+                dims = [1]
+            for dim in dims:
+                size = [max_seq_length - s if i == dim else s for i, s in enumerate(value.size())]
+                if size[dim] == 0:
+                    continue
+                if key in {"discourse_labels"}:
+                    fill_value: Union[float, int] = IGNORE_INDEX
+                else:
+                    fill_value = MASKED if torch.is_floating_point(value) else 0
+                padding = torch.full(size, fill_value, dtype=value.dtype, device=value.device)
+                value = torch.cat([value, padding], dim=dim)
+            kwargs[key] = value
+
     def compute(self) -> Dict[str, float]:
         assert self.training_tasks is not None, "training_tasks isn't set"
 
@@ -126,33 +148,19 @@ def _build_documents(self) -> Tuple[List[Document], ...]:
             example_id,
             reading_predictions,
             reading_subword_map,
-            pos_logits,
-            subpos_logits,
-            conjtype_logits,
-            conjform_logits,
+            *morpheme_attribute_logits,  # pos_logits, subpos_logits, conjtype_logits, conjform_logits
             word_feature_probabilities,
             ne_predictions,
             base_phrase_feature_probabilities,
             dependency_predictions,
             dependency_type_predictions,
             cohesion_logits,
-        ) in zip(
-            self.example_ids.tolist(),
-            self.reading_predictions.tolist(),
-            self.reading_subword_map.tolist(),
-            self.pos_logits,
-            self.subpos_logits,
-            self.conjtype_logits,
-            self.conjform_logits,
-            self.word_feature_probabilities.tolist(),
-            self.ne_predictions,
-            self.base_phrase_feature_probabilities.tolist(),
-            self.dependency_predictions.tolist(),
-            self.dependency_type_predictions.tolist(),
-            self.cohesion_logits.tolist(),
-        ):
+            _,  # discourse_predictions
+            _,  # discourse_labels
+        ) in zip(*[getattr(self, state_name).tolist() for state_name in self.STATE_NAMES]):
             example = self.dataset.examples[example_id]
             gold_document = self.dataset.doc_id2document[example.doc_id]
+            num_morphemes = len(gold_document.morphemes)
             orig_doc_id = to_orig_doc_id(gold_document.doc_id)
 
             word_reading_predictions = get_word_reading_predictions(
@@ -162,20 +170,14 @@ def _build_documents(self) -> Tuple[List[Document], ...]:
                 self.dataset.tokenizer,
                 reading_subword_map,
             )
-            (
-                pos_predictions,
-                subpos_predictions,
-                conjtype_predictions,
-                conjform_predictions,
-            ) = get_morpheme_attribute_predictions(pos_logits, subpos_logits, conjtype_logits, conjform_logits)
+            morpheme_attribute_predictions = get_morpheme_attribute_predictions(
+                *(logits[:num_morphemes] for logits in morpheme_attribute_logits)
+            )
             morphemes = build_morphemes(
                 [m.surf for m in gold_document.morphemes],
                 [m.lemma for m in gold_document.morphemes],
                 word_reading_predictions,
-                pos_predictions,
-                subpos_predictions,
-                conjtype_predictions,
-                conjform_predictions,
+                morpheme_attribute_predictions,
             )
             predicted_document = chunk_morphemes(gold_document, morphemes, word_feature_probabilities)
             predicted_document.doc_id = gold_document.doc_id
@@ -190,7 +192,7 @@ def _build_documents(self) -> Tuple[List[Document], ...]:
                 add_named_entities(sentence, ne_predictions)
                 add_base_phrase_features(sentence, base_phrase_feature_probabilities)
                 add_dependency(
-                    sentence, dependency_predictions, dependency_type_predictions, self.dataset.special_token2index
+                    sentence, dependency_predictions, dependency_type_predictions, example.special_token_indexer
                 )
                 doc_id2partly_gold_sentences1[orig_doc_id].append(sentence)
 
@@ -202,7 +204,7 @@ def _build_documents(self) -> Tuple[List[Document], ...]:
                 partly_gold_document2,
                 cohesion_logits,
                 self.dataset.cohesion_task2utils,
-                self.dataset.index2special_token,
+                example.special_token_indexer,
             )
             for sentence in extract_target_sentences(partly_gold_document2):
                 doc_id2partly_gold_sentences2[orig_doc_id].append(sentence)
@@ -260,25 +262,25 @@ def compute_morphological_analysis_metrics(
         # m.pos not in POS_TAGS ... 未定義語、その他など
         pos_labels = [[f"B-{m.pos}" if m.pos in POS_TAGS else "B-" for m in d.morphemes] for d in gold_documents]
         pos_predictions = [[f"B-{m.pos}" for m in d.morphemes] for d in predicted_documents]
-        metrics["pos_f1"] = f1_score(y_true=pos_labels, y_pred=pos_predictions)
+        metrics["pos_f1"] = f1_score(y_true=pos_labels, y_pred=pos_predictions).item()
 
         subpos_labels = [
             [f"B-{m.subpos}" if m.subpos in SUBPOS_TAGS else "B-" for m in d.morphemes] for d in gold_documents
         ]
         subpos_predictions = [[f"B-{m.subpos}" for m in d.morphemes] for d in predicted_documents]
-        metrics["subpos_f1"] = f1_score(y_true=subpos_labels, y_pred=subpos_predictions)
+        metrics["subpos_f1"] = f1_score(y_true=subpos_labels, y_pred=subpos_predictions).item()
 
         conjtype_labels = [
             [f"B-{m.conjtype}" if m.conjtype in CONJTYPE_TAGS else "B-" for m in d.morphemes] for d in gold_documents
         ]
         conjtype_predictions = [[f"B-{m.conjtype}" for m in d.morphemes] for d in predicted_documents]
-        metrics["conjtype_f1"] = f1_score(y_true=conjtype_labels, y_pred=conjtype_predictions)
+        metrics["conjtype_f1"] = f1_score(y_true=conjtype_labels, y_pred=conjtype_predictions).item()
 
         conjform_labels = [
             [f"B-{m.conjform}" if m.conjform in CONJFORM_TAGS else "B-" for m in d.morphemes] for d in gold_documents
         ]
         conjform_predictions = [[f"B-{m.conjform}" for m in d.morphemes] for d in predicted_documents]
-        metrics["conjform_f1"] = f1_score(y_true=conjform_labels, y_pred=conjform_predictions)
+        metrics["conjform_f1"] = f1_score(y_true=conjform_labels, y_pred=conjform_predictions).item()
 
         metrics["morphological_analysis_f1"] = mean(metrics.values())
         return metrics
@@ -306,20 +308,14 @@ def compute_word_feature_tagging_metrics(
                     for d in predicted_documents
                 ]
             metrics[f"{word_feature}_f1"] = f1_score(
-                y_true=labels,
-                y_pred=predictions,
-                mode="strict",
-                scheme=IOB2,
-            )
+                y_true=labels, y_pred=predictions, mode="strict", scheme=IOB2
+            ).item()
             concatenated_labels += labels
             concatenated_predictions += predictions
         metrics["macro_word_feature_tagging_f1"] = mean(metrics.values())
         metrics["micro_word_feature_tagging_f1"] = f1_score(
-            y_true=concatenated_labels,
-            y_pred=concatenated_predictions,
-            mode="strict",
-            scheme=IOB2,
-        )
+            y_true=concatenated_labels, y_pred=concatenated_predictions, mode="strict", scheme=IOB2
+        ).item()
         return metrics
 
     @staticmethod
@@ -335,18 +331,12 @@ def compute_ner_metrics(predicted_documents: List[Document], gold_documents: Lis
                     morpheme.features["NE"] = f"{bi}-{named_entity.category.value}"
         labels = [[m.features.get("NE") or "O" for m in d.morphemes] for d in gold_documents]
         predictions = [[m.features.get("NE") or "O" for m in d.morphemes] for d in predicted_documents]
-        return {
-            # default: micro平均
-            "ner_f1": f1_score(
-                y_true=labels,
-                y_pred=predictions,
-                mode="strict",
-                scheme=IOB2,
-            )
-        }
+        # `f1_score` function calculates micro average by default
+        return {"ner_f1": f1_score(y_true=labels, y_pred=predictions, mode="strict", scheme=IOB2).item()}
 
+    @staticmethod
     def compute_base_phrase_feature_tagging_metrics(
-        self, partly_gold_documents1: List[Document], gold_documents: List[Document]
+        partly_gold_documents1: List[Document], gold_documents: List[Document]
     ) -> Dict[str, float]:
         metrics: Dict[str, float] = {}
         concatenated_labels = []
@@ -354,7 +344,7 @@ def compute_base_phrase_feature_tagging_metrics(
         for base_phrase_feature in BASE_PHRASE_FEATURES:
             labels = [
                 [
-                    self._convert_feature_to_bo_tag(m.base_phrase.features.get(base_phrase_feature))
+                    WordModuleMetric._convert_feature_to_bo_tag(m.base_phrase.features.get(base_phrase_feature))
                     for m in d.morphemes
                     if m == m.base_phrase.head
                 ]
@@ -362,7 +352,7 @@ def compute_base_phrase_feature_tagging_metrics(
             ]
             predictions = [
                 [
-                    self._convert_feature_to_bo_tag(m.base_phrase.features.get(base_phrase_feature))
+                    WordModuleMetric._convert_feature_to_bo_tag(m.base_phrase.features.get(base_phrase_feature))
                     for m in d.morphemes
                     if m == m.base_phrase.head
                 ]
@@ -372,35 +362,30 @@ def compute_base_phrase_feature_tagging_metrics(
             if "B" not in set(chain.from_iterable(labels)):
                 continue
             metrics[f"{base_phrase_feature}_f1"] = f1_score(
-                y_true=labels,
-                y_pred=predictions,
-                mode="strict",
-                scheme=IOB2,
-            )
+                y_true=labels, y_pred=predictions, mode="strict", scheme=IOB2
+            ).item()
             concatenated_labels += labels
             concatenated_predictions += predictions
         metrics["macro_base_phrase_feature_tagging_f1"] = mean(metrics.values())
         metrics["micro_base_phrase_feature_tagging_f1"] = f1_score(
-            y_true=concatenated_labels,
-            y_pred=concatenated_predictions,
-            mode="strict",
-            scheme=IOB2,
-        )
+            y_true=concatenated_labels, y_pred=concatenated_predictions, mode="strict", scheme=IOB2
+        ).item()
         return metrics
 
     @staticmethod
     def _convert_feature_to_bo_tag(feature: Optional[Union[bool, str]]) -> str:
         return "B" if feature is not None else "O"
 
+    @staticmethod
     def compute_dependency_parsing_metrics(
-        self, partly_gold_documents1: List[Document], gold_documents: List[Document]
+        partly_gold_documents1: List[Document], gold_documents: List[Document]
     ) -> Dict[str, float]:
         metrics: Dict[str, float] = {}
         metrics.update(
             {
                 f"base_phrase_{k}": v
                 for k, v in conll18_ud_eval(
-                    *self._to_conll_lines(partly_gold_documents1, gold_documents, "base_phrase")
+                    *WordModuleMetric._to_conll_lines(partly_gold_documents1, gold_documents, "base_phrase")
                 ).items()
             }
         )
@@ -408,14 +393,14 @@ def compute_dependency_parsing_metrics(
             {
                 f"morpheme_{k}": v
                 for k, v in conll18_ud_eval(
-                    *self._to_conll_lines(partly_gold_documents1, gold_documents, "morpheme")
+                    *WordModuleMetric._to_conll_lines(partly_gold_documents1, gold_documents, "morpheme")
                 ).items()
             }
         )
         return metrics
 
+    @staticmethod
     def _to_conll_lines(
-        self,
         partly_gold_documents1: List[Document],
         gold_documents: List[Document],
         unit: Literal["base_phrase", "morpheme"],
@@ -424,10 +409,10 @@ def _to_conll_lines(
         system_lines = []
         for gold_document, partly_gold_document1 in zip(gold_documents, partly_gold_documents1):
             for sentence in gold_document.sentences:
-                gold_lines += [self._to_conll_line(u) for u in getattr(sentence, f"{unit}s")]
+                gold_lines += [WordModuleMetric._to_conll_line(u) for u in getattr(sentence, f"{unit}s")]
                 gold_lines.append("\n")
             for sentence in partly_gold_document1.sentences:
-                system_lines += [self._to_conll_line(u) for u in getattr(sentence, f"{unit}s")]
+                system_lines += [WordModuleMetric._to_conll_line(u) for u in getattr(sentence, f"{unit}s")]
                 system_lines.append("\n")
         return gold_lines, system_lines
 
diff --git a/src/kwja/modules/__init__.py b/src/kwja/modules/__init__.py
index e009c5a3..91ad413d 100644
--- a/src/kwja/modules/__init__.py
+++ b/src/kwja/modules/__init__.py
@@ -1,9 +1,11 @@
 from .char import CharModule
+from .senter import SenterModule
 from .seq2seq import Seq2SeqModule
 from .typo import TypoModule
 from .word import WordModule
 
 __all__ = [
+    "SenterModule",
     "TypoModule",
     "Seq2SeqModule",
     "CharModule",
diff --git a/src/kwja/modules/base.py b/src/kwja/modules/base.py
index 2fb7e4c1..cd33f79c 100644
--- a/src/kwja/modules/base.py
+++ b/src/kwja/modules/base.py
@@ -1,16 +1,56 @@
-import copy
-from typing import Any, Dict
+import os
+from copy import deepcopy
+from typing import Any, Callable, Dict, Generic, List, Optional, Sequence, TypeVar
 
 import hydra
 import pytorch_lightning as pl
+import torch
 from omegaconf import DictConfig, ListConfig, OmegaConf
+from torch.overrides import TorchFunctionMode  # type: ignore
+from torch.serialization import FILE_LIKE, MAP_LOCATION  # type: ignore
+from typing_extensions import Self
 
 
-class BaseModule(pl.LightningModule):
-    def __init__(self, hparams: DictConfig) -> None:
+class DummyModuleMetric:
+    def __init__(self) -> None:
+        pass
+
+    def update(self, *args: Any, **kwargs: Any) -> None:
+        pass
+
+    def compute(self) -> Dict[str, float]:
+        return {}
+
+    def reset(self) -> None:
+        pass
+
+    def pad(self, *args: Any, **kwargs: Any) -> None:
+        pass
+
+    def set_properties(self, *args: Any, **kwargs: Any) -> None:
+        pass
+
+
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    BaseModuleMetric = DummyModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics.base import BaseModuleMetric  # type: ignore
+
+MetricType = TypeVar("MetricType", bound=BaseModuleMetric)
+
+
+class BaseModule(pl.LightningModule, Generic[MetricType]):
+    def __init__(self, hparams: DictConfig, metric: MetricType) -> None:
         super().__init__()
         self.save_hyperparameters(hparams)
 
+        if valid_corpora := getattr(hparams.datamodule, "valid", None):
+            self.valid_corpora: List[str] = list(valid_corpora)
+            self.valid_corpus2metric: Dict[str, MetricType] = {corpus: deepcopy(metric) for corpus in valid_corpora}
+        if test_corpora := getattr(hparams.datamodule, "test", None):
+            self.test_corpora: List[str] = list(test_corpora)
+            self.test_corpus2metric: Dict[str, MetricType] = {corpus: deepcopy(metric) for corpus in test_corpora}
+
     def configure_optimizers(self):
         # Split weights in two groups, one with weight decay and the other not.
         no_decay = ("bias", "LayerNorm.weight")
@@ -40,12 +80,60 @@ def configure_optimizers(self):
         return {"optimizer": optimizer, "lr_scheduler": {"scheduler": lr_scheduler, "interval": "step", "frequency": 1}}
 
     def on_save_checkpoint(self, checkpoint: Dict[str, Any]) -> None:
-        hparams: DictConfig = copy.deepcopy(checkpoint["hyper_parameters"])
+        hparams: DictConfig = deepcopy(checkpoint["hyper_parameters"])
         OmegaConf.set_struct(hparams, False)
         if self.hparams.ignore_hparams_on_save:
             hparams = filter_dict_items(hparams, self.hparams.hparams_to_ignore_on_save)
         checkpoint["hyper_parameters"] = hparams
 
+    @classmethod
+    def fast_load_from_checkpoint(
+        cls,
+        checkpoint_path: FILE_LIKE,
+        map_location: MAP_LOCATION = None,
+        strict: bool = True,
+    ) -> Self:
+        checkpoint = torch.load(checkpoint_path, map_location=map_location)
+        with _EmptyInit():
+            module = cls(hparams=checkpoint[cls.CHECKPOINT_HYPER_PARAMS_KEY])  # type: ignore
+        module.load_state_dict(checkpoint["state_dict"], strict=strict)
+        return module
+
+
+# TODO: use `Fabric.init_module(empty_weights=True)` instead after this PR is merged: https://github.com/Lightning-AI/lightning/pull/17627
+# From https://lernapparat.de/faster-model-init by Thomas Viehmann
+class _EmptyInit(TorchFunctionMode):
+    """Initialize `nn.Module` with empty tensors, i.e., uninitialized memory.
+    Example::
+        with _EmptyInit():
+            model = BigModel()
+        model.load_state_dict(torch.load("checkpoint.pt"))
+    """
+
+    def __init__(self, enabled: bool = True):
+        super().__init__()
+        self.enabled = enabled
+
+    def __torch_function__(
+        self,
+        func: Callable,
+        types: Sequence,
+        args: Sequence[Any] = (),
+        kwargs: Optional[Dict] = None,
+    ):
+        kwargs = kwargs or {}
+        if not self.enabled:
+            return func(*args, **kwargs)
+        if getattr(func, "__module__", None) == "torch.nn.init":
+            if "tensor" in kwargs:
+                return kwargs["tensor"]
+            return args[0]
+        if getattr(func, "__module__", None) is None and func.__name__ == "normal_":
+            if "mean" in kwargs:
+                return kwargs["mean"]
+            return args[0]
+        return func(*args, **kwargs)
+
 
 def filter_dict_items(item: DictConfig, keys_to_ignore: ListConfig) -> DictConfig:
     """Filter out dictionary items whose key is in keys_to_ignore recursively."""
diff --git a/src/kwja/modules/char.py b/src/kwja/modules/char.py
index 7fc91de3..d72ec60c 100644
--- a/src/kwja/modules/char.py
+++ b/src/kwja/modules/char.py
@@ -1,74 +1,51 @@
+import os
 from statistics import mean
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict
 
 import hydra
 import torch
 from omegaconf import DictConfig
-from transformers import PretrainedConfig, PreTrainedModel
+from transformers import PreTrainedModel
 
-from kwja.metrics import CharModuleMetric
 from kwja.modules.base import BaseModule
 from kwja.modules.components.head import SequenceLabelingHead
 from kwja.modules.functions.loss import compute_token_mean_loss
 from kwja.utils.constants import WORD_NORM_OP_TAGS, WORD_SEGMENTATION_TAGS
 
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    from kwja.modules.base import DummyModuleMetric as CharModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics import CharModuleMetric  # type: ignore
 
-class CharModule(BaseModule):
+
+class CharModule(BaseModule[CharModuleMetric]):
     def __init__(self, hparams: DictConfig) -> None:
-        super().__init__(hparams)
-
-        if valid_corpora := getattr(hparams.datamodule, "valid", None):
-            self.valid_corpora: List[str] = list(valid_corpora)
-            self.valid_corpus2char_module_metric: Dict[str, CharModuleMetric] = {
-                corpus: CharModuleMetric() for corpus in self.valid_corpora
-            }
-        if test_corpora := getattr(hparams.datamodule, "test", None):
-            self.test_corpora: List[str] = list(test_corpora)
-            self.test_corpus2char_module_metric: Dict[str, CharModuleMetric] = {
-                corpus: CharModuleMetric() for corpus in self.test_corpora
-            }
-
-        self.char_encoder: PreTrainedModel = hydra.utils.call(hparams.encoder)
-        pretrained_model_config: PretrainedConfig = self.char_encoder.config
+        super().__init__(hparams, CharModuleMetric())
+
+        self.encoder: PreTrainedModel = hydra.utils.call(hparams.encoder.from_config)
         if hasattr(hparams, "special_tokens"):
-            self.char_encoder.resize_token_embeddings(pretrained_model_config.vocab_size + len(hparams.special_tokens))
-        head_args: Tuple[int, float] = (
-            pretrained_model_config.hidden_size,
-            pretrained_model_config.hidden_dropout_prob,
-        )
+            self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(hparams.special_tokens))
+        head_kwargs: Dict[str, Any] = dict(hidden_size=self.encoder.config.hidden_size, hidden_dropout_prob=0.05)
 
         # ---------- word segmentation ----------
-        self.word_segmentation_tagger = SequenceLabelingHead(len(WORD_SEGMENTATION_TAGS), *head_args)
+        self.word_segmentation_tagger = SequenceLabelingHead(len(WORD_SEGMENTATION_TAGS), **head_kwargs)
 
         # ---------- word normalization ----------
-        self.word_norm_op_tagger = SequenceLabelingHead(len(WORD_NORM_OP_TAGS), *head_args)
+        self.word_norm_op_tagger = SequenceLabelingHead(len(WORD_NORM_OP_TAGS), **head_kwargs)
+
+    def setup(self, stage: str) -> None:
+        if stage == "fit":
+            self.encoder = hydra.utils.call(self.hparams.encoder.from_pretrained)
+            if hasattr(self.hparams, "special_tokens"):
+                self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(self.hparams.special_tokens))
 
     def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
-        truncation_length = self._truncate(batch)
-        encoded = self.char_encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
-        word_segmentation_logits = self.word_segmentation_tagger(encoded.last_hidden_state)
-        word_norm_op_logits = self.word_norm_op_tagger(encoded.last_hidden_state)
-        if truncation_length > 0:
-            word_segmentation_logits = self._pad(word_segmentation_logits, truncation_length)
-            word_norm_op_logits = self._pad(word_norm_op_logits, truncation_length)
+        encoded = self.encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
         return {
-            "word_segmentation_logits": word_segmentation_logits,
-            "word_norm_op_logits": word_norm_op_logits,
+            "word_segmentation_logits": self.word_segmentation_tagger(encoded.last_hidden_state),
+            "word_norm_op_logits": self.word_norm_op_tagger(encoded.last_hidden_state),
         }
 
-    def _truncate(self, batch: Any) -> int:
-        max_seq_length = batch["attention_mask"].sum(dim=1).max().item()
-        for key, value in batch.items():
-            if key in {"input_ids", "attention_mask"}:
-                batch[key] = value[:, :max_seq_length].contiguous()
-        return self.hparams.max_seq_length - max_seq_length
-
-    @staticmethod
-    def _pad(tensor: torch.Tensor, padding_length: int) -> torch.Tensor:
-        batch_size, _, num_labels = tensor.shape
-        padding = torch.zeros((batch_size, padding_length, num_labels), dtype=tensor.dtype, device=tensor.device)
-        return torch.cat([tensor, padding], dim=1)
-
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         ret: Dict[str, torch.Tensor] = self(batch)
         word_segmentation_loss = compute_token_mean_loss(
@@ -79,23 +56,24 @@ def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         self.log("train/word_normalization_loss", word_normalization_loss)
         return word_segmentation_loss + word_normalization_loss
 
-    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"word_norm_op_labels": batch["word_norm_op_labels"]})
-        corpus = self.valid_corpora[dataloader_idx or 0]
-        self.valid_corpus2char_module_metric[corpus].update(kwargs)
-
-    def validation_epoch_end(self, validation_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.valid_corpora}
-        for corpus, char_module_metric in self.valid_corpus2char_module_metric.items():
-            dataset = self.trainer.val_dataloaders[self.valid_corpora.index(corpus)].dataset
-            char_module_metric.set_properties({"dataset": dataset})
-            metrics = char_module_metric.compute()
+        metric = self.valid_corpus2metric[self.valid_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_validation_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.valid_corpus2metric.items():
+            dataset = self.trainer.val_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics = metric.compute()
             metrics["aggregated_char_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            char_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"valid_{corpus}/{key}": value for key, value in metrics.items()})
@@ -103,23 +81,24 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.valid_corpora if key in metrics_log[corpus])
             self.log(f"valid/{key}", mean_score)
 
-    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"word_norm_op_labels": batch["word_norm_op_labels"]})
-        corpus = self.test_corpora[dataloader_idx or 0]
-        self.test_corpus2char_module_metric[corpus].update(kwargs)
-
-    def test_epoch_end(self, test_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.test_corpora}
-        for corpus, char_module_metric in self.test_corpus2char_module_metric.items():
-            dataset = self.trainer.test_dataloaders[self.test_corpora.index(corpus)].dataset
-            char_module_metric.set_properties({"dataset": dataset})
-            metrics = char_module_metric.compute()
+        metric = self.test_corpus2metric[self.test_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_test_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.test_corpus2metric.items():
+            dataset = self.trainer.test_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics = metric.compute()
             metrics["aggregated_char_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            char_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"test_{corpus}/{key}": value for key, value in metrics.items()})
@@ -127,7 +106,7 @@ def test_epoch_end(self, test_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.test_corpora if key in metrics_log[corpus])
             self.log(f"test/{key}", mean_score)
 
-    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Dict[str, torch.Tensor]:
+    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Dict[str, torch.Tensor]:
         ret: Dict[str, torch.Tensor] = self(batch)
         return {
             "example_ids": batch["example_ids"],
diff --git a/src/kwja/modules/components/crf.py b/src/kwja/modules/components/crf.py
index f29fe170..9e41a220 100644
--- a/src/kwja/modules/components/crf.py
+++ b/src/kwja/modules/components/crf.py
@@ -46,14 +46,16 @@ def forward(
         reduction: Literal["token_mean", "mean", "sum", "none"] = "token_mean",
     ) -> torch.Tensor:
         if mask is None:
-            mask = torch.ones_like(tags)
+            mask = torch.full_like(tags, True, dtype=torch.bool)
 
         numerator = self._compute_numerator(emissions, tags, mask)  # (b, )
         denominator = self._compute_denominator(emissions, mask)  # (b, )
         llh = denominator - numerator  # (b, )
 
         if reduction == "token_mean":
-            return llh.sum() / mask.sum()
+            labels_in_batch = mask.sum(dim=1)
+            eps = 1e-6
+            return (llh / (labels_in_batch + eps)).sum() / (labels_in_batch.ne(0).sum(0) + eps).sum()
         elif reduction == "mean":
             return llh.mean()
         elif reduction == "sum":
@@ -67,21 +69,21 @@ def _compute_numerator(self, emissions: torch.Tensor, tags: torch.Tensor, mask:
         indices = torch.arange(batch_size)
 
         arange = torch.arange(seq_len, device=mask.device)
-        heads = torch.amin(arange * mask + seq_len * (1 - mask), dim=1)
-        head_tags = tags[indices, heads]
-        min_head = int(heads.min())
-        tails = torch.amax(arange * mask, dim=1)
-        tail_tags = tags[indices, tails]
-        max_tail = int(tails.max())
-
-        score = self.start_transitions[head_tags] + emissions[indices, heads, head_tags]
-        for j in range(min_head + 1, max_tail + 1):
-            condition = torch.logical_and(mask[:, j] == 1, heads != j)
+        head_indices = torch.amin(torch.where(mask, arange, seq_len - 1), dim=1)
+        head_tags = tags[indices, head_indices]
+        min_head_index = int(head_indices.min())
+        tail_indices = torch.amax(arange * mask, dim=1)
+        tail_tags = tags[indices, tail_indices]
+        max_tail_index = int(tail_indices.max())
+
+        score = self.start_transitions[head_tags] + emissions[indices, head_indices, head_tags]
+        for j in range(min_head_index + 1, max_tail_index + 1):
+            condition = torch.logical_and(mask[:, j] == 1, head_indices != j)
             next_score = score + self.transitions[tags[:, j - 1], tags[:, j]] + emissions[indices, j, tags[:, j]]
             score = torch.where(condition, next_score, score)
-        score += self.end_transitions[tail_tags]
+        score = score + self.end_transitions[tail_tags]
 
-        return score  # (b, )
+        return torch.where(mask.sum(dim=1).ne(0), score, torch.zeros_like(score))  # (b, )
 
     # あり得るタグ系列のスコアの和を計算する
     def _compute_denominator(self, emissions: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
@@ -89,37 +91,38 @@ def _compute_denominator(self, emissions: torch.Tensor, mask: torch.Tensor) -> t
         indices = torch.arange(batch_size)
 
         arange = torch.arange(seq_len, device=mask.device)
-        heads = torch.amin(arange * mask + seq_len * (1 - mask), dim=1)
-        min_head = int(heads.min())
-        tails = torch.amax(arange * mask, dim=1)
-        max_tail = int(tails.max())
-
-        score = self.start_transitions + emissions[indices, heads]  # (b, num_tags)
-        for j in range(min_head + 1, max_tail + 1):
-            condition = torch.logical_and(mask[:, j] == 1, heads != j)
+        head_indices = torch.amin(torch.where(mask, arange, seq_len - 1), dim=1)
+        min_head_index = int(head_indices.min())
+        tail_indices = torch.amax(arange * mask, dim=1)
+        max_tail_index = int(tail_indices.max())
+
+        score = self.start_transitions + emissions[indices, head_indices]  # (b, num_tags)
+        for j in range(min_head_index + 1, max_tail_index + 1):
+            condition = torch.logical_and(mask[:, j] == 1, head_indices != j)
             broadcast_score = score.unsqueeze(2)  # (b, num_tags, 1)
             broadcast_emissions = emissions[:, j].unsqueeze(1)  # (b, 1, num_tags)
             next_score = broadcast_score + self.transitions + broadcast_emissions  # (b, num_tags, num_tags)
             next_score = torch.logsumexp(next_score, dim=1)  # (b, num_tags)
             score = torch.where(condition.unsqueeze(1), next_score, score)
-        score += self.end_transitions
+        score = score + self.end_transitions
+        score = torch.logsumexp(score, dim=1)  # (b, )
 
-        return torch.logsumexp(score, dim=1)  # (b, )
+        return torch.where(mask.sum(dim=1).ne(0), score, torch.zeros_like(score))  # (b, )
 
     def viterbi_decode(self, emissions: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
         batch_size, seq_len = mask.shape
         indices = torch.arange(batch_size)
 
         arange = torch.arange(seq_len, device=mask.device)
-        heads = torch.amin(arange * mask + seq_len * (1 - mask), dim=1)
-        min_head = int(heads.min())
-        tails = torch.amax(arange * mask, dim=1)
-        max_tail = int(tails.max())
+        head_indices = torch.amin(torch.where(mask, arange, seq_len - 1), dim=1)
+        min_head_index = int(head_indices.min())
+        tail_indices = torch.amax(arange * mask, dim=1)
+        max_tail_index = int(tail_indices.max())
 
-        score = self.start_transitions + emissions[indices, heads]  # (b, num_tags)
+        score = self.start_transitions + emissions[indices, head_indices]  # (b, num_tags)
         history = []
-        for j in range(min_head + 1, max_tail + 1):
-            condition = torch.logical_and(mask[:, j] == 1, heads != j)
+        for j in range(min_head_index + 1, max_tail_index + 1):
+            condition = torch.logical_and(mask[:, j] == 1, head_indices != j)
             broadcast_score = score.unsqueeze(2)  # (b, num_tags, 1)
             broadcast_emissions = emissions[:, j].unsqueeze(1)  # (b, 1, num_tags)
             next_score = broadcast_score + self.transitions + broadcast_emissions  # (b, num_tags, num_tags)
@@ -130,16 +133,15 @@ def viterbi_decode(self, emissions: torch.Tensor, mask: torch.Tensor) -> torch.T
 
         batch_best_tags = []
         for i in range(batch_size):
-            head, tail = int(heads[i]), int(tails[i])
+            head_index, tail_index = int(head_indices[i]), int(tail_indices[i])
             _, best_tag = score[i].max(dim=0)
             best_tags = [best_tag.item()]
-            span = slice(head - min_head, tail - min_head)
-            for max_indices in history[span][::-1]:
+            for max_indices in history[head_index - min_head_index : tail_index - min_head_index][::-1]:
                 best_tag = max_indices[i][best_tags[-1]]
                 best_tags.append(best_tag.item())
-            best_tags += [self.tags.index("O")] * head
+            best_tags += [self.tags.index("O")] * head_index
             best_tags = best_tags[::-1]
-            best_tags += [self.tags.index("O")] * (seq_len - tail - 1)
+            best_tags += [self.tags.index("O")] * (seq_len - tail_index - 1)
             assert len(best_tags) == seq_len, "the length of decoded sequence is inconsistent with max seq length"
             batch_best_tags.append(best_tags)
 
diff --git a/src/kwja/modules/components/head.py b/src/kwja/modules/components/head.py
index f4260e68..ebff13a4 100644
--- a/src/kwja/modules/components/head.py
+++ b/src/kwja/modules/components/head.py
@@ -1,35 +1,84 @@
-from typing import Dict
+import math
 
 import torch
 import torch.nn as nn
 
 
 class SequenceLabelingHead(nn.Sequential):
-    def __init__(
-        self, num_labels: int, hidden_size: int, hidden_dropout_prob: float, multi_label: bool = False
-    ) -> None:
+    def __init__(self, num_labels: int, hidden_size: int, hidden_dropout_prob: float) -> None:
         super().__init__(
             nn.Linear(hidden_size, hidden_size),
             nn.GELU(),
             nn.Dropout(hidden_dropout_prob),
             nn.Linear(hidden_size, num_labels),
         )
-        if multi_label is True:
-            self.add_module("sigmoid", nn.Sigmoid())
+
+
+class LoRASequenceMultiLabelingHead(nn.Module):
+    """
+    In multi-labeling tasks such as word feature tagging and base phrase feature tagging, rare labels are easily ignored
+     during training, leading to a decrease in macro-F1. This module provides a low-rank adaptation layer for each label
+     to encourage learning of rare labels.
+    c.f. https://github.com/microsoft/LoRA
+    """
+
+    def __init__(self, num_labels: int, hidden_size: int, hidden_dropout_prob: float, rank: int = 4) -> None:
+        super().__init__()
+        self.dense = nn.Linear(hidden_size, hidden_size)
+        self.dense_a = nn.Parameter(torch.Tensor(hidden_size, rank, num_labels))
+        self.dense_b = nn.Parameter(torch.Tensor(rank, hidden_size, num_labels))
+        self.activation = nn.GELU()
+        self.dropout = nn.Dropout(hidden_dropout_prob)
+        self.classifier_weight = nn.Parameter(torch.Tensor(hidden_size, num_labels))
+        self.classifier_bias = nn.Parameter(torch.Tensor(num_labels))
+        self.reset_parameters()
+
+    def reset_parameters(self) -> None:
+        nn.init.kaiming_uniform_(self.dense_a, a=math.sqrt(5))
+        nn.init.zeros_(self.dense_b)
+        nn.init.kaiming_uniform_(self.classifier_weight, a=math.sqrt(5))
+        bound = 1 / math.sqrt(self.classifier_weight.size(0))
+        nn.init.uniform_(self.classifier_bias, -bound, bound)
+
+    def forward(self, pooled: torch.Tensor) -> torch.Tensor:
+        dense_out = self.dense(pooled)  # (b, seq, hid)
+        dense_delta = torch.einsum("hrl,ril->hil", self.dense_a, self.dense_b)  # (hid, hid, label)
+        dense_delta_out = torch.einsum("bsh,hil->bsil", pooled, dense_delta)  # (b, seq, hid, label)
+        hidden = self.dropout(self.activation(dense_out.unsqueeze(dim=3) + dense_delta_out))  # (b, seq, hid, label)
+        # (b, seq, label), (1, 1, label) -> (b, seq, label)
+        logits = torch.einsum("bshl,hl->bsl", hidden, self.classifier_weight) + self.classifier_bias.view(1, 1, -1)
+        return torch.sigmoid(logits)  # (b, seq, label)
 
 
 class WordSelectionHead(nn.Module):
-    def __init__(self, num_relations: int, hidden_size: int, hidden_dropout_prob: float) -> None:
+    def __init__(self, num_labels: int, hidden_size: int, hidden_dropout_prob: float) -> None:
         super().__init__()
         self.l_source = nn.Linear(hidden_size, hidden_size)
         self.l_target = nn.Linear(hidden_size, hidden_size)
+        self.activation = nn.GELU()
         self.dropout = nn.Dropout(hidden_dropout_prob)
-        self.output_layer = nn.Linear(hidden_size, num_relations, bias=False)
+        self.output_layer = nn.Linear(hidden_size, num_labels, bias=False)
 
-    def forward(self, pooled: torch.Tensor) -> Dict[str, torch.Tensor]:
+    def forward(self, pooled: torch.Tensor) -> torch.Tensor:
         h_source = self.l_source(pooled)  # (b, seq, hid)
         h_target = self.l_target(pooled)  # (b, seq, hid)
-        h = torch.tanh(h_source.unsqueeze(2) + h_target.unsqueeze(1))  # (b, seq, seq, hid)
-        h = self.dropout(h)
-        logits = self.output_layer(h)  # (b, seq, seq, rel)
-        return logits
+        hidden = self.dropout(self.activation(h_source.unsqueeze(2) + h_target.unsqueeze(1)))  # (b, seq, seq, hid)
+        return self.output_layer(hidden)  # (b, seq, seq, label)
+
+
+class RelationWiseWordSelectionHead(nn.Module):
+    def __init__(self, num_relations: int, hidden_size: int, hidden_dropout_prob: float) -> None:
+        super().__init__()
+        self.l_source = nn.Linear(hidden_size, hidden_size * num_relations)
+        self.l_target = nn.Linear(hidden_size, hidden_size * num_relations)
+        self.activation = nn.GELU()
+        self.dropout = nn.Dropout(hidden_dropout_prob)
+        self.classifier_weight = nn.Parameter(torch.Tensor(hidden_size, num_relations))
+        nn.init.kaiming_uniform_(self.classifier_weight, a=math.sqrt(5))
+
+    def forward(self, pooled: torch.Tensor) -> torch.Tensor:
+        batch_size, seq_length, hidden_size = pooled.size()
+        h_source = self.l_source(pooled).view(batch_size, seq_length, -1, hidden_size)  # (b, seq, rel, hid)
+        h_target = self.l_target(pooled).view(batch_size, seq_length, -1, hidden_size)  # (b, seq, rel, hid)
+        hidden = self.dropout(self.activation(h_source.unsqueeze(2) + h_target.unsqueeze(1)))  # (b, seq, seq, rel, hid)
+        return torch.einsum("bstlh,hl->bstl", hidden, self.classifier_weight)  # (b, seq, seq, rel)
diff --git a/src/kwja/modules/components/logits_processor.py b/src/kwja/modules/components/logits_processor.py
index 2079ecce..2578d453 100644
--- a/src/kwja/modules/components/logits_processor.py
+++ b/src/kwja/modules/components/logits_processor.py
@@ -5,7 +5,7 @@
 from transformers import PreTrainedTokenizerBase
 from transformers.generation import LogitsProcessor
 
-from kwja.utils.constants import NEW_LINE_TOKEN
+from kwja.utils.constants import FULL_SPACE_TOKEN, NEW_LINE_TOKEN
 
 
 def get_char2tokens(tokenizer: PreTrainedTokenizerBase) -> Tuple[Dict[str, Dict[str, int]], Dict[str, Dict[str, int]]]:
@@ -30,14 +30,18 @@ def __init__(
         char2underscore_tokens: Dict[str, Dict[str, int]],
     ) -> None:
         self.tokenizer = tokenizer
-        self.texts = tokenizer.batch_decode(tokenizer.batch_encode_plus(texts).input_ids, skip_special_tokens=True)
+        self.texts: List[str] = []
+        for text in tokenizer.batch_decode(tokenizer.batch_encode_plus(texts).input_ids):
+            if text.endswith(self.tokenizer.eos_token):
+                text = text[: -len(self.tokenizer.eos_token)]
+            if f"{FULL_SPACE_TOKEN} " in text:
+                text = text.replace(f"{FULL_SPACE_TOKEN} ", FULL_SPACE_TOKEN)
+            self.texts.append(text)
         self.char2tokens: Dict[str, Dict[str, int]] = char2tokens
         self.char2underscore_tokens: Dict[str, Dict[str, int]] = char2underscore_tokens
         self.new_line_token_id: int = tokenizer.convert_tokens_to_ids(NEW_LINE_TOKEN)
         self.under_score_token_id: int = tokenizer.convert_tokens_to_ids("▁")
         self.pad_token_id: int = self.tokenizer.pad_token_id
-        assert self.new_line_token_id == 250100
-        assert self.under_score_token_id == 259
 
     def get_generated_surfs(self, input_ids: torch.Tensor) -> List[str]:
         generated_surfs: List[str] = []
@@ -48,10 +52,10 @@ def get_generated_surfs(self, input_ids: torch.Tensor) -> List[str]:
                 stripped_line: str = line.lstrip().rstrip()
                 if stripped_line in ["", "EOS"]:
                     break
-                if stripped_line[0] in ["!", "?", ","]:
-                    generated_surf += stripped_line[0]
-                elif stripped_line[:3] == "...":
+                if stripped_line == "............/...":
                     generated_surf += "..."
+                elif stripped_line[0] in ["!", "?", ",", "."]:
+                    generated_surf += stripped_line[0]
                 else:
                     split_line: List[str] = stripped_line.split()
                     if len(split_line) > 0:
diff --git a/src/kwja/modules/functions/loss.py b/src/kwja/modules/functions/loss.py
index e50db870..95af8c50 100644
--- a/src/kwja/modules/functions/loss.py
+++ b/src/kwja/modules/functions/loss.py
@@ -3,23 +3,47 @@
 
 from kwja.utils.constants import IGNORE_INDEX, MASKED
 
+eps = 1e-6
 
-def compute_token_mean_loss(input_: torch.Tensor, target: torch.Tensor):
-    return F.cross_entropy(
-        input=input_.view(-1, input_.size(-1)),
-        target=target.view(-1),
-        ignore_index=IGNORE_INDEX,
-    )
 
+def _average_loss(
+    losses: torch.Tensor,  # (b, *dim)
+    mask: torch.Tensor,  # (b, *dim)
+) -> torch.Tensor:  # ()
+    masked_loss_in_batch = (losses * mask).sum(dim=tuple(range(1, losses.ndim)))  # (b)
+    labels_in_batch = mask.sum(dim=tuple(range(1, losses.ndim)))  # (b)
+    return (masked_loss_in_batch / (labels_in_batch + eps)).sum() / (labels_in_batch.ne(0).sum() + eps)
 
-def compute_multi_label_token_mean_loss(input_: torch.Tensor, target: torch.Tensor, mask: torch.Tensor):
-    num_units = mask[:, :, 0].sum(dim=1)  # (b, )
+
+def compute_token_mean_loss(
+    input_: torch.Tensor,  # (b, *seq, num_classes)
+    target: torch.Tensor,  # (b, *seq)
+) -> torch.Tensor:  # ()
+    batch_size, *_, num_classes = input_.shape
+    input_ = input_.view(batch_size, -1, num_classes)  # (b, seq, num_classes)
+    target = target.view(batch_size, -1)  # (b, seq)
+    losses = F.cross_entropy(input_.transpose(1, 2), target, ignore_index=IGNORE_INDEX, reduction="none")  # (b, seq)
+    return _average_loss(losses, target.ne(IGNORE_INDEX))
+
+
+def compute_multi_label_token_mean_loss(
+    input_: torch.Tensor,  # (b, seq, num_features)
+    target: torch.Tensor,  # (b, seq, num_features)
+) -> torch.Tensor:  # ()
     # binary_cross_entropy は IGNORE_INDEX を渡せない
-    losses = F.binary_cross_entropy(input=input_, target=target, reduction="none")
-    # batch と sequence の軸は平均を、 label の軸は和をとる
-    losses = losses.sum(dim=1).sum(dim=1)  # (b, seq, num_features) -> (b, num_features) -> (b, )
-    loss = (losses / num_units).mean()
-    return loss
+    losses = F.binary_cross_entropy(input_, target.float(), reduction="none")  # (b, seq, num_features)
+    mask: torch.Tensor = target.ne(IGNORE_INDEX)  # (b, seq, num_features)
+    # features の軸は和をとる
+    losses = (losses * mask).sum(dim=2)  # (b, seq)
+    return _average_loss(losses, mask[:, :, 0])
+
+
+def compute_cohesion_analysis_loss(
+    input_: torch.Tensor,  # (b, rel, seq, seq)
+    target: torch.Tensor,  # (b, rel, seq, seq)
+) -> torch.Tensor:  # ()
+    log_softmax = torch.log_softmax(input_, dim=3)  # (b, rel, seq, seq)
+    return _average_loss(-log_softmax, target)
 
 
 def mask_logits(logits: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
diff --git a/src/kwja/modules/senter.py b/src/kwja/modules/senter.py
index 9eb54621..bdb2a00a 100644
--- a/src/kwja/modules/senter.py
+++ b/src/kwja/modules/senter.py
@@ -1,47 +1,45 @@
+import os
 from statistics import mean
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict
 
 import hydra
 import torch
 from omegaconf import DictConfig
-from transformers import PretrainedConfig, PreTrainedModel
+from transformers import PreTrainedModel
 
-from kwja.metrics import SenterModuleMetric
 from kwja.modules.base import BaseModule
 from kwja.modules.components.head import SequenceLabelingHead
 from kwja.modules.functions.loss import compute_token_mean_loss
 from kwja.utils.constants import SENT_SEGMENTATION_TAGS
 
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    from kwja.modules.base import DummyModuleMetric as SenterModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics import SenterModuleMetric  # type: ignore
 
-class SenterModule(BaseModule):
+
+class SenterModule(BaseModule[SenterModuleMetric]):
     def __init__(self, hparams: DictConfig) -> None:
-        super().__init__(hparams)
-
-        if valid_corpora := getattr(hparams.datamodule, "valid", None):
-            self.valid_corpora: List[str] = list(valid_corpora)
-            self.valid_corpus2senter_module_metric: Dict[str, SenterModuleMetric] = {
-                corpus: SenterModuleMetric() for corpus in self.valid_corpora
-            }
-        if test_corpora := getattr(hparams.datamodule, "test", None):
-            self.test_corpora: List[str] = list(test_corpora)
-            self.test_corpus2senter_module_metric: Dict[str, SenterModuleMetric] = {
-                corpus: SenterModuleMetric() for corpus in self.test_corpora
-            }
-
-        self.char_encoder: PreTrainedModel = hydra.utils.call(hparams.encoder)
-        pretrained_model_config: PretrainedConfig = self.char_encoder.config
+        super().__init__(hparams, SenterModuleMetric())
+
+        self.encoder: PreTrainedModel = hydra.utils.call(hparams.encoder.from_config)
         if hasattr(hparams, "special_tokens"):
-            self.char_encoder.resize_token_embeddings(pretrained_model_config.vocab_size + len(hparams.special_tokens))
+            self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(hparams.special_tokens))
         self.sent_segmentation_tagger = SequenceLabelingHead(
             num_labels=len(SENT_SEGMENTATION_TAGS),
-            hidden_size=pretrained_model_config.hidden_size,
-            hidden_dropout_prob=pretrained_model_config.hidden_dropout_prob,
+            hidden_size=self.encoder.config.hidden_size,
+            hidden_dropout_prob=self.encoder.config.hidden_dropout_prob,
         )
 
+    def setup(self, stage: str) -> None:
+        if stage == "fit":
+            self.encoder = hydra.utils.call(self.hparams.encoder.from_pretrained)
+            if hasattr(self.hparams, "special_tokens"):
+                self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(self.hparams.special_tokens))
+
     def forward(self, batch: Any) -> Dict[str, Dict[str, torch.Tensor]]:
-        encoded = self.char_encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
-        sent_segmentation_logits = self.sent_segmentation_tagger(encoded.last_hidden_state)
-        return {"sent_segmentation_logits": sent_segmentation_logits}
+        encoded = self.encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
+        return {"sent_segmentation_logits": self.sent_segmentation_tagger(encoded.last_hidden_state)}
 
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         ret: Dict[str, torch.Tensor] = self(batch)
@@ -51,22 +49,23 @@ def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         self.log("train/sent_segmentation_loss", sent_segmentation_loss)
         return sent_segmentation_loss
 
-    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
+    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
-        corpus = self.valid_corpora[dataloader_idx or 0]
-        self.valid_corpus2senter_module_metric[corpus].update(kwargs)
-
-    def validation_epoch_end(self, validation_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.valid_corpora}
-        for corpus, senter_module_metric in self.valid_corpus2senter_module_metric.items():
-            dataset = self.trainer.val_dataloaders[self.valid_corpora.index(corpus)].dataset
-            senter_module_metric.set_properties({"dataset": dataset})
-            metrics = senter_module_metric.compute()
+        metric = self.valid_corpus2metric[self.valid_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_validation_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.valid_corpus2metric.items():
+            dataset = self.trainer.val_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics = metric.compute()
             metrics["aggregated_senter_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            senter_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"valid_{corpus}/{key}": value for key, value in metrics.items()})
@@ -74,22 +73,23 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.valid_corpora if key in metrics_log[corpus])
             self.log(f"valid/{key}", mean_score)
 
-    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
+    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
-        corpus = self.test_corpora[dataloader_idx or 0]
-        self.test_corpus2senter_module_metric[corpus].update(kwargs)
-
-    def test_epoch_end(self, test_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.test_corpora}
-        for corpus, senter_module_metric in self.test_corpus2senter_module_metric.items():
-            dataset = self.trainer.test_dataloaders[self.test_corpora.index(corpus)].dataset
-            senter_module_metric.set_properties({"dataset": dataset})
-            metrics = senter_module_metric.compute()
+        metric = self.test_corpus2metric[self.test_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_test_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.test_corpus2metric.items():
+            dataset = self.trainer.test_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics = metric.compute()
             metrics["aggregated_senter_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            senter_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"test_{corpus}/{key}": value for key, value in metrics.items()})
@@ -97,7 +97,7 @@ def test_epoch_end(self, test_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.test_corpora if key in metrics_log[corpus])
             self.log(f"test/{key}", mean_score)
 
-    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
+    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any:
         ret: Dict[str, torch.Tensor] = self(batch)
         return {
             "example_ids": batch["example_ids"],
diff --git a/src/kwja/modules/seq2seq.py b/src/kwja/modules/seq2seq.py
index d87b2166..dbcb0641 100644
--- a/src/kwja/modules/seq2seq.py
+++ b/src/kwja/modules/seq2seq.py
@@ -1,5 +1,6 @@
+import os
 from statistics import mean
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict
 
 import hydra
 import torch
@@ -7,69 +8,56 @@
 from transformers import PreTrainedModel, PreTrainedTokenizerBase
 from transformers.generation import LogitsProcessorList
 
-from kwja.metrics import Seq2SeqModuleMetric
 from kwja.modules.base import BaseModule
 from kwja.modules.components.logits_processor import ForcedSurfLogitsProcessor, get_char2tokens
-from kwja.utils.constants import IGNORE_INDEX
 
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    from kwja.modules.base import DummyModuleMetric as Seq2SeqModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics import Seq2SeqModuleMetric  # type: ignore
 
-class Seq2SeqModule(BaseModule):
+
+class Seq2SeqModule(BaseModule[Seq2SeqModuleMetric]):
     def __init__(self, hparams: DictConfig) -> None:
-        super().__init__(hparams)
-
-        if valid_corpora := getattr(hparams.datamodule, "valid", None):
-            self.valid_corpora: List[str] = list(valid_corpora)
-            self.valid_corpus2seq2seq_module_metric: Dict[str, Seq2SeqModuleMetric] = {
-                corpus: Seq2SeqModuleMetric() for corpus in self.valid_corpora
-            }
-        if test_corpora := getattr(hparams.datamodule, "test", None):
-            self.test_corpora: List[str] = list(test_corpora)
-            self.test_corpus2seq2seq_module_metric: Dict[str, Seq2SeqModuleMetric] = {
-                corpus: Seq2SeqModuleMetric() for corpus in self.test_corpora
-            }
+        super().__init__(hparams, Seq2SeqModuleMetric())
 
         self.tokenizer: PreTrainedTokenizerBase = hydra.utils.call(hparams.module.tokenizer)
 
-        self.seq2seq_model: PreTrainedModel = hydra.utils.call(hparams.encoder)
-        if hasattr(hparams, "special_tokens"):
-            # https://github.com/huggingface/transformers/issues/4875
-            self.seq2seq_model.resize_token_embeddings(len(self.tokenizer.get_vocab()))
+        self.encoder_decoder: PreTrainedModel = hydra.utils.call(hparams.encoder.from_config)
+        # https://github.com/huggingface/transformers/issues/4875
+        self.encoder_decoder.resize_token_embeddings(len(self.tokenizer.get_vocab()))
 
         self.char2tokens, self.char2underscore_tokens = get_char2tokens(self.tokenizer)
         self.use_forced_surf_decoding: bool = getattr(hparams, "use_forced_surf_decoding", False)
 
+    def setup(self, stage: str) -> None:
+        if stage == "fit":
+            self.encoder_decoder = hydra.utils.call(self.hparams.encoder.from_pretrained)
+            # https://github.com/huggingface/transformers/issues/4875
+            self.encoder_decoder.resize_token_embeddings(len(self.tokenizer.get_vocab()))
+
     def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
-        self._truncate(batch)
-        output = self.seq2seq_model(
+        output = self.encoder_decoder(
             input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["seq2seq_labels"]
         )
         return {"loss": output.loss, "logits": output.logits}
 
-    @staticmethod
-    def _truncate(batch: Any) -> None:
-        max_src_length = batch["attention_mask"].sum(dim=1).max().item()
-        for key, value in batch.items():
-            if key in {"input_ids", "attention_mask"}:
-                batch[key] = value[:, :max_src_length].contiguous()
-        max_tgt_length = (batch["seq2seq_labels"] != IGNORE_INDEX).sum(dim=1).max().item()
-        batch["seq2seq_labels"] = batch["seq2seq_labels"][:, :max_tgt_length].contiguous()
-
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         ret: Dict[str, torch.Tensor] = self(batch)
         self.log("train/loss", ret["loss"])
         return ret["loss"]
 
-    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self(batch)
         kwargs.update({"example_ids": batch["example_ids"], "loss": kwargs["loss"]})
-        corpus = self.valid_corpora[dataloader_idx or 0]
-        self.valid_corpus2seq2seq_module_metric[corpus].update(kwargs)
+        metric = self.valid_corpus2metric[self.valid_corpora[dataloader_idx]]
+        metric.update(kwargs)
 
-    def validation_epoch_end(self, outputs: List[Any]) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.valid_corpora}
-        for corpus, seq2seq_module_metric in self.valid_corpus2seq2seq_module_metric.items():
-            metrics_log[corpus] = seq2seq_module_metric.compute()
-            seq2seq_module_metric.reset()
+    def on_validation_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.valid_corpus2metric.items():
+            metrics_log[corpus] = metric.compute()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"valid_{corpus}/{key}": value for key, value in metrics.items()})
@@ -77,17 +65,17 @@ def validation_epoch_end(self, outputs: List[Any]) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.valid_corpora if key in metrics_log[corpus])
             self.log(f"valid/{key}", mean_score)
 
-    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self(batch)
         kwargs.update({"example_ids": batch["example_ids"], "loss": kwargs["loss"]})
-        corpus = self.test_corpora[dataloader_idx or 0]
-        self.test_corpus2seq2seq_module_metric[corpus].update(kwargs)
+        metric = self.test_corpus2metric[self.test_corpora[dataloader_idx]]
+        metric.update(kwargs)
 
-    def test_epoch_end(self, outputs: List[Any]) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.test_corpora}
-        for corpus, seq2seq_module_metric in self.test_corpus2seq2seq_module_metric.items():
-            metrics_log[corpus] = seq2seq_module_metric.compute()
-            seq2seq_module_metric.reset()
+    def on_test_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.test_corpus2metric.items():
+            metrics_log[corpus] = metric.compute()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"test_{corpus}/{key}": value for key, value in metrics.items()})
@@ -95,8 +83,8 @@ def test_epoch_end(self, outputs: List[Any]) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.test_corpora if key in metrics_log[corpus])
             self.log(f"test/{key}", mean_score)
 
-    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Any:
-        generateds = self.seq2seq_model.generate(
+    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Any:
+        generations = self.encoder_decoder.generate(
             input_ids=batch["input_ids"],
             attention_mask=batch["attention_mask"],
             logits_processor=LogitsProcessorList(
@@ -115,5 +103,5 @@ def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int]
         )  # (b, max_tgt_len)
         return {
             "example_ids": batch["example_ids"] if "example_ids" in batch else [],
-            "seq2seq_predictions": generateds["sequences"],
+            "seq2seq_predictions": generations["sequences"],
         }
diff --git a/src/kwja/modules/typo.py b/src/kwja/modules/typo.py
index 94652a40..281044d4 100644
--- a/src/kwja/modules/typo.py
+++ b/src/kwja/modules/typo.py
@@ -1,68 +1,48 @@
+import os
 from statistics import mean
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict
 
 import hydra
 import torch
 from omegaconf import DictConfig
-from transformers import PretrainedConfig, PreTrainedModel
+from transformers import PreTrainedModel
 
-from kwja.metrics import TypoModuleMetric
 from kwja.modules.base import BaseModule
 from kwja.modules.components.head import SequenceLabelingHead
 from kwja.modules.functions.loss import compute_token_mean_loss
 from kwja.utils.constants import RESOURCE_PATH
 
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    from kwja.modules.base import DummyModuleMetric as TypoModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics import TypoModuleMetric  # type: ignore
 
-class TypoModule(BaseModule):
+
+class TypoModule(BaseModule[TypoModuleMetric]):
     def __init__(self, hparams: DictConfig) -> None:
-        super().__init__(hparams)
-
-        if valid_corpora := getattr(hparams.datamodule, "valid", None):
-            self.valid_corpora: List[str] = list(valid_corpora)
-            self.valid_corpus2typo_module_metric: Dict[str, TypoModuleMetric] = {
-                corpus: TypoModuleMetric() for corpus in self.valid_corpora
-            }
-        if test_corpora := getattr(hparams.datamodule, "test", None):
-            self.test_corpora: List[str] = list(test_corpora)
-            self.test_corpus2typo_module_metric: Dict[str, TypoModuleMetric] = {
-                corpus: TypoModuleMetric() for corpus in self.test_corpora
-            }
-
-        self.char_encoder: PreTrainedModel = hydra.utils.call(hparams.encoder)
-        pretrained_model_config: PretrainedConfig = self.char_encoder.config
+        super().__init__(hparams, TypoModuleMetric())
+
+        self.encoder: PreTrainedModel = hydra.utils.call(hparams.encoder.from_config)
         if hasattr(hparams, "special_tokens"):
-            self.char_encoder.resize_token_embeddings(pretrained_model_config.vocab_size + len(hparams.special_tokens))
-        head_args: Tuple[int, float] = (
-            pretrained_model_config.hidden_size,
-            pretrained_model_config.hidden_dropout_prob,
-        )
+            self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(hparams.special_tokens))
+        head_kwargs: Dict[str, Any] = dict(hidden_size=self.encoder.config.hidden_size, hidden_dropout_prob=0.05)
 
-        self.kdr_tagger = SequenceLabelingHead(pretrained_model_config.vocab_size, *head_args)
+        self.kdr_tagger = SequenceLabelingHead(self.encoder.config.vocab_size, **head_kwargs)
         extended_vocab_size = sum(1 for _ in RESOURCE_PATH.joinpath("typo_correction", "multi_char_vocab.txt").open())
-        self.ins_tagger = SequenceLabelingHead(pretrained_model_config.vocab_size + extended_vocab_size, *head_args)
+        self.ins_tagger = SequenceLabelingHead(self.encoder.config.vocab_size + extended_vocab_size, **head_kwargs)
+
+    def setup(self, stage: str) -> None:
+        if stage == "fit":
+            self.encoder = hydra.utils.call(self.hparams.encoder.from_pretrained)
+            if hasattr(self.hparams, "special_tokens"):
+                self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(self.hparams.special_tokens))
 
     def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
-        truncation_length = self._truncate(batch)
-        encoded = self.char_encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
-        kdr_logits = self.kdr_tagger(encoded.last_hidden_state)
-        ins_logits = self.ins_tagger(encoded.last_hidden_state)
-        if truncation_length > 0:
-            kdr_logits = self._pad(kdr_logits, truncation_length)
-            ins_logits = self._pad(ins_logits, truncation_length)
-        return {"kdr_logits": kdr_logits, "ins_logits": ins_logits}
-
-    def _truncate(self, batch: Any) -> int:
-        max_seq_length = batch["attention_mask"].sum(dim=1).max().item()
-        for key, value in batch.items():
-            if key in {"input_ids", "attention_mask"}:
-                batch[key] = value[:, :max_seq_length].contiguous()
-        return self.hparams.max_seq_length - max_seq_length
-
-    @staticmethod
-    def _pad(tensor: torch.Tensor, padding_length: int) -> torch.Tensor:
-        batch_size, _, num_labels = tensor.shape
-        padding = torch.zeros((batch_size, padding_length, num_labels), dtype=tensor.dtype, device=tensor.device)
-        return torch.cat([tensor, padding], dim=1)
+        encoded = self.encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
+        return {
+            "kdr_logits": self.kdr_tagger(encoded.last_hidden_state),
+            "ins_logits": self.ins_tagger(encoded.last_hidden_state),
+        }
 
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         ret: Dict[str, torch.Tensor] = self(batch)
@@ -72,20 +52,20 @@ def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         self.log("train/ins_loss", ins_loss)
         return kdr_loss + ins_loss
 
-    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"kdr_labels": batch["kdr_labels"], "ins_labels": batch["ins_labels"]})
-        corpus = self.valid_corpora[dataloader_idx or 0]
-        self.valid_corpus2typo_module_metric[corpus].update(kwargs)
-
-    def validation_epoch_end(self, validation_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.valid_corpora}
-        for corpus, typo_module_metric in self.valid_corpus2typo_module_metric.items():
-            dataset = self.trainer.val_dataloaders[self.valid_corpora.index(corpus)].dataset
-            typo_module_metric.set_properties({"dataset": dataset})
-            metrics = typo_module_metric.compute()
-            metrics_log[corpus] = metrics
-            typo_module_metric.reset()
+        metric = self.valid_corpus2metric[self.valid_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_validation_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.valid_corpus2metric.items():
+            dataset = self.trainer.val_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics_log[corpus] = metric.compute()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"valid_{corpus}/{key}": value for key, value in metrics.items()})
@@ -93,20 +73,20 @@ def validation_epoch_end(self, validation_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.valid_corpora if key in metrics_log[corpus])
             self.log(f"valid/{key}", mean_score)
 
-    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"kdr_labels": batch["kdr_labels"], "ins_labels": batch["ins_labels"]})
-        corpus = self.test_corpora[dataloader_idx or 0]
-        self.test_corpus2typo_module_metric[corpus].update(kwargs)
-
-    def test_epoch_end(self, test_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.test_corpora}
-        for corpus, typo_module_metric in self.test_corpus2typo_module_metric.items():
-            dataset = self.trainer.test_dataloaders[self.test_corpora.index(corpus)].dataset
-            typo_module_metric.set_properties({"dataset": dataset})
-            metrics = typo_module_metric.compute()
-            metrics_log[corpus] = metrics
-            typo_module_metric.reset()
+        metric = self.test_corpus2metric[self.test_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_test_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.test_corpus2metric.items():
+            dataset = self.trainer.test_dataloaders[corpus].dataset
+            metric.set_properties({"dataset": dataset})
+            metrics_log[corpus] = metric.compute()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"test_{corpus}/{key}": value for key, value in metrics.items()})
@@ -114,7 +94,7 @@ def test_epoch_end(self, test_step_outputs) -> None:
             mean_score = mean(metrics_log[corpus][key] for corpus in self.test_corpora if key in metrics_log[corpus])
             self.log(f"test/{key}", mean_score)
 
-    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Dict[str, torch.Tensor]:
+    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Dict[str, torch.Tensor]:
         ret: Dict[str, torch.Tensor] = self(batch)
         kdr_probabilities = ret["kdr_logits"].softmax(dim=2)
         kdr_max_probabilities, kdr_predictions = kdr_probabilities.max(dim=2)
diff --git a/src/kwja/modules/word.py b/src/kwja/modules/word.py
index 3dcdd7b7..fb93580a 100644
--- a/src/kwja/modules/word.py
+++ b/src/kwja/modules/word.py
@@ -1,17 +1,28 @@
+import os
+from functools import reduce
 from statistics import mean
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict, List
 
 import hydra
 import torch
 from omegaconf import DictConfig
 from transformers import PretrainedConfig, PreTrainedModel
 
-from kwja.metrics import WordModuleMetric
 from kwja.modules.base import BaseModule
 from kwja.modules.components.crf import CRF
-from kwja.modules.components.head import SequenceLabelingHead, WordSelectionHead
+from kwja.modules.components.head import (
+    LoRASequenceMultiLabelingHead,
+    RelationWiseWordSelectionHead,
+    SequenceLabelingHead,
+    WordSelectionHead,
+)
 from kwja.modules.components.pooling import PoolingStrategy, pool_subwords
-from kwja.modules.functions.loss import compute_multi_label_token_mean_loss, compute_token_mean_loss, mask_logits
+from kwja.modules.functions.loss import (
+    compute_cohesion_analysis_loss,
+    compute_multi_label_token_mean_loss,
+    compute_token_mean_loss,
+    mask_logits,
+)
 from kwja.utils.cohesion_analysis import BridgingUtils, CoreferenceUtils, PasUtils
 from kwja.utils.constants import (
     BASE_PHRASE_FEATURES,
@@ -30,57 +41,49 @@
 )
 from kwja.utils.reading_prediction import get_reading2reading_id
 
+if os.environ.get("KWJA_CLI_MODE") == "1":
+    from kwja.modules.base import DummyModuleMetric as WordModuleMetric  # dummy class for faster loading
+else:
+    from kwja.metrics import WordModuleMetric  # type: ignore
 
-class WordModule(BaseModule):
+
+class WordModule(BaseModule[WordModuleMetric]):
     def __init__(self, hparams: DictConfig) -> None:
-        super().__init__(hparams)
+        super().__init__(hparams, WordModuleMetric())
 
         self.training_tasks: List[WordTask] = list(map(WordTask, self.hparams.training_tasks))
 
-        if valid_corpora := getattr(hparams.datamodule, "valid", None):
-            self.valid_corpora: List[str] = list(valid_corpora)
-            self.valid_corpus2word_module_metric: Dict[str, WordModuleMetric] = {
-                corpus: WordModuleMetric() for corpus in self.valid_corpora
-            }
-        if test_corpora := getattr(hparams.datamodule, "test", None):
-            self.test_corpora: List[str] = list(test_corpora)
-            self.test_corpus2word_module_metric: Dict[str, WordModuleMetric] = {
-                corpus: WordModuleMetric() for corpus in self.test_corpora
-            }
-
-        self.word_encoder: PreTrainedModel = hydra.utils.call(hparams.encoder)
-        pretrained_model_config: PretrainedConfig = self.word_encoder.config
+        self.encoder: PreTrainedModel = hydra.utils.call(hparams.encoder.from_config)
+        pretrained_model_config: PretrainedConfig = self.encoder.config
         if hasattr(hparams, "special_tokens"):
-            self.word_encoder.resize_token_embeddings(pretrained_model_config.vocab_size + len(hparams.special_tokens))
-        head_args: Tuple[int, float] = (
-            pretrained_model_config.hidden_size,
-            pretrained_model_config.hidden_dropout_prob,
-        )
+            self.encoder.resize_token_embeddings(pretrained_model_config.vocab_size + len(hparams.special_tokens))
+        head_kwargs: Dict[str, Any] = dict(hidden_size=self.encoder.config.hidden_size, hidden_dropout_prob=0.05)
 
         # ---------- reading prediction ----------
-        reading2reading_id: Dict[str, int] = get_reading2reading_id(RESOURCE_PATH / "reading_prediction" / "vocab.txt")
-        self.reading_id2reading: Dict[int, str] = {v: k for k, v in reading2reading_id.items()}
-        self.reading_tagger = SequenceLabelingHead(len(reading2reading_id), *head_args)
+        self.reading_id2reading: Dict[int, str] = {
+            v: k for k, v in get_reading2reading_id(RESOURCE_PATH / "reading_prediction" / "vocab.txt").items()
+        }
+        self.reading_tagger = SequenceLabelingHead(len(self.reading_id2reading), **head_kwargs)
 
         # ---------- morphological analysis ----------
-        self.pos_tagger = SequenceLabelingHead(len(POS_TAGS), *head_args)
-        self.subpos_tagger = SequenceLabelingHead(len(SUBPOS_TAGS), *head_args)
-        self.conjtype_tagger = SequenceLabelingHead(len(CONJTYPE_TAGS), *head_args)
-        self.conjform_tagger = SequenceLabelingHead(len(CONJFORM_TAGS), *head_args)
+        self.pos_tagger = SequenceLabelingHead(len(POS_TAGS), **head_kwargs)
+        self.subpos_tagger = SequenceLabelingHead(len(SUBPOS_TAGS), **head_kwargs)
+        self.conjtype_tagger = SequenceLabelingHead(len(CONJTYPE_TAGS), **head_kwargs)
+        self.conjform_tagger = SequenceLabelingHead(len(CONJFORM_TAGS), **head_kwargs)
 
         # ---------- word feature tagging ----------
-        self.word_feature_tagger = SequenceLabelingHead(len(WORD_FEATURES), *head_args, multi_label=True)
+        self.word_feature_tagger = LoRASequenceMultiLabelingHead(len(WORD_FEATURES), **head_kwargs)
 
         # ---------- named entity recognition ----------
-        self.ne_tagger = SequenceLabelingHead(len(NE_TAGS), *head_args)
+        self.ne_tagger = SequenceLabelingHead(len(NE_TAGS), **head_kwargs)
         self.crf = CRF(NE_TAGS)
 
         # ---------- base phrase feature tagging ----------
-        self.base_phrase_feature_tagger = SequenceLabelingHead(len(BASE_PHRASE_FEATURES), *head_args, multi_label=True)
+        self.base_phrase_feature_tagger = LoRASequenceMultiLabelingHead(len(BASE_PHRASE_FEATURES), **head_kwargs)
 
         # ---------- dependency parsing ----------
-        self.topk: int = hparams.dependency_topk
-        self.dependency_parser = WordSelectionHead(1, *head_args)
+        self.dependency_topk: int = hparams.dependency_topk
+        self.dependency_parser = WordSelectionHead(1, **head_kwargs)
         self.dependency_type_parser = SequenceLabelingHead(
             len(DEPENDENCY_TYPES),
             pretrained_model_config.hidden_size * 2,
@@ -88,53 +91,46 @@ def __init__(self, hparams: DictConfig) -> None:
         )
 
         # ---------- cohesion analysis ----------
-        self.cohesion_analyzer = WordSelectionHead(self._get_num_cohesion_rels(hparams), *head_args)
+        self.cohesion_analyzer = RelationWiseWordSelectionHead(self._get_num_cohesion_rels(hparams), **head_kwargs)
 
         # ---------- discourse parsing ----------
         self.discourse_parsing_threshold: float = hparams.discourse_parsing_threshold
-        self.discourse_parser = WordSelectionHead(len(DISCOURSE_RELATIONS), *head_args)
+        self.discourse_parser = WordSelectionHead(len(DISCOURSE_RELATIONS), **head_kwargs)
 
     @staticmethod
     def _get_num_cohesion_rels(hparams: DictConfig) -> int:
-        num_cohesion_rels = 0
-        kwargs = {
-            "exophora_referents": hparams.exophora_referents,
-            "restrict_target": hparams.restrict_cohesion_target,
-        }
-        for cohesion_task in [CohesionTask(t) for t in hparams.cohesion_tasks]:
-            if cohesion_task == CohesionTask.PAS_ANALYSIS:
-                pas_utils = PasUtils(hparams.pas_cases, "all", **kwargs)
-                num_cohesion_rels += len(pas_utils.rels)
-            elif cohesion_task == CohesionTask.BRIDGING_REFERENCE_RESOLUTION:
-                bridging_utils = BridgingUtils(hparams.br_cases, **kwargs)
-                num_cohesion_rels += len(bridging_utils.rels)
-            elif cohesion_task == CohesionTask.COREFERENCE_RESOLUTION:
-                coreference_utils = CoreferenceUtils(**kwargs)
-                num_cohesion_rels += len(coreference_utils.rels)
+        cohesion_tasks = [CohesionTask(ct) for ct in hparams.cohesion_tasks]
+        num_cohesion_rels: int = 0
+        cohesion_utils_args = (hparams.exophora_referents, hparams.restrict_cohesion_target)
+        if CohesionTask.PAS_ANALYSIS in cohesion_tasks:
+            num_cohesion_rels += len(PasUtils(hparams.pas_cases, "all", *cohesion_utils_args).rels)
+        if CohesionTask.BRIDGING_REFERENCE_RESOLUTION in cohesion_tasks:
+            num_cohesion_rels += len(BridgingUtils(hparams.br_cases, *cohesion_utils_args).rels)
+        if CohesionTask.COREFERENCE_RESOLUTION in cohesion_tasks:
+            num_cohesion_rels += len(CoreferenceUtils(*cohesion_utils_args).rels)
         return num_cohesion_rels
 
+    def setup(self, stage: str) -> None:
+        if stage == "fit":
+            self.encoder = hydra.utils.call(self.hparams.encoder.from_pretrained)
+            if hasattr(self.hparams, "special_tokens"):
+                self.encoder.resize_token_embeddings(self.encoder.config.vocab_size + len(self.hparams.special_tokens))
+
     def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
-        # (b, seq, hid)
-        encoded = self.word_encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])
-        pooled = pool_subwords(encoded.last_hidden_state, batch["subword_map"], PoolingStrategy.FIRST)
+        encoded = self.encoder(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"])  # (b, seq, hid)
+        pooled = pool_subwords(encoded.last_hidden_state, batch["subword_map"], PoolingStrategy.FIRST)  # (b, seq, hid)
 
-        dependency_logits = self.dependency_parser(pooled)
+        dependency_logits = self.dependency_parser(pooled)  # (b, seq, seq, 1)
         masked_dependency_logits = mask_logits(dependency_logits.squeeze(3), batch["dependency_mask"])
-        if 0 not in batch["dependency_labels"].size():
-            dependency_labels = batch["dependency_labels"]
-            dependency_labels = (dependency_labels * dependency_labels.ne(IGNORE_INDEX)).unsqueeze(2).unsqueeze(3)
-            topk = 1
+        if batch["dependency_labels"].size(1) == 0:
+            dependency_labels = masked_dependency_logits.topk(self.dependency_topk, dim=2).indices.unsqueeze(3)
         else:
-            dependency_labels = masked_dependency_logits.topk(self.topk, dim=2).indices.unsqueeze(3)
-            topk = self.topk
+            dependency_labels_mask = batch["dependency_labels"].ne(IGNORE_INDEX)
+            dependency_labels = (batch["dependency_labels"] * dependency_labels_mask).unsqueeze(2).unsqueeze(3)
         unsqueezed = pooled.unsqueeze(2)
-        batch_size, seq_len, hidden_size = pooled.shape
-        source_shape = (batch_size, seq_len, seq_len, hidden_size)
-        target_shape = (batch_size, seq_len, topk, hidden_size)
-        # gather は IGNORE_INDEX を渡せない
-        head_hidden_states = unsqueezed.expand(source_shape).gather(2, dependency_labels.expand(target_shape))
+        head_hidden_states = torch.take_along_dim(unsqueezed, dependency_labels, dim=1)
 
-        cohesion_logits = self.cohesion_analyzer(pooled)
+        cohesion_logits = self.cohesion_analyzer(pooled)  # (b, seq, seq, rel)
         cohesion_logits = cohesion_logits.permute(0, 3, 1, 2).contiguous()  # -> (b, rel, seq, seq)
         masked_cohesion_logits = mask_logits(cohesion_logits, batch["cohesion_mask"])
 
@@ -149,7 +145,7 @@ def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
             "base_phrase_feature_probabilities": self.base_phrase_feature_tagger(pooled),
             "dependency_logits": masked_dependency_logits,
             "dependency_type_logits": self.dependency_type_parser(
-                torch.cat([unsqueezed.expand(target_shape), head_hidden_states], dim=3)
+                torch.cat([unsqueezed.expand(head_hidden_states.shape), head_hidden_states], dim=3)
             ),
             "cohesion_logits": masked_cohesion_logits,
             "discourse_logits": self.discourse_parser(pooled),
@@ -157,134 +153,129 @@ def forward(self, batch: Any) -> Dict[str, torch.Tensor]:
 
     def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
         ret: Dict[str, torch.Tensor] = self(batch)
-        loss = torch.tensor(0.0, device=self.device)
+        loss_log: Dict[str, torch.Tensor] = {}
 
         if WordTask.READING_PREDICTION in self.training_tasks:
-            reading_prediction_loss = compute_token_mean_loss(ret["reading_logits"], batch["reading_labels"])
-            loss += reading_prediction_loss
-            self.log("train/reading_prediction_loss", reading_prediction_loss)
+            loss_log["reading_prediction_loss"] = compute_token_mean_loss(
+                ret["reading_logits"], batch["reading_labels"]
+            )
 
         if WordTask.MORPHOLOGICAL_ANALYSIS in self.training_tasks:
-            pos_loss = compute_token_mean_loss(ret["pos_logits"], batch["pos_labels"])
-            subpos_loss = compute_token_mean_loss(ret["subpos_logits"], batch["subpos_labels"])
-            conjtype_loss = compute_token_mean_loss(ret["conjtype_logits"], batch["conjtype_labels"])
-            conjform_loss = compute_token_mean_loss(ret["conjform_logits"], batch["conjform_labels"])
-            morphological_analysis_loss = (pos_loss + subpos_loss + conjtype_loss + conjform_loss) / 4
-            loss += morphological_analysis_loss
-            self.log("train/morphological_analysis_loss", morphological_analysis_loss)
+            morphological_analysis_losses = [
+                compute_token_mean_loss(ret["pos_logits"], batch["pos_labels"]),
+                compute_token_mean_loss(ret["subpos_logits"], batch["subpos_labels"]),
+                compute_token_mean_loss(ret["conjtype_logits"], batch["conjtype_labels"]),
+                compute_token_mean_loss(ret["conjform_logits"], batch["conjform_labels"]),
+            ]
+            loss_log["morphological_analysis_loss"] = torch.stack(morphological_analysis_losses).mean()
 
         if WordTask.WORD_FEATURE_TAGGING in self.training_tasks:
-            word_feature_mask = batch["word_feature_labels"].ne(IGNORE_INDEX)
-            word_feature_tagging_loss = compute_multi_label_token_mean_loss(
-                ret["word_feature_probabilities"] * word_feature_mask,
-                batch["word_feature_labels"].float() * word_feature_mask,
-                word_feature_mask,
+            loss_log["word_feature_tagging_loss"] = compute_multi_label_token_mean_loss(
+                ret["word_feature_probabilities"],
+                batch["word_feature_labels"],
             )
-            loss += word_feature_tagging_loss
-            self.log("train/word_feature_tagging_loss", word_feature_tagging_loss)
 
         if WordTask.NER in self.training_tasks:
-            ne_labels = torch.where(batch["target_mask"] == 1, batch["ne_labels"], NE_TAGS.index("O"))
-            ner_loss = self.crf(ret["ne_logits"], ne_labels, mask=batch["target_mask"])
-            loss += ner_loss
-            self.log("train/ner_loss", ner_loss)
+            loss_log["ner_loss"] = self.crf(ret["ne_logits"], batch["ne_labels"], mask=batch["ne_mask"])
 
         if WordTask.BASE_PHRASE_FEATURE_TAGGING in self.training_tasks:
-            base_phrase_feature_mask = batch["base_phrase_feature_labels"].ne(IGNORE_INDEX)
-            base_phrase_feature_tagging_loss = compute_multi_label_token_mean_loss(
-                ret["base_phrase_feature_probabilities"] * base_phrase_feature_mask,
-                batch["base_phrase_feature_labels"].float() * base_phrase_feature_mask,
-                base_phrase_feature_mask,
+            loss_log["base_phrase_feature_tagging_loss"] = compute_multi_label_token_mean_loss(
+                ret["base_phrase_feature_probabilities"],
+                batch["base_phrase_feature_labels"],
             )
-            loss += base_phrase_feature_tagging_loss
-            self.log("train/base_phrase_feature_tagging_loss", base_phrase_feature_tagging_loss)
 
         if WordTask.DEPENDENCY_PARSING in self.training_tasks:
-            dependency_parsing_loss = compute_token_mean_loss(ret["dependency_logits"], batch["dependency_labels"])
-            top1 = ret["dependency_type_logits"][:, :, 0, :]
-            dependency_type_parsing_loss = compute_token_mean_loss(top1, batch["dependency_type_labels"])
-            loss += dependency_parsing_loss
-            loss += dependency_type_parsing_loss
-            self.log("train/dependency_parsing_loss", dependency_parsing_loss)
-            self.log("train/dependency_type_parsing_loss", dependency_type_parsing_loss)
+            loss_log["dependency_parsing_loss"] = compute_token_mean_loss(
+                ret["dependency_logits"], batch["dependency_labels"]
+            )
+            top1 = ret["dependency_type_logits"][:, :, 0]
+            loss_log["dependency_type_parsing_loss"] = compute_token_mean_loss(top1, batch["dependency_type_labels"])
 
         if WordTask.COHESION_ANALYSIS in self.training_tasks:
-            log_softmax = torch.log_softmax(ret["cohesion_logits"], dim=3)  # (b, rel, seq, seq)
-            denominator = batch["cohesion_labels"].sum() + 1e-6
-            cohesion_analysis_loss = (-log_softmax * batch["cohesion_labels"]).sum().div(denominator)
-            loss += cohesion_analysis_loss
-            self.log("train/cohesion_analysis_loss", cohesion_analysis_loss)
+            loss_log["cohesion_analysis_loss"] = compute_cohesion_analysis_loss(
+                ret["cohesion_logits"], batch["cohesion_labels"]
+            )
 
         if WordTask.DISCOURSE_PARSING in self.training_tasks:
-            discourse_parsing_loss = compute_token_mean_loss(ret["discourse_logits"], batch["discourse_labels"])
-            loss += discourse_parsing_loss
-            self.log("train/discourse_parsing_loss", discourse_parsing_loss)
+            loss_log["discourse_parsing_loss"] = compute_token_mean_loss(
+                ret["discourse_logits"], batch["discourse_labels"]
+            )
+
+        self.log_dict({f"train/{key}": value for key, value in loss_log.items()})
 
-        return loss
+        return torch.stack(list(loss_log.values())).sum()
 
-    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def validation_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"discourse_labels": batch["discourse_labels"]})
-        corpus = self.valid_corpora[dataloader_idx or 0]
-        self.valid_corpus2word_module_metric[corpus].update(kwargs)
-
-    def validation_epoch_end(self, validation_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.valid_corpora}
-        for corpus, word_module_metric in self.valid_corpus2word_module_metric.items():
-            dataset = self.trainer.val_dataloaders[self.valid_corpora.index(corpus)].dataset
-            word_module_metric.set_properties(
+        metric = self.valid_corpus2metric[self.valid_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_validation_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.valid_corpus2metric.items():
+            dataset = self.trainer.val_dataloaders[corpus].dataset
+            metric.set_properties(
                 {
                     "dataset": dataset,
                     "reading_id2reading": self.reading_id2reading,
                     "training_tasks": self.training_tasks,
                 }
             )
-            metrics = word_module_metric.compute()
+            metrics = metric.compute()
+            if corpus != "kwdlc":
+                # discourse parsing labels are available only for KWDLC
+                metrics = {key: value for key, value in metrics.items() if not key.startswith("discourse_parsing")}
             metrics["aggregated_word_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            word_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"valid_{corpus}/{key}": value for key, value in metrics.items()})
-        for key in list(metrics_log.values())[0].keys():
+        for key in reduce(set.union, [set(metrics.keys()) for metrics in metrics_log.values()]):
             mean_score = mean(metrics_log[corpus][key] for corpus in self.valid_corpora if key in metrics_log[corpus])
             self.log(f"valid/{key}", mean_score)
 
-    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> None:
+    def test_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> None:
         kwargs = self.predict_step(batch, batch_idx, dataloader_idx=dataloader_idx)
         kwargs.update({"discourse_labels": batch["discourse_labels"]})
-        corpus = self.test_corpora[dataloader_idx or 0]
-        self.test_corpus2word_module_metric[corpus].update(kwargs)
-
-    def test_epoch_end(self, test_step_outputs) -> None:
-        metrics_log: Dict[str, Dict[str, float]] = {corpus: {} for corpus in self.test_corpora}
-        for corpus, word_module_metric in self.test_corpus2word_module_metric.items():
-            dataset = self.trainer.test_dataloaders[self.test_corpora.index(corpus)].dataset
-            word_module_metric.set_properties(
+        metric = self.test_corpus2metric[self.test_corpora[dataloader_idx]]
+        metric.pad(kwargs, self.hparams.max_seq_length)
+        metric.update(kwargs)
+
+    def on_test_epoch_end(self) -> None:
+        metrics_log: Dict[str, Dict[str, float]] = {}
+        for corpus, metric in self.test_corpus2metric.items():
+            dataset = self.trainer.test_dataloaders[corpus].dataset
+            metric.set_properties(
                 {
                     "dataset": dataset,
                     "reading_id2reading": self.reading_id2reading,
                     "training_tasks": self.training_tasks,
                 }
             )
-            metrics = word_module_metric.compute()
+            metrics = metric.compute()
+            if corpus != "kwdlc":
+                # discourse parsing labels are available only for KWDLC
+                metrics = {key: value for key, value in metrics.items() if not key.startswith("discourse_parsing")}
             metrics["aggregated_word_metrics"] = mean(
                 metrics[key] for key in self.hparams.aggregating_metrics if key in metrics
             )
             metrics_log[corpus] = metrics
-            word_module_metric.reset()
+            metric.reset()
 
         for corpus, metrics in metrics_log.items():
             self.log_dict({f"test_{corpus}/{key}": value for key, value in metrics.items()})
-        for key in list(metrics_log.values())[0].keys():
+        for key in reduce(set.union, [set(metrics.keys()) for metrics in metrics_log.values()]):
             mean_score = mean(metrics_log[corpus][key] for corpus in self.test_corpora if key in metrics_log[corpus])
             self.log(f"test/{key}", mean_score)
 
-    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int] = None) -> Dict[str, torch.Tensor]:
+    def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: int = 0) -> Dict[str, torch.Tensor]:
         ret: Dict[str, torch.Tensor] = self(batch)
-        ne_predictions = self.crf.viterbi_decode(ret["ne_logits"], batch["target_mask"])
+        ne_predictions = self.crf.viterbi_decode(ret["ne_logits"], batch["ne_mask"])
         discourse_probabilities = ret["discourse_logits"].softmax(dim=3)
         discourse_max_probabilities, discourse_predictions = discourse_probabilities.max(dim=3)
         discourse_unconfident_indices = discourse_max_probabilities < self.discourse_parsing_threshold
@@ -300,7 +291,7 @@ def predict_step(self, batch: Any, batch_idx: int, dataloader_idx: Optional[int]
             "word_feature_probabilities": ret["word_feature_probabilities"],
             "ne_predictions": ne_predictions,
             "base_phrase_feature_probabilities": ret["base_phrase_feature_probabilities"],
-            "dependency_predictions": ret["dependency_logits"].topk(k=self.topk, dim=2).indices,
+            "dependency_predictions": ret["dependency_logits"].topk(k=self.dependency_topk, dim=2).indices,
             "dependency_type_predictions": ret["dependency_type_logits"].argmax(dim=3),
             "cohesion_logits": ret["cohesion_logits"],
             "discourse_predictions": discourse_predictions,
diff --git a/src/kwja/utils/cohesion_analysis.py b/src/kwja/utils/cohesion_analysis.py
index 39bbca3a..3b8825ec 100644
--- a/src/kwja/utils/cohesion_analysis.py
+++ b/src/kwja/utils/cohesion_analysis.py
@@ -66,7 +66,7 @@ def __init__(
         exophora_referents: List[ExophoraReferent],
         restrict_target: bool,
     ) -> None:
-        super().__init__(exophora_referents, restrict_target=restrict_target)
+        super().__init__(exophora_referents, restrict_target)
         self.cases = cases
         self.target = target
 
@@ -127,7 +127,7 @@ def _get_argument_tags(self, arguments: List[Argument], antecedent_candidates: L
                 argument_tag = str(target_argument.base_phrase.global_index)
             else:
                 assert isinstance(target_argument, ExophoraArgument)
-                argument_tag = target_argument.exophora_referent.text
+                argument_tag = f"[{target_argument.exophora_referent.text}]"  # 著者 -> [著者]
             argument_tags.append(argument_tag)
         return argument_tags or ["[NULL]"]
 
@@ -153,7 +153,7 @@ def __init__(
         exophora_referents: List[ExophoraReferent],
         restrict_target: bool,
     ) -> None:
-        super().__init__(exophora_referents, restrict_target=restrict_target)
+        super().__init__(exophora_referents, restrict_target)
         assert "ノ" in cases, '"ノ" case isn\'t found'
         self.cases = cases
 
@@ -212,7 +212,7 @@ def _get_argument_tags(self, arguments: List[Argument], antecedent_candidates: L
                 argument_tag = str(target_argument.base_phrase.global_index)
             else:
                 assert isinstance(target_argument, ExophoraArgument)
-                argument_tag = target_argument.exophora_referent.text
+                argument_tag = f"[{target_argument.exophora_referent.text}]"  # 著者 -> [著者]
             argument_tags.append(argument_tag)
         return argument_tags or ["[NULL]"]
 
@@ -235,7 +235,7 @@ def __init__(
         exophora_referents: List[ExophoraReferent],
         restrict_target: bool,
     ) -> None:
-        super().__init__(exophora_referents, restrict_target=restrict_target)
+        super().__init__(exophora_referents, restrict_target)
 
     @property
     def rels(self) -> List[str]:
@@ -269,7 +269,7 @@ def _get_mention_tags(self, mention: BasePhrase, antecedent_candidates: List[Bas
         for exophora_referent in [e.exophora_referent for e in mention.entities if e.exophora_referent is not None]:
             exophora_referent.index = None  # 不特定:人１ -> 不特定:人
             if exophora_referent in self.exophora_referents:
-                mention_tags.append(exophora_referent.text)
+                mention_tags.append(f"[{exophora_referent.text}]")  # 著者 -> [著者]
         return mention_tags or ["[NA]"]
 
     def is_target(self, base_phrase: BasePhrase) -> bool:
diff --git a/src/kwja/utils/constants.py b/src/kwja/utils/constants.py
index 6f6270f8..9362ad86 100644
--- a/src/kwja/utils/constants.py
+++ b/src/kwja/utils/constants.py
@@ -2,7 +2,10 @@
 from enum import Enum
 from typing import Dict, Optional, Tuple
 
-import importlib_resources
+try:
+    from importlib.resources import files as resource_files  # type: ignore
+except ImportError:
+    from importlib_resources import files as resource_files
 from rhoknp.props import DepType, NamedEntityCategory
 
 import kwja
@@ -10,15 +13,15 @@
 # ---------- common ----------
 IGNORE_INDEX = -100
 MASKED = -1024.0
-RESOURCE_PATH = importlib_resources.files(kwja) / "resource"
+RESOURCE_PATH = resource_files(kwja) / "resource"
 
 # ---------- senter module ----------
 SENT_SEGMENTATION_TAGS = ("B", "I")
 
 # ---------- seq2seq module----------
-NEW_LINE_TOKEN: str = "<br>"
-NO_READING_TOKEN: str = "<no_read>"
-NO_CANON_TOKEN: str = "<no_canon>"
+NEW_LINE_TOKEN: str = "<extra_id_0>"  # "<br>"
+FULL_SPACE_TOKEN: str = "<extra_id_1>"  # "<full_space>"
+NO_CANON_TOKEN: str = "<extra_id_2>"  # "<no_canon>"
 
 # ---------- word (inference) dataset ----------
 SPLIT_INTO_WORDS_MODEL_NAMES = [
@@ -197,11 +200,15 @@
     "れ": "え",
 }
 
+_HIRAGANA = "ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろわをんーゎゐゑゕゖゔゝゞ"
+_KATAKANA = "ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロワヲンーヮヰヱヵヶヴヽヾ"
+KATA2HIRA = str.maketrans(_KATAKANA, _HIRAGANA)
+
 
 # ---------- char module|text normalization ----------
 # 制御文字(\t,\nを含む)は削除
 TRANSLATION_TABLE: Dict[int, Optional[int]] = str.maketrans(
-    ' "▁', "␣”▂", "".join(chr(i) for i in [*range(32), *range(127, 160)])
+    ' "#▁', "␣”＃▂", "".join(chr(i) for i in [*range(32), *range(127, 160)])
 )
 
 
diff --git a/src/kwja/utils/jumandic.py b/src/kwja/utils/jumandic.py
index 248ccff6..68bcc515 100644
--- a/src/kwja/utils/jumandic.py
+++ b/src/kwja/utils/jumandic.py
@@ -5,18 +5,14 @@
 
 import cdblib
 
-from kwja.utils.progress_bar import track
+from kwja.utils.logging_util import track
 
 
 class JumanDic:
     def __init__(self, dic_dir: Path) -> None:
-        with dic_dir.joinpath("jumandic.db").open(mode="rb") as f:
-            self.jumandic = cdblib.Reader(f.read())
-        with dic_dir.joinpath("jumandic_canon.db").open(mode="rb") as f:
-            self.jumandic_canon = cdblib.Reader(f.read())
-
-        with dic_dir.joinpath("grammar.json").open(mode="rt") as f:
-            self.grammar_data = json.loads(f.read())
+        self.jumandic = cdblib.Reader(dic_dir.joinpath("jumandic.db").read_bytes())
+        self.jumandic_canon = cdblib.Reader(dic_dir.joinpath("jumandic_canon.db").read_bytes())
+        self.grammar_data: Dict[str, List[str]] = json.loads(dic_dir.joinpath("grammar.json").read_text())
 
     def lookup_by_norm(self, surf: str) -> List[Dict[str, str]]:
         buf = self.jumandic.get(surf.encode("utf-8"))
@@ -111,31 +107,27 @@ def _build_reverse_lookup(dic) -> List[str]:
             surf2entries[surf] = surf2entries.get(surf, b"") + v
             canon2entries[canon] = canon2entries.get(canon, b"") + v
 
-        out_dir.joinpath("jumandic.db").unlink(missing_ok=True)
         with out_dir.joinpath("jumandic.db").open(mode="wb") as f1:
             with cdblib.Writer(f1) as writer1:
                 for key_surf, value_entries in surf2entries.items():
                     writer1.put(key_surf.encode("utf-8"), value_entries)
 
-        out_dir.joinpath("jumandic_canon.db").unlink(missing_ok=True)
         with out_dir.joinpath("jumandic_canon.db").open(mode="wb") as f2:
             with cdblib.Writer(f2) as writer2:
                 for key_canon, value_entries in canon2entries.items():
                     writer2.put(key_canon.encode("utf-8"), value_entries)
 
-        out_dir.joinpath("grammar.json").unlink(missing_ok=True)
-        with out_dir.joinpath("grammar.json").open(mode="wt") as f3:
-            f3.write(
-                json.dumps(
-                    {
-                        "id2reading": _build_reverse_lookup(reading2id),
-                        "id2lemma": _build_reverse_lookup(lemma2id),
-                        "id2pos": _build_reverse_lookup(pos2id),
-                        "id2subpos": _build_reverse_lookup(subpos2id),
-                        "id2conjtype": _build_reverse_lookup(conjtype2id),
-                        "id2conjform": _build_reverse_lookup(conjform2id),
-                        "id2semantics": _build_reverse_lookup(semantics2id),
-                    },
-                    ensure_ascii=False,
-                )
+        out_dir.joinpath("grammar.json").write_text(
+            json.dumps(
+                {
+                    "id2reading": _build_reverse_lookup(reading2id),
+                    "id2lemma": _build_reverse_lookup(lemma2id),
+                    "id2pos": _build_reverse_lookup(pos2id),
+                    "id2subpos": _build_reverse_lookup(subpos2id),
+                    "id2conjtype": _build_reverse_lookup(conjtype2id),
+                    "id2conjform": _build_reverse_lookup(conjform2id),
+                    "id2semantics": _build_reverse_lookup(semantics2id),
+                },
+                ensure_ascii=False,
             )
+        )
diff --git a/src/kwja/utils/kanjidic.py b/src/kwja/utils/kanjidic.py
index a3c0adce..2173fe41 100644
--- a/src/kwja/utils/kanjidic.py
+++ b/src/kwja/utils/kanjidic.py
@@ -1,7 +1,7 @@
 import unicodedata
 from typing import Any, Dict
 
-import jaconv
+from kwja.utils.constants import KATA2HIRA
 
 
 class KanjiDic:
@@ -324,7 +324,7 @@ def _parse(self, fpath: str) -> None:
                         else:
                             struct["korean"].append(f[1:])
                     elif unicodedata.name(f[0])[0:8] in ("HIRAGANA", "KATAKANA"):
-                        f = jaconv.kata2hira(f)
+                        f = f.translate(KATA2HIRA)
                         if f[-1] == "-":
                             f = f[:-1]
                         if "reading" not in struct:
diff --git a/src/kwja/utils/progress_bar.py b/src/kwja/utils/logging_util.py
similarity index 56%
rename from src/kwja/utils/progress_bar.py
rename to src/kwja/utils/logging_util.py
index a15a1725..b730273f 100644
--- a/src/kwja/utils/progress_bar.py
+++ b/src/kwja/utils/logging_util.py
@@ -1,12 +1,45 @@
+import logging
 import sys
+import warnings
 from datetime import timedelta
 from functools import partial
-from typing import Iterable, List, Optional, Sequence, Union
+from typing import Iterable, List, Literal, Optional, Sequence, Union
 
+from lightning_fabric.utilities.warnings import PossibleUserWarning
 from rich.console import Console
 from rich.progress import BarColumn, Progress, ProgressColumn, ProgressType, TextColumn
 from rich.style import StyleType
 from rich.text import Text
+from transformers.utils import logging as hf_logging
+
+
+def filter_logs(environment: Literal["development", "production"]) -> None:
+    logging.getLogger("rhoknp").setLevel(logging.ERROR)
+    hf_logging.set_verbosity(hf_logging.ERROR)
+    if environment == "production":
+        warnings.filterwarnings("ignore")
+        logging.getLogger("kwja").setLevel(logging.ERROR)
+        logging.getLogger("torch").setLevel(logging.ERROR)
+        logging.getLogger("pytorch_lightning").setLevel(logging.ERROR)
+    elif environment == "development":
+        warnings.filterwarnings(
+            "ignore",
+            message=(
+                r"It is recommended to use .+ when logging on epoch level in distributed setting to accumulate the metric"
+                r" across devices"
+            ),
+            category=PossibleUserWarning,
+        )
+        warnings.filterwarnings(
+            "ignore",
+            message=(
+                r"Using `DistributedSampler` with the dataloaders. During `trainer..+`, it is recommended to use"
+                r" `Trainer(devices=1, num_nodes=1)` to ensure each sample/batch gets evaluated exactly once. Otherwise,"
+                r" multi-device settings use `DistributedSampler` that replicates some samples to make sure all devices have"
+                r" same batch size in case of uneven inputs."
+            ),
+            category=PossibleUserWarning,
+        )
 
 
 class CustomPostfixColumn(ProgressColumn):
diff --git a/src/kwja/utils/reader.py b/src/kwja/utils/reader.py
new file mode 100644
index 00000000..70bac356
--- /dev/null
+++ b/src/kwja/utils/reader.py
@@ -0,0 +1,30 @@
+from typing import Iterator, List, TextIO
+
+from rhoknp import Sentence
+
+
+def chunk_by_document_for_line_by_line_text(f: TextIO) -> Iterator[str]:
+    sentences: List[Sentence] = []
+    prev_doc_id = ""
+    for sentence_text in chunk_by_sentence_for_line_by_line_text(f):
+        sentence = Sentence.from_raw_text(sentence_text)
+        if sentence.doc_id != prev_doc_id:
+            if sentences:
+                yield "".join(sentence.to_raw_text() for sentence in sentences)
+                sentences = []
+        sentences.append(sentence)
+        prev_doc_id = sentence.doc_id
+    yield "".join(sentence.to_raw_text() for sentence in sentences)
+
+
+def chunk_by_sentence_for_line_by_line_text(f: TextIO) -> Iterator[str]:
+    buffer = []
+    for line in f:
+        if line.strip() == "":
+            continue
+        buffer.append(line)
+        if not Sentence.is_comment_line(line):
+            yield "".join(buffer)
+            buffer = []
+    if buffer:
+        yield "".join(buffer)
diff --git a/src/kwja/utils/reading_prediction.py b/src/kwja/utils/reading_prediction.py
index 7cd115d4..96de6ea2 100644
--- a/src/kwja/utils/reading_prediction.py
+++ b/src/kwja/utils/reading_prediction.py
@@ -7,7 +7,6 @@
 from pathlib import Path
 from typing import Dict, List, Literal, Optional, Tuple, Union
 
-import jaconv
 import numpy as np
 from rhoknp import Document, Morpheme
 from transformers import AutoTokenizer, PreTrainedTokenizerBase
@@ -17,6 +16,7 @@
     HATSUON_SET,
     ID,
     ID_ID,
+    KATA2HIRA,
     LOWER2UPPER,
     PROLONGED_MAP,
     PROLONGED_MAP_FOR_EROW,
@@ -127,7 +127,7 @@ class Node:
                     kanji_reading_list = self._extend_kanji_reading_list(self.kanji_dic.entries[surf[i - 1]]["reading"])
             for j in range(len(reading)):
                 cj = reading[j]
-                if jaconv.kata2hira(ci) == cj:
+                if ci.translate(KATA2HIRA) == cj:
                     node = Node(i=i, j=j, wI=1, wJ=1, cost=0)
                     td_lattice[i][j].append(node)
                 if ci in VOICED2VOICELESS and cj == VOICED2VOICELESS[ci]:
@@ -165,7 +165,7 @@ class Node:
                             if reading_part == kanji_reading2:
                                 node = Node(i=i, j=j, wI=1, wJ=len(kanji_reading), cost=10)
                                 td_lattice[i][j].append(node)
-                        # TODO: combinatoin
+                        # TODO: combination
                 # fallback nodes
                 if cj in ("ぁ", "ぃ", "ぅ", "ぇ", "ぉ", "ゃ", "ゅ", "ょ", "っ", "ん", "ー"):
                     initial_penalty = 500
@@ -321,7 +321,7 @@ def main():
     reading_counter: Dict[str, int] = Counter()
     for path in Path(args.input).glob("**/*.knp"):
         logger.info(f"processing {path}")
-        with path.open(mode="r") as f:
+        with path.open() as f:
             document = Document.from_knp(f.read())
         try:
             for reading in reading_aligner.align(document.morphemes):
diff --git a/src/kwja/utils/seq2seq_format.py b/src/kwja/utils/seq2seq_format.py
index 4596e915..4f3f5ea8 100644
--- a/src/kwja/utils/seq2seq_format.py
+++ b/src/kwja/utils/seq2seq_format.py
@@ -3,20 +3,23 @@
 
 from rhoknp import Sentence
 
-from kwja.utils.constants import NO_CANON_TOKEN, NO_READING_TOKEN
+from kwja.utils.constants import FULL_SPACE_TOKEN, NO_CANON_TOKEN
 
 
 def get_seq2seq_format(sentence: Sentence) -> str:
     output: str = ""
     for mrph in sentence.morphemes:
-        if mrph.reading == "\u3000":
-            reading: str = NO_READING_TOKEN
-        elif "/" in mrph.reading:
-            reading = mrph.reading.split("/")[0]
+        if mrph.surf == "\u3000":
+            mrph_info: str = f"{FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {NO_CANON_TOKEN}\n"
         else:
-            reading = mrph.reading
-        canon: str = mrph.canon if mrph.canon is not None else NO_CANON_TOKEN
-        mrph_info: str = f"{mrph.surf} {reading} {mrph.lemma} {canon}\n"
+            if mrph.reading == "\u3000":
+                reading: str = FULL_SPACE_TOKEN
+            elif "/" in mrph.reading and len(mrph.reading) > 1:
+                reading = mrph.reading.split("/")[0]
+            else:
+                reading = mrph.reading
+            canon: str = mrph.canon if mrph.canon is not None else NO_CANON_TOKEN
+            mrph_info = f"{mrph.surf} {reading} {mrph.lemma} {canon}\n"
         output += mrph_info
     return output
 
@@ -28,28 +31,29 @@ def get_sent_from_seq2seq_format(input_text: str) -> Sentence:
     for line in lines:
         if not line:
             continue
-        if line == "EOS" or line.startswith("*") or line.startswith("+"):
+        if line == "EOS":
             formatted += line + "\n"
         else:
-            preds: List[str] = line.split(" ")
+            preds: List[str] = line.split(" ")  # surf reading lemma canon
+            mrphs: List[str] = copy.deepcopy(mrph_placeholder)
             if len(preds) == 4:
-                mrphs: List[str] = copy.deepcopy(mrph_placeholder)
-                for idx in range(3):
-                    mrphs[idx] = preds[idx]
-                mrphs[-1] = "NIL" if preds[3] == NO_CANON_TOKEN else f'"代表表記:{preds[3]}"'
-                formatted += " ".join(mrphs) + "\n"
-            elif line in ["!!!!/!", "????/?", ",,,,/,"]:
-                mrphs = copy.deepcopy(mrph_placeholder)
+                mrphs[0] = "\u3000" if preds[0] == FULL_SPACE_TOKEN else preds[0]
+                mrphs[1] = "\u3000" if preds[1] == FULL_SPACE_TOKEN else preds[1]
+                mrphs[2] = "\u3000" if preds[2] == FULL_SPACE_TOKEN else preds[2]
+                if preds[3] == NO_CANON_TOKEN:
+                    if preds[0] == FULL_SPACE_TOKEN and preds[1] == FULL_SPACE_TOKEN and preds[2] == FULL_SPACE_TOKEN:
+                        mrphs[-1] = '"代表表記:/"'
+                    else:
+                        mrphs[-1] = "NIL"
+                else:
+                    mrphs[-1] = f'"代表表記:{preds[3]}"'
+            elif line in ["!!!!/!", "????/?", ",,,,/,", "..../."]:
                 for idx in range(3):
                     mrphs[idx] = line[idx]
-                mrphs[-1] = f'"代表表記:{line[-1]}/{line[-1]}"'
-                formatted += " ".join(mrphs) + "\n"
+                mrphs[-1] = f'"代表表記:{line[0]}/{line[0]}"'
             elif line == "............/...":
-                mrphs = copy.deepcopy(mrph_placeholder)
                 for idx in range(3):
                     mrphs[idx] = "…"
                 mrphs[-1] = '"代表表記:…/…"'
-                formatted += " ".join(mrphs) + "\n"
-            else:
-                formatted += " ".join(mrph_placeholder) + "\n"
+            formatted += " ".join(mrphs) + "\n"
     return Sentence.from_jumanpp(formatted)
diff --git a/sweep/senter_base.yaml b/sweep/senter_base.yaml
new file mode 100644
index 00000000..e5214b7a
--- /dev/null
+++ b/sweep/senter_base.yaml
@@ -0,0 +1,25 @@
+project: kwja
+entity: ueda
+program: scripts/train.py
+method: bayes
+metric:
+  name: valid/aggregated_senter_metrics
+  goal: maximize
+parameters:
+  effective_batch_size:
+    values: [16, 32, 48]
+  lr:
+    values: [0.00001, 0.00002, 0.00003, 0.00004, 0.00005]
+  max_epochs:
+    values: [12, 16, 20, 24]
+  warmup_steps:
+    values: [500, 1000, 1500, 2000]
+command:
+  - ${env}
+  - ${interpreter}
+  - ${program}
+  - "-cn"
+  - "senter_module"
+  - "encoder=char_deberta_base"
+  - "do_predict_after_train=false"
+  - ${args_no_hyphens}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1451871a..cb88921b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,24 +7,21 @@ jobs:
     name: Run tests with pytest
     runs-on: ubuntu-latest
     strategy:
-      max-parallel: 3
+      max-parallel: 4
       fail-fast: false
       matrix:
-        python-version: [ "3.8", "3.9", "3.10" ]
+        python-version: [ "3.8", "3.9", "3.10", "3.11" ]
     steps:
       - name: Checkout repository
         uses: actions/checkout@v3
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v3
+        uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python-version }}
       - name: Install Poetry
         run: |
-          python3 -m pip install --user pipx
-          python3 -m pipx ensurepath
-          python3 -m pipx install poetry
-      - name: Add path for Python packages
-        run: echo "$HOME/.local/bin" >> $GITHUB_PATH
+            pipx install poetry
+            echo "$HOME/.local/bin" >> $GITHUB_PATH
       - name: Install dependencies
         run: |
           poetry config virtualenvs.create false
@@ -42,3 +39,25 @@ jobs:
           root_dir: true
           name: codecov-umbrella
           verbose: true
+
+  test-cli:
+    name: Install KWJA from pip and run CLI
+    runs-on: ubuntu-latest
+    strategy:
+      max-parallel: 4
+      fail-fast: false
+      matrix:
+        python-version: [ "3.8", "3.9", "3.10", "3.11" ]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install KWJA from pip
+        run: pip3 install .
+      - name: Run CLI
+        run: |
+          kwja --version
+          kwja --help
diff --git a/configs/datamodule/test/seq2seq_norm.yaml b/configs/datamodule/test/seq2seq_norm.yaml
new file mode 100644
index 00000000..2e8fba4c
--- /dev/null
+++ b/configs/datamodule/test/seq2seq_norm.yaml
@@ -0,0 +1,5 @@
+defaults:
+  - /datamodule/base/seq2seq@norm
+
+norm:
+  path: ${oc.env:DATA_DIR}/norm/test
diff --git a/scripts/test.py b/scripts/test.py
index 9257d360..3b3eab39 100644
--- a/scripts/test.py
+++ b/scripts/test.py
@@ -1,48 +1,39 @@
-import logging
-import warnings
-from typing import List, Optional
+from typing import List, Union
 
 import hydra
 import pytorch_lightning as pl
-import transformers.utils.logging as hf_logging
+import torch
 from dotenv import load_dotenv
-from lightning_fabric.utilities.warnings import PossibleUserWarning
 from omegaconf import DictConfig, ListConfig, OmegaConf
 from pytorch_lightning.callbacks import Callback
 from pytorch_lightning.loggers import Logger
 from pytorch_lightning.trainer.states import TrainerFn
 
 from kwja.datamodule.datamodule import DataModule
+from kwja.utils.logging_util import filter_logs
 
-hf_logging.set_verbosity(hf_logging.ERROR)
-logging.getLogger("rhoknp").setLevel(logging.ERROR)
-warnings.filterwarnings(
-    "ignore",
-    message=r"It is recommended to use .+ when logging on epoch level in distributed setting to accumulate the metric"
-    r" across devices",
-    category=PossibleUserWarning,
-)
-OmegaConf.register_new_resolver("concat", lambda x, y: x + y)
+filter_logs(environment="development")
 
 
 @hydra.main(version_base=None, config_path="../configs", config_name="eval")
 def main(eval_cfg: DictConfig):
     load_dotenv()
     if isinstance(eval_cfg.devices, str):
-        try:
-            eval_cfg.devices = [int(x) for x in eval_cfg.devices.split(",")]
-        except ValueError:
-            eval_cfg.devices = None
+        eval_cfg.devices = (
+            list(map(int, eval_cfg.devices.split(","))) if "," in eval_cfg.devices else int(eval_cfg.devices)
+        )
 
     # Load saved model and config
     model: pl.LightningModule = hydra.utils.call(eval_cfg.module.load_from_checkpoint, _recursive_=False)
+    if eval_cfg.compile is True:
+        model = torch.compile(model)  # type: ignore
 
     train_cfg: DictConfig = model.hparams
     OmegaConf.set_struct(train_cfg, False)  # enable to add new key-value pairs
     cfg = OmegaConf.merge(train_cfg, eval_cfg)
     assert isinstance(cfg, DictConfig)
 
-    logger: Optional[Logger] = cfg.get("logger") and hydra.utils.instantiate(cfg.get("logger"))
+    logger: Union[Logger, bool] = cfg.get("logger", False) and hydra.utils.instantiate(cfg.get("logger"))
     callbacks: List[Callback] = list(map(hydra.utils.instantiate, cfg.get("callbacks", {}).values()))
 
     num_devices: int = len(cfg.devices) if isinstance(cfg.devices, (list, ListConfig)) else cfg.devices
diff --git a/tests/callbacks/test_char_module_writer.py b/tests/callbacks/test_char_module_writer.py
index 709b7f0d..58f2ae49 100644
--- a/tests/callbacks/test_char_module_writer.py
+++ b/tests/callbacks/test_char_module_writer.py
@@ -1,3 +1,5 @@
+import tempfile
+from importlib.metadata import version
 from pathlib import Path
 from tempfile import TemporaryDirectory
 from textwrap import dedent
@@ -5,11 +7,9 @@
 
 import pytest
 import torch
-from omegaconf import ListConfig
 from torch.utils.data import DataLoader
 from transformers import PreTrainedTokenizerBase
 
-import kwja
 from kwja.callbacks.char_module_writer import CharModuleWriter
 from kwja.datamodule.datasets import CharInferenceDataset
 from kwja.utils.constants import WORD_NORM_OP_TAGS, WORD_SEGMENTATION_TAGS
@@ -33,15 +33,24 @@ def test_init(destination: Optional[Union[str, Path]]):
 
 
 def test_write_on_batch_end(char_tokenizer: PreTrainedTokenizerBase):
-    texts = ["花咲ガニを買ぅ", "うまそーですね〜〜"]
     max_seq_length = 32
     doc_id_prefix = "test"
+    senter_text = dedent(
+        f"""\
+        # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
+        花咲ガニを買ぅ
+        # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
+        うまそーですね〜〜
+        """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
     dataset = CharInferenceDataset(
-        texts=ListConfig(texts),
         tokenizer=char_tokenizer,
         max_seq_length=max_seq_length,
         document_split_stride=-1,
-        doc_id_prefix=doc_id_prefix,
+        senter_file=Path(senter_file.name),
     )
     num_examples = len(dataset)
 
@@ -97,13 +106,13 @@ def test_write_on_batch_end(char_tokenizer: PreTrainedTokenizerBase):
         assert isinstance(writer.destination, Path), "destination isn't set"
         assert writer.destination.read_text() == dedent(
             f"""\
-            # S-ID:{doc_id_prefix}-0-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
             花咲 _ 花咲 未定義語 15 その他 1 * 0 * 0
             ガニ _ カニ 未定義語 15 その他 1 * 0 * 0
             を _ を 未定義語 15 その他 1 * 0 * 0
             買ぅ _ 買う 未定義語 15 その他 1 * 0 * 0
             EOS
-            # S-ID:{doc_id_prefix}-1-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
             うま _ うま 未定義語 15 その他 1 * 0 * 0
             そーです _ そうです 未定義語 15 その他 1 * 0 * 0
             ね〜〜 _ ねえ 未定義語 15 その他 1 * 0 * 0
diff --git a/tests/callbacks/test_senter_module_writer.py b/tests/callbacks/test_senter_module_writer.py
new file mode 100644
index 00000000..98145f3f
--- /dev/null
+++ b/tests/callbacks/test_senter_module_writer.py
@@ -0,0 +1,94 @@
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from textwrap import dedent
+from typing import List, Optional, Union
+
+import pytest
+import torch
+from omegaconf import ListConfig
+from torch.utils.data import DataLoader
+from transformers import PreTrainedTokenizerBase
+
+import kwja
+from kwja.callbacks.senter_module_writer import SenterModuleWriter
+from kwja.callbacks.utils import SENT_SEGMENTATION_TAGS
+from kwja.datamodule.datasets import SenterInferenceDataset
+
+
+class MockTrainer:
+    def __init__(self, predict_dataloaders: List[DataLoader]):
+        self.predict_dataloaders = predict_dataloaders
+
+
+@pytest.mark.parametrize(
+    "destination",
+    [
+        None,
+        Path(TemporaryDirectory().name) / Path("senter_prediction.txt"),
+        str(Path(TemporaryDirectory().name) / Path("senter_prediction.txt")),
+    ],
+)
+def test_init(destination: Optional[Union[str, Path]]):
+    _ = SenterModuleWriter(destination=destination)
+
+
+def test_write_on_batch_end(char_tokenizer: PreTrainedTokenizerBase) -> None:
+    texts = ["一文目。二文目。", "違う文書の一文目。二文目。"]
+    num_examples = 2
+    max_seq_length = 32
+
+    probabilities = torch.zeros((num_examples, max_seq_length, len(SENT_SEGMENTATION_TAGS)), dtype=torch.float)
+    probabilities[0, 0, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # [CLS]
+    probabilities[0, 1, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # 一
+    probabilities[0, 2, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 文
+    probabilities[0, 3, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 目
+    probabilities[0, 4, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 。
+    probabilities[0, 5, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # 二
+    probabilities[0, 6, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 文
+    probabilities[0, 7, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 目
+    probabilities[0, 8, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 。
+    probabilities[0, 9, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # [SEP]
+    probabilities[1, 0, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # [CLS]
+    probabilities[1, 1, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # 違
+    probabilities[1, 2, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # う
+    probabilities[1, 3, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 文
+    probabilities[1, 4, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 書
+    probabilities[1, 5, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # の
+    probabilities[1, 6, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 一
+    probabilities[1, 7, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 文
+    probabilities[1, 8, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 目
+    probabilities[1, 9, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 。
+    probabilities[1, 10, SENT_SEGMENTATION_TAGS.index("B")] = 1.0  # 二
+    probabilities[1, 11, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 文
+    probabilities[1, 12, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 目
+    probabilities[1, 13, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # 。
+    probabilities[1, 14, SENT_SEGMENTATION_TAGS.index("I")] = 1.0  # [SEP]
+
+    predictions = probabilities.argmax(dim=2)
+    prediction = {
+        "example_ids": torch.arange(num_examples, dtype=torch.long),
+        "sent_segmentation_predictions": predictions,
+    }
+
+    doc_id_prefix = "test"
+    with TemporaryDirectory() as tmp_dir:
+        destination = tmp_dir / Path("senter_prediction.txt")
+        expected_texts = [
+            dedent(
+                f"""\
+                # S-ID:{doc_id_prefix}-0-1 kwja:{kwja.__version__}
+                一文目。
+                # S-ID:{doc_id_prefix}-0-2 kwja:{kwja.__version__}
+                二文目。
+                # S-ID:{doc_id_prefix}-1-1 kwja:{kwja.__version__}
+                違う文書の一文目。
+                # S-ID:{doc_id_prefix}-1-2 kwja:{kwja.__version__}
+                二文目。"""
+            ),
+        ]
+        dataset = SenterInferenceDataset(ListConfig(texts), char_tokenizer, max_seq_length, doc_id_prefix=doc_id_prefix)
+        trainer = MockTrainer([DataLoader(dataset, batch_size=num_examples)])
+        writer = SenterModuleWriter(destination=destination)
+        writer.write_on_batch_end(trainer, ..., prediction, None, ..., 0, 0)
+        assert isinstance(writer.destination, Path), "destination isn't set"
+        assert writer.destination.read_text() == expected_texts[0]
diff --git a/tests/callbacks/test_seq2seq_module_writer.py b/tests/callbacks/test_seq2seq_module_writer.py
index ede3abff..9f1b03a5 100644
--- a/tests/callbacks/test_seq2seq_module_writer.py
+++ b/tests/callbacks/test_seq2seq_module_writer.py
@@ -1,3 +1,5 @@
+import tempfile
+from importlib.metadata import version
 from pathlib import Path
 from tempfile import TemporaryDirectory
 from textwrap import dedent
@@ -5,11 +7,9 @@
 
 import pytest
 import torch
-from omegaconf import ListConfig
 from torch.utils.data import DataLoader
 from transformers import PreTrainedTokenizerBase
 
-import kwja
 from kwja.callbacks.seq2seq_module_writer import Seq2SeqModuleWriter
 from kwja.datamodule.datasets import Seq2SeqInferenceDataset
 
@@ -32,24 +32,34 @@ def test_init(destination: Optional[Union[str, Path]], seq2seq_tokenizer: PreTra
 
 
 def test_write_on_batch_end(seq2seq_tokenizer: PreTrainedTokenizerBase):
-    texts = ["太郎と次郎はよくけんかする", "辛いラーメンが好きなので頼みました"]
     max_src_length = 32
     max_tgt_length = 128
     doc_id_prefix = "test"
+    senter_text = dedent(
+        f"""\
+            # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
+            太郎と次郎はよくけんかする
+            # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
+            辛いラーメンが好きなので頼みました
+            """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
+
     dataset = Seq2SeqInferenceDataset(
-        texts=ListConfig(texts),
         tokenizer=seq2seq_tokenizer,
         max_src_length=max_src_length,
         max_tgt_length=max_tgt_length,
-        doc_id_prefix=doc_id_prefix,
+        senter_file=Path(senter_file.name),
     )
     num_examples = len(dataset)
 
     trainer = MockTrainer([DataLoader(dataset, batch_size=num_examples)])
 
     generated_texts = [
-        "太郎 たろう 太郎 太郎/たろう<br> と と と と/と<br> 次郎 じろう 次郎 次郎/じろう<br> は は は は/は<br> よく よく よい 良い/よい<br> けんか けんか けんか 喧嘩/けんか<br> する する する する/する<br>",
-        "辛い からい 辛い 辛い/からい<br> ラーメン らーめん ラーメン ラーメン/らーめん<br> が が が が/が<br> 好きな すきな 好きだ 好きだ/すきだ<br> ので ので のだ のだ/のだ<br> 頼み たのみ 頼む 頼む/たのむ<br> ました ました ます ます/ます<br>",
+        "太郎 たろう 太郎 太郎/たろう<extra_id_0> と と と と/と<extra_id_0> 次郎 じろう 次郎 次郎/じろう<extra_id_0> は は は は/は<extra_id_0> よく よく よい 良い/よい<extra_id_0> けんか けんか けんか 喧嘩/けんか<extra_id_0> する する する する/する<extra_id_0>",
+        "辛い からい 辛い 辛い/からい<extra_id_0> ラーメン らーめん ラーメン ラーメン/らーめん<extra_id_0> が が が が/が<extra_id_0> 好きな すきな 好きだ 好きだ/すきだ<extra_id_0> ので ので のだ のだ/のだ<extra_id_0> 頼み たのみ 頼む 頼む/たのむ<extra_id_0> ました ました ます ます/ます<extra_id_0>",
     ]
     seq2seq_predictions = torch.zeros((num_examples, max_tgt_length), dtype=torch.long)
     for i, generated_text in enumerate(generated_texts):
@@ -70,7 +80,7 @@ def test_write_on_batch_end(seq2seq_tokenizer: PreTrainedTokenizerBase):
         assert isinstance(writer.destination, Path), "destination isn't set"
         assert writer.destination.read_text() == dedent(
             f"""\
-            # S-ID:{doc_id_prefix}-0-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
             太郎 たろう 太郎 未定義語 15 その他 1 * 0 * 0 "代表表記:太郎/たろう"
             と と と 未定義語 15 その他 1 * 0 * 0 "代表表記:と/と"
             次郎 じろう 次郎 未定義語 15 その他 1 * 0 * 0 "代表表記:次郎/じろう"
@@ -79,7 +89,7 @@ def test_write_on_batch_end(seq2seq_tokenizer: PreTrainedTokenizerBase):
             けんか けんか けんか 未定義語 15 その他 1 * 0 * 0 "代表表記:喧嘩/けんか"
             する する する 未定義語 15 その他 1 * 0 * 0 "代表表記:する/する"
             EOS
-            # S-ID:{doc_id_prefix}-1-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
             辛い からい 辛い 未定義語 15 その他 1 * 0 * 0 "代表表記:辛い/からい"
             ラーメン らーめん ラーメン 未定義語 15 その他 1 * 0 * 0 "代表表記:ラーメン/らーめん"
             が が が 未定義語 15 その他 1 * 0 * 0 "代表表記:が/が"
diff --git a/tests/callbacks/test_typo_module_writer.py b/tests/callbacks/test_typo_module_writer.py
index ca6c193d..00cddf57 100644
--- a/tests/callbacks/test_typo_module_writer.py
+++ b/tests/callbacks/test_typo_module_writer.py
@@ -24,8 +24,8 @@ def __init__(self, predict_dataloaders: List[DataLoader]):
     "destination",
     [
         None,
-        Path(TemporaryDirectory().name) / Path("typo_prediction.juman"),
-        str(Path(TemporaryDirectory().name) / Path("typo_prediction.juman")),
+        Path(TemporaryDirectory().name) / "typo_prediction.juman",
+        str(Path(TemporaryDirectory().name) / "typo_prediction.juman"),
     ],
 )
 def test_init(destination: Optional[Union[str, Path]], typo_tokenizer: PreTrainedTokenizerBase):
@@ -37,10 +37,7 @@ def test_write_on_batch_end(typo_tokenizer: PreTrainedTokenizerBase):
     num_examples = 2  # num_stash = 2
     max_seq_length = 32  # >= 11
 
-    token2token_id, _ = get_maps(
-        typo_tokenizer,
-        RESOURCE_PATH / "typo_correction" / "multi_char_vocab.txt",
-    )
+    token2token_id, _ = get_maps(typo_tokenizer, RESOURCE_PATH / "typo_correction" / "multi_char_vocab.txt")
 
     kdr_probabilities = torch.zeros((num_examples, max_seq_length, len(token2token_id)), dtype=torch.float)
     kdr_probabilities[0, 0, token2token_id["<k>"]] = 1.0  # [CLS]
diff --git a/tests/callbacks/test_word_module_writer.py b/tests/callbacks/test_word_module_writer.py
index 06d0c73f..109f8328 100644
--- a/tests/callbacks/test_word_module_writer.py
+++ b/tests/callbacks/test_word_module_writer.py
@@ -1,4 +1,5 @@
 import tempfile
+from importlib.metadata import version
 from pathlib import Path
 from textwrap import dedent
 from typing import Any, Dict, List, Optional, Union
@@ -10,7 +11,6 @@
 from torch.utils.data import DataLoader
 from transformers import PreTrainedTokenizerBase
 
-import kwja
 from kwja.callbacks.word_module_writer import WordModuleWriter
 from kwja.datamodule.datasets import WordInferenceDataset
 from kwja.utils.constants import (
@@ -80,7 +80,7 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
     doc_id_prefix = "test"
     juman_text = dedent(
         f"""\
-        # S-ID:{doc_id_prefix}-0-0 kwja:{kwja.__version__}
+        # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
         太郎 _ 太郎 未定義語 15 その他 1 * 0 * 0
         と _ と 未定義語 15 その他 1 * 0 * 0
         次郎 _ 次郎 未定義語 15 その他 1 * 0 * 0
@@ -89,7 +89,7 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
         けんか _ けんか 未定義語 15 その他 1 * 0 * 0
         する _ する 未定義語 15 その他 1 * 0 * 0
         EOS
-        # S-ID:{doc_id_prefix}-1-0 kwja:{kwja.__version__}
+        # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
         辛い _ 辛い 未定義語 15 その他 1 * 0 * 0
         ラーメン _ ラーメン 未定義語 15 その他 1 * 0 * 0
         が _ が 未定義語 15 その他 1 * 0 * 0
@@ -308,14 +308,16 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
     dependency_logits[0, 2, 5] = 1.0  # 次郎 -> けんか
     dependency_logits[0, 3, 2] = 1.0  # は -> 次郎
     dependency_logits[0, 4, 5] = 1.0  # よく -> けんか
-    dependency_logits[0, 5, dataset.special_token2index["[ROOT]"]] = 1.0  # けんか -> [ROOT]
+    # けんか -> [ROOT]
+    dependency_logits[0, 5, dataset.examples[0].special_token_indexer.get_morpheme_level_index("[ROOT]")] = 1.0
     dependency_logits[0, 6, 5] = 1.0  # する -> けんか
     dependency_logits[1, 0, 1] = 1.0  # 辛い -> ラーメン
     dependency_logits[1, 1, 3] = 1.0  # ラーメン -> 好きな
     dependency_logits[1, 2, 1] = 1.0  # が -> ラーメン
     dependency_logits[1, 3, 5] = 1.0  # 好きな -> 頼み
     dependency_logits[1, 4, 3] = 1.0  # ので -> 好きな
-    dependency_logits[1, 5, dataset.special_token2index["[ROOT]"]] = 1.0  # 頼み -> [ROOT]
+    # 頼み -> [ROOT]
+    dependency_logits[1, 5, dataset.examples[1].special_token_indexer.get_morpheme_level_index("[ROOT]")] = 1.0
     dependency_logits[1, 6, 5] = 1.0  # ました -> 頼み
 
     dependency_type_logits = torch.zeros(
@@ -339,14 +341,22 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
     # (b, rel, src, tgt)
     flatten_rels = [r for cohesion_utils in dataset.cohesion_task2utils.values() for r in cohesion_utils.rels]
     cohesion_logits = torch.zeros((num_examples, len(flatten_rels), max_seq_length, max_seq_length), dtype=torch.float)
-    for i, rel in enumerate(flatten_rels):
-        j = dataset.special_token2index["[NA]"] if rel == "=" else dataset.special_token2index["[NULL]"]
-        cohesion_logits[:, i, :, j] = 1.0
+    for i in range(num_examples):
+        for j, rel in enumerate(flatten_rels):
+            if rel == "=":
+                k = dataset.examples[i].special_token_indexer.get_morpheme_level_index("[NA]")
+            else:
+                k = dataset.examples[i].special_token_indexer.get_morpheme_level_index("[NULL]")
+            cohesion_logits[i, j, :, k] = 1.0
     cohesion_logits[0, flatten_rels.index("ガ"), 5, 2] = 2.0  # 次郎 ガ けんか
     cohesion_logits[1, flatten_rels.index("ガ"), 0, 1] = 2.0  # ラーメン ガ 辛い
     cohesion_logits[1, flatten_rels.index("ガ"), 3, 1] = 2.0  # ラーメン ガ 好き
-    cohesion_logits[1, flatten_rels.index("ガ２"), 3, dataset.special_token2index["著者"]] = 2.0  # 著者 ガ２ 好き
-    cohesion_logits[1, flatten_rels.index("ガ"), 5, dataset.special_token2index["著者"]] = 2.0  # 著者 ガ 頼み
+    cohesion_logits[
+        1, flatten_rels.index("ガ２"), 3, dataset.examples[1].special_token_indexer.get_morpheme_level_index("[著者]")
+    ] = 2.0  # 著者 ガ２ 好き
+    cohesion_logits[
+        1, flatten_rels.index("ガ"), 5, dataset.examples[1].special_token_indexer.get_morpheme_level_index("[著者]")
+    ] = 2.0  # 著者 ガ 頼み
     cohesion_logits[1, flatten_rels.index("ヲ"), 5, 1] = 2.0  # ラーメン ヲ 頼み
 
     # (b, src, tgt, rel)
@@ -381,7 +391,7 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
         assert isinstance(writer.destination, Path), "destination isn't set"
         assert writer.destination.read_text() == dedent(
             f"""\
-            # S-ID:{doc_id_prefix}-0-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-0-0 kwja:{version("kwja")}
             * 1P
             + 1P <NE:PERSON:太郎><体言><SM-主体>
             太郎 たろう 太郎 名詞 6 人名 5 * 0 * 0 "代表表記:太郎/たろう 人名" <基本句-主辞>
@@ -398,7 +408,7 @@ def test_write_on_batch_end(word_tokenizer: PreTrainedTokenizerBase, dataset_kwa
             けんか けんか けんか 名詞 6 サ変名詞 2 * 0 * 0 "代表表記:喧嘩/けんか" <基本句-主辞><用言表記先頭><用言表記末尾><ALT-けんか-けんか-けんか-6-2-0-0-"代表表記:献花/けんか">
             する する する 動詞 2 * 0 サ変動詞 16 基本形 2
             EOS
-            # S-ID:{doc_id_prefix}-1-0 kwja:{kwja.__version__}
+            # S-ID:{doc_id_prefix}-1-0 kwja:{version("kwja")}
             * 1D
             + 1D <rel type="ガ" target="ラーメン" sid="test-1-0" id="1"/><用言:形><時制:非過去><レベル:B-><状態述語>
             辛い からい 辛い 形容詞 3 * 0 イ形容詞アウオ段 18 基本形 2 <基本句-主辞><用言表記先頭><用言表記末尾>
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
index d37ec4fd..febc892c 100644
--- a/tests/cli/test_cli.py
+++ b/tests/cli/test_cli.py
@@ -1,4 +1,4 @@
-from typing import List
+from typing import List, Set, Tuple
 
 from typer.testing import CliRunner
 
@@ -29,31 +29,32 @@ def test_task_input():
         "",
         "dummy",
         "typo",
-        "char",
-        "word",
-        "word_discourse",
-        "typo,char",
-        "typo,word",
-        "typo,word_discourse",
-        "char,word",
-        "char,word_discourse",
-        "word,word_discourse",
-        "typo,char,word",
-        "typo,char,word_discourse",
-        "typo,word,word_discourse",
-        "char,word,word_discourse",
-        "typo,char,word,word_discourse",
-    ]
-    valid_tasks: List[str] = [
-        "typo",
-        "char",
-        "typo,char",
-        "char,word",
-        "typo,char,word",
-        "char,word,word_discourse",
-        "typo,char,word,word_discourse",
+        "typo,senter",
+        "typo,senter,char",
+        "typo,senter,word",
+        "typo,senter,seq2seq",
+        "typo,senter,seq2seq,char",
+        "typo,senter,seq2seq,word",
+        "senter",
+        "senter,char",
+        "senter,char,word",
+        "senter,seq2seq",
+        "senter,seq2seq,word",
     ]
+    valid_tasks: Set[Tuple[str, ...]] = {
+        ("typo",),
+        ("typo", "senter"),
+        ("typo", "senter", "char"),
+        ("typo", "senter", "char", "word"),
+        ("typo", "senter", "seq2seq"),
+        ("typo", "senter", "seq2seq", "word"),
+        ("senter",),
+        ("senter", "char"),
+        ("senter", "char", "word"),
+        ("senter", "seq2seq"),
+        ("senter", "seq2seq", "word"),
+    }
     for task in tasks:
         ret = runner.invoke(app, args=["--model-size", "tiny", "--text", "おはよう", "--task", task])
-        if task not in valid_tasks:
+        if tuple(task.split(",")) not in valid_tasks:
             assert isinstance(ret.exception, SystemExit)
diff --git a/tests/conftest.py b/tests/conftest.py
index c10e2072..fe6b5433 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,7 +34,7 @@ def typo_tokenizer() -> PreTrainedTokenizerBase:
 def seq2seq_tokenizer() -> PreTrainedTokenizerBase:
     return AutoTokenizer.from_pretrained(
         "google/mt5-small",
-        additional_special_tokens=["<br>", "<no_read>", "<no_canon>"],
+        additional_special_tokens=[f"<extra_id_{idx}>" for idx in range(100)],
     )
 
 
@@ -70,7 +70,7 @@ def br_cases() -> List[str]:
 
 @pytest.fixture()
 def special_tokens(exophora_referents: List[str]) -> List[str]:
-    return exophora_referents + ["[NULL]", "[NA]", "[ROOT]"]
+    return [f"[{e}]" for e in exophora_referents] + ["[NULL]", "[NA]", "[ROOT]"]
 
 
 @pytest.fixture()
diff --git a/tests/data/modules/juman/004.juman b/tests/data/modules/juman/004.juman
index aab86e28..375ba897 100644
--- a/tests/data/modules/juman/004.juman
+++ b/tests/data/modules/juman/004.juman
@@ -4,4 +4,6 @@
 まで まで まで 助詞 9 格助詞 1 * 0 * 0 "代表表記:まで/まで"
 ！ ！ ！ 特殊 1 記号 5 * 0 * 0 "代表表記:！/！"
 ？ ？ ？ 特殊 1 記号 5 * 0 * 0 "代表表記:？/？"
+. . . 特殊 1 句点 1 * 0 * 0 "代表表記:./."
+/ / / 特殊 1 記号 5 * 0 * 0 "代表表記:///"
 EOS
diff --git a/tests/data/modules/juman/005.juman b/tests/data/modules/juman/005.juman
new file mode 100644
index 00000000..e5f523e5
--- /dev/null
+++ b/tests/data/modules/juman/005.juman
@@ -0,0 +1,4 @@
+ＪＵＭＰ ＪＵＭＰ ＪＵＭＰ 名詞 6 普通名詞 1 * 0 * 0 "代表表記:ＪＵＭＰ/ＪＵＭＰ 自動獲得:Wikipedia Wikipediaページ内一覧:うずまきナルト Wikipedia上位語:スタジオアルバム"
+　 　 　 特殊 1 空白 6 * 0 * 0 "代表表記:/"
+ＣＯＭＩＣＳ ＣＯＭＩＣＳ ＣＯＭＩＣＳ 名詞 6 普通名詞 1 * 0 * 0 "代表表記:ＣＯＭＩＣＳ/ＣＯＭＩＣＳ 自動獲得:Wikipedia Wikipediaページ内一覧:スペオペ宙学"
+EOS
diff --git a/tests/data/modules/permitted_tokens.json b/tests/data/modules/permitted_tokens.json
index e7726a9d..8b176b89 100644
--- a/tests/data/modules/permitted_tokens.json
+++ b/tests/data/modules/permitted_tokens.json
@@ -1,128 +1,254 @@
 {
   "case001": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>"],
-    "permitted_tokens": ["▁", "計", "計算"],
+    "t5": {
+      "input_tokens": ["<pad>"],
+      "permitted_tokens": ["▁", "計", "計算", "計算機"]
+    },
+    "mt5": {
+        "input_tokens": ["<pad>"],
+        "permitted_tokens": ["▁", "計", "計算"]
+    },
     "is_consecutive": false
   },
   "case002": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "▁"],
-    "permitted_tokens": ["計", "計算"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁"],
+      "permitted_tokens": ["計", "計算", "計算機"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁"],
+      "permitted_tokens": ["計", "計算"]
+    },
     "is_consecutive": false
   },
   "case003": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "計"],
-    "permitted_tokens": ["算"],
+    "t5": {
+      "input_tokens": ["<pad>", "計"],
+      "permitted_tokens": ["算"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "計"],
+      "permitted_tokens": ["算"]
+    },
     "is_consecutive": true
   },
   "case004": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "計算"],
-    "permitted_tokens": ["機", "機に"],
+    "t5": {
+      "input_tokens": ["<pad>", "計算"],
+      "permitted_tokens": ["機"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "計算"],
+      "permitted_tokens": ["機", "機に"]
+    },
     "is_consecutive": true
   },
   "case005": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "▁", "計算"],
-    "permitted_tokens": ["機", "機に"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算"],
+      "permitted_tokens": ["機"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算"],
+      "permitted_tokens": ["機", "機に"]
+    },
     "is_consecutive": true
   },
   "case006": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "▁", "計算", "▁"],
-    "permitted_tokens": [],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁"],
+      "permitted_tokens": []
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁"],
+      "permitted_tokens": []
+    },
     "is_consecutive": false
   },
   "case007": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<br>"],
-    "permitted_tokens": ["▁", "機", "機に"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "機"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "機", "機に"]
+    },
     "is_consecutive": false
   },
   "case008": {
     "text": "計算機による言語理解を実現する",
-    "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<br>", "▁"],
-    "permitted_tokens": ["機", "機に"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>", "▁"],
+      "permitted_tokens": ["機"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>", "▁"],
+      "permitted_tokens": ["機", "機に"]
+    },
     "is_consecutive": false
   },
   "case009": {
     "text": "あなたのメモリアルデーを可愛いミニマルちゃんで飾ってください。",
-    "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<br>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<br>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<br>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<br>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<br>", "▁", "可愛い"],
-    "permitted_tokens": ["ミ", "ミニ"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<extra_id_0>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "可愛い"],
+      "permitted_tokens": ["▁", "ミ", "ミニ"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<extra_id_0>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "可愛い"],
+      "permitted_tokens": ["▁", "ミ", "ミニ"]
+    },
     "is_consecutive": true
   },
   "case010": {
     "text": "あなたのメモリアルデーを可愛いミニマルちゃんで飾ってください。",
-    "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<br>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<br>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<br>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<br>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<br>", "▁", "可愛い", "▁", "かわいい", "▁", "可愛い", "▁", "可愛い", "/", "かわいい", "<br>"],
-    "permitted_tokens": ["▁", "ミ", "ミニ", "▁ミニ"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<extra_id_0>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "可愛い", "▁", "かわいい", "▁", "可愛い", "▁", "可愛い", "/", "かわいい", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "ミ", "ミニ"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "あなた", "▁", "あなた", "▁", "あなた", "▁", "あなた", "/", "あなた", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "メモリ", "アル", "▁", "め", "もり", "ある", "▁", "メモリ", "アル", "だ", "▁", "メモリ", "アル", "/", "め", "もり", "ある", "a", "<extra_id_0>", "▁", "デー", "▁", "で", "ー", "▁", "デー", "▁", "デー", "/", "で", "ー", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "可愛い", "▁", "かわいい", "▁", "可愛い", "▁", "可愛い", "/", "かわいい", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "ミ", "ミニ", "▁ミニ"]
+    },
     "is_consecutive": false
   },
   "case011": {
     "text": "村山富市首相は年頭の記者会見で、「創造とやさしさの国造りのビジョン」と題する所感を発表した。",
-    "input_tokens": ["<pad>", "▁", "村", "山", "▁", "むら", "やま", "▁", "村", "山", "▁", "村", "山", "/", "むら", "やま", "<br>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<br>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<br>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<br>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<br>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<br>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<br>", "▁", "会", "見", "▁", "かい", "けん", "▁", "会", "見", "▁", "会", "見", "/", "かい", "けん", "<br>", "▁", "で", "▁", "で", "▁", "で", "▁", "で", "/", "で", "<br>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<br>"],
-    "permitted_tokens": ["▁", "「", "▁「"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "村山", "▁", "むら", "やま", "▁", "村山", "▁", "村山", "/", "むら", "やま", "<extra_id_0>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<extra_id_0>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<extra_id_0>", "▁", "会見", "▁", "かい", "けん", "▁", "会見", "▁", "会見", "/", "かい", "けん", "<extra_id_0>", "▁", "で", "▁", "で", "▁", "で", "▁", "で", "/", "で", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "「"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "村", "山", "▁", "むら", "やま", "▁", "村", "山", "▁", "村", "山", "/", "むら", "やま", "<extra_id_0>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<extra_id_0>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<extra_id_0>", "▁", "会", "見", "▁", "かい", "けん", "▁", "会", "見", "▁", "会", "見", "/", "かい", "けん", "<extra_id_0>", "▁", "で", "▁", "で", "▁", "で", "▁", "で", "/", "で", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>"],
+      "permitted_tokens": ["▁", "「", "▁「"]
+    },
     "is_consecutive": false
   },
   "case012": {
     "text": "村山富市首相は年頭の記者会見で、「創造とやさしさの国造りのビジョン」と題する所感を発表した。",
-    "input_tokens": ["<pad>", "▁", "村", "山", "▁", "むら", "やま", "▁", "村", "山", "▁", "村", "山", "/", "むら", "やま", "<br>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<br>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<br>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<br>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<br>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<br>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<br>", "▁", "会", "見", "▁", "かい", "けん", "▁", "会", "見", "▁", "会", "見", "/", "かい", "けん", "<br>", "▁", "で", "▁", "で", "▁", "だ", "▁", "だ", "/", "だ", "<br>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<br>", "▁「", "▁"],
-    "permitted_tokens": [],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "村山", "▁", "むら", "やま", "▁", "村山", "▁", "村山", "/", "むら", "やま", "<extra_id_0>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<extra_id_0>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<extra_id_0>", "▁", "会見", "▁", "かい", "けん", "▁", "会見", "▁", "会見", "/", "かい", "けん", "<extra_id_0>", "▁", "で", "▁", "で", "▁", "で", "▁", "で", "/", "で", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "<extra_id_0>", "▁", "「", "▁"],
+      "permitted_tokens": []
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "村", "山", "▁", "むら", "やま", "▁", "村", "山", "▁", "村", "山", "/", "むら", "やま", "<extra_id_0>", "▁", "富", "市", "▁", "と", "み", "いち", "▁", "富", "市", "▁", "富", "市", "/", "と", "み", "いち", "<extra_id_0>", "▁", "首相", "▁", "しゅ", "しょう", "▁", "首相", "▁", "首相", "/", "しゅ", "しょう", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "年", "頭", "▁", "ねん", "とう", "▁", "年", "頭", "▁", "年", "頭", "/", "ねん", "とう", "<extra_id_0>", "▁", "の", "▁", "の", "▁", "の", "▁", "の", "/", "の", "<extra_id_0>", "▁", "記者", "▁", "き", "しゃ", "▁", "記者", "▁", "記者", "/", "き", "しゃ", "<extra_id_0>", "▁", "会", "見", "▁", "かい", "けん", "▁", "会", "見", "▁", "会", "見", "/", "かい", "けん", "<extra_id_0>", "▁", "で", "▁", "で", "▁", "で", "▁", "で", "/", "で", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "▁「", "▁"],
+      "permitted_tokens": []
+    },
     "is_consecutive": false
   },
   "case013": {
     "text": "第一は「自由で活力ある経済社会の創造」。",
-    "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<br>", "▁", "一", "▁", "いち", "▁", "一", "▁", "一", "/", "いち", "<br>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<br>", "▁", "「"],
-    "permitted_tokens": ["自", "自由"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<extra_id_0>", "▁", "一", "▁", "いち", "▁", "一", "▁", "一", "/", "いち", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "「"],
+      "permitted_tokens": ["自", "自由"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<extra_id_0>", "▁", "一", "▁", "いち", "▁", "一", "▁", "一", "/", "いち", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "「"],
+      "permitted_tokens": ["自", "自由"]
+    },
     "is_consecutive": true
   },
   "case014": {
     "text": "第一は「自由で活力ある経済社会の創造」。",
-    "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<br>", "▁", "一", "▁", "いっ", "▁", "一", "▁", "一", "/", "いち", "<br>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<br>", "▁「"],
-    "permitted_tokens": ["自", "自由"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<extra_id_0>", "▁", "一", "▁", "いっ", "▁", "一", "▁", "一", "/", "いち", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁", "「"],
+      "permitted_tokens": ["自", "自由"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁第", "▁", "だい", "▁第", "▁第", "/", "だい", "<extra_id_0>", "▁", "一", "▁", "いっ", "▁", "一", "▁", "一", "/", "いち", "<extra_id_0>", "▁", "は", "▁", "は", "▁", "は", "▁", "は", "/", "は", "<extra_id_0>", "▁「"],
+      "permitted_tokens": ["自", "自由"]
+    },
     "is_consecutive": true
   },
   "case015": {
     "text": "「わが国にふさわしい国際貢献による世界平和の創造」と銘打った非軍事分野の国際貢献など「四つの創造」を打ち出している。",
-    "input_tokens": ["<pad>", "▁「"],
-    "permitted_tokens": ["わ", "わが"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "「"],
+      "permitted_tokens": ["わ", "わが"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁「"],
+      "permitted_tokens": ["わ", "わが"]
+    },
     "is_consecutive": true
   },
   "case016": {
     "text": "「わが国にふさわしい国際貢献による世界平和の創造」と銘打った非軍事分野の国際貢献など「四つの創造」を打ち出している。",
-    "input_tokens": ["<pad>", "「"],
-    "permitted_tokens": ["わ", "わが"],
+    "t5": {
+      "input_tokens": ["<pad>", "「"],
+      "permitted_tokens": ["わ", "わが"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "「"],
+      "permitted_tokens": ["わ", "わが"]
+    },
     "is_consecutive": true
   },
   "case017": {
     "text": "本業から外れない、堅実な経営を心がけます。",
-    "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<br>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<br>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<br>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<br>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<br>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<br>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<br>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<br>", "▁"],
-    "permitted_tokens": ["心", "心が"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<extra_id_0>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<extra_id_0>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<extra_id_0>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<extra_id_0>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁"],
+      "permitted_tokens": ["心"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<extra_id_0>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<extra_id_0>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<extra_id_0>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<extra_id_0>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁"],
+      "permitted_tokens": ["心", "心が"]
+    },
     "is_consecutive": false
   },
   "case018": {
     "text": "本業から外れない、堅実な経営を心がけます。",
-    "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<br>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<br>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<br>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<br>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<br>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<br>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<br>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<br>", "▁", "心が"],
-    "permitted_tokens": ["け", "けます"],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<extra_id_0>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<extra_id_0>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<extra_id_0>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<extra_id_0>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "心"],
+      "permitted_tokens": ["が"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "本", "業", "▁", "ほん", "ぎ", "ょう", "▁", "本", "業", "▁", "本", "業", "/", "ほん", "ぎ", "ょう", "<extra_id_0>", "▁", "から", "▁", "から", "▁", "から", "▁", "から", "/", "から", "<extra_id_0>", "▁", "外", "れ", "▁", "はず", "れ", "▁", "外", "れる", "▁", "外", "れる", "/", "はず", "れる", "<extra_id_0>", "▁", "ない", "▁", "ない", "▁", "ない", "▁", "ない", "/", "ない", "<extra_id_0>", "▁", "、", "▁", "、", "▁", "、", "▁", "、", "/", "、", "<extra_id_0>", "▁", "堅", "実", "な", "▁", "けん", "じ", "つ", "な", "▁", "堅", "実", "だ", "▁", "堅", "実", "だ", "/", "けん", "じ", "つ", "だ", "<extra_id_0>", "▁", "経営", "▁", "け", "いえ", "い", "▁", "経営", "▁", "経営", "/", "け", "いえ", "い", "<extra_id_0>", "▁", "を", "▁", "を", "▁", "を", "▁", "を", "/", "を", "<extra_id_0>", "▁", "心が"],
+      "permitted_tokens": ["け", "けます"]
+    },
     "is_consecutive": true
   },
   "case019": {
     "text": "子どもが勉強嫌いになるのは、学力にちょうどのことが与えられていないからである。",
-    "input_tokens": ["<pad>", "子どもが", "▁", "こども", "が", "▁", "子ども", "▁子供", "/", "こども", "<br>", "▁", "勉強", "▁", "べ", "ん", "きょう", "▁", "勉強", "▁", "勉強", "/", "べ", "ん", "きょう", "<br>", "▁", "嫌い"],
-    "permitted_tokens": ["に", "になる", "になるのは"],
+    "t5": {
+      "input_tokens": ["<pad>", "子ども", "▁", "こども", "▁", "子ども", "▁", "子供", "/", "こども", "<extra_id_0>", "▁", "が", "▁", "が", "▁", "が", "▁", "が", "/", "が", "<extra_id_0>", "▁", "勉強", "▁", "べん", "きょう", "▁", "勉強", "▁", "勉強", "/", "べん", "きょう", "<extra_id_0>", "▁", "嫌い"],
+      "permitted_tokens": ["に", "になる"]
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "子どもが", "▁", "こども", "が", "▁", "子ども", "▁子供", "/", "こども", "<extra_id_0>", "▁", "勉強", "▁", "べ", "ん", "きょう", "▁", "勉強", "▁", "勉強", "/", "べ", "ん", "きょう", "<extra_id_0>", "▁", "嫌い"],
+      "permitted_tokens": ["に", "になる", "になるのは"]
+    },
     "is_consecutive": true
   },
   "case020": {
     "text": "計算",
-    "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<br>"],
-    "permitted_tokens": [],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": []
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": []
+    },
     "is_consecutive": false
   },
   "case021": {
     "text": "計量",
-    "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<br>"],
-    "permitted_tokens": [],
+    "t5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": []
+    },
+    "mt5": {
+      "input_tokens": ["<pad>", "▁", "計算", "▁", "けい", "さん", "▁", "計算", "▁", "計算", "/", "けい", "さん", "<extra_id_0>"],
+      "permitted_tokens": []
+    },
     "is_consecutive": false
   }
 }
diff --git a/tests/datamodule/datasets/test_base_dataset.py b/tests/datamodule/datasets/test_base_dataset.py
index b5dbae72..d406e14e 100644
--- a/tests/datamodule/datasets/test_base_dataset.py
+++ b/tests/datamodule/datasets/test_base_dataset.py
@@ -32,8 +32,8 @@ def test_split_document(fixture_data_dir: Path, char_tokenizer: PreTrainedTokeni
     dataset = FullAnnotatedDocumentLoaderMixin(path, char_tokenizer, max_seq_length=15, document_split_stride=1)
     assert len(dataset.orig_documents) == 2
     assert [d.doc_id for d in dataset.orig_documents] == ["test-0", "test-1"]
-    assert len(dataset.documents) == 3
-    assert [d.doc_id for d in dataset.documents] == ["test-0-s1i0", "test-0-s1i1", "test-1"]
+    assert len(dataset.doc_id2document) == 3
+    assert [doc_id for doc_id in dataset.doc_id2document] == ["test-0-s1i0", "test-0-s1i1", "test-1"]
 
 
 def test_split_document_overflow(fixture_data_dir: Path, char_tokenizer: PreTrainedTokenizerBase):
@@ -41,5 +41,5 @@ def test_split_document_overflow(fixture_data_dir: Path, char_tokenizer: PreTrai
     dataset = FullAnnotatedDocumentLoaderMixin(path, char_tokenizer, max_seq_length=3, document_split_stride=1)
     assert len(dataset.orig_documents) == 2
     assert [d.doc_id for d in dataset.orig_documents] == ["test-0", "test-1"]
-    assert len(dataset.documents) == 3
-    assert [d.doc_id for d in dataset.documents] == ["test-0-s1i0", "test-0-s1i1", "test-1-s1i0"]
+    assert len(dataset.doc_id2document) == 3
+    assert [doc_id for doc_id in dataset.doc_id2document] == ["test-0-s1i0", "test-0-s1i1", "test-1-s1i0"]
diff --git a/tests/datamodule/datasets/test_char_inference_dataset.py b/tests/datamodule/datasets/test_char_inference_dataset.py
index 468bab12..4b9b37f0 100644
--- a/tests/datamodule/datasets/test_char_inference_dataset.py
+++ b/tests/datamodule/datasets/test_char_inference_dataset.py
@@ -1,21 +1,59 @@
-from omegaconf import ListConfig
+import tempfile
+from pathlib import Path
+from textwrap import dedent
+
 from transformers import PreTrainedTokenizerBase
 
 from kwja.datamodule.datasets import CharInferenceDataset
 
 
 def test_init(char_tokenizer: PreTrainedTokenizerBase):
-    _ = CharInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length=512)
+    _ = CharInferenceDataset(char_tokenizer, max_seq_length=512, document_split_stride=1)
 
 
 def test_len(char_tokenizer: PreTrainedTokenizerBase):
-    dataset = CharInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length=512)
+    senter_text = dedent(
+        """\
+        # S-ID:test-0-0
+        今日は晴れだ
+        # S-ID:test-0-1
+        散歩に行こう
+        # S-ID:test-1-0
+        今日は雨だ
+        # S-ID:test-1-1
+        家でゆっくりしよう
+        """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
+
+    dataset = CharInferenceDataset(
+        char_tokenizer, max_seq_length=512, document_split_stride=1, senter_file=Path(senter_file.name)
+    )
     assert len(dataset) == 2
 
 
 def test_getitem(char_tokenizer: PreTrainedTokenizerBase):
-    max_seq_length = 512
-    dataset = CharInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length)
+    max_seq_length = 64
+    senter_text = dedent(
+        """\
+        # S-ID:test-0-0
+        今日は晴れだ
+        # S-ID:test-0-1
+        散歩に行こう
+        # S-ID:test-1-0
+        今日は雨だ
+        # S-ID:test-1-1
+        家でゆっくりしよう
+        """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
+    dataset = CharInferenceDataset(
+        char_tokenizer, max_seq_length=max_seq_length, document_split_stride=1, senter_file=Path(senter_file.name)
+    )
     for i in range(len(dataset)):
         feature = dataset[i]
         assert feature.example_ids == i
diff --git a/tests/datamodule/datasets/test_senter_dataset.py b/tests/datamodule/datasets/test_senter_dataset.py
new file mode 100644
index 00000000..bfd76e51
--- /dev/null
+++ b/tests/datamodule/datasets/test_senter_dataset.py
@@ -0,0 +1,51 @@
+from pathlib import Path
+
+import torch
+from transformers import PreTrainedTokenizerBase
+
+from kwja.datamodule.datasets import SenterDataset
+from kwja.utils.constants import IGNORE_INDEX, SENT_SEGMENTATION_TAGS
+
+
+def test_init(fixture_data_dir: Path, char_tokenizer: PreTrainedTokenizerBase):
+    path = fixture_data_dir / "datasets" / "char_files"
+    _ = SenterDataset(str(path), char_tokenizer, max_seq_length=512)
+
+
+def test_getitem(fixture_data_dir: Path, char_tokenizer: PreTrainedTokenizerBase):
+    path = fixture_data_dir / "datasets" / "char_files"
+    max_seq_length: int = 512
+    dataset = SenterDataset(str(path), char_tokenizer, max_seq_length)
+    for i in range(len(dataset)):
+        feature = dataset[i]
+        assert feature.example_ids == i
+        assert len(feature.input_ids) == max_seq_length
+        assert len(feature.attention_mask) == max_seq_length
+        assert len(feature.sent_segmentation_labels) == max_seq_length
+
+
+def test_encode(fixture_data_dir: Path, char_tokenizer: PreTrainedTokenizerBase):
+    path = fixture_data_dir / "datasets" / "char_files"
+    max_seq_length = 512
+    dataset = SenterDataset(str(path), char_tokenizer, max_seq_length)
+    num_examples = len(dataset)
+
+    sent_segmentation_labels = torch.full((num_examples, max_seq_length), IGNORE_INDEX, dtype=torch.long)
+    sent_segmentation_labels[0, 1] = SENT_SEGMENTATION_TAGS.index("B")  # 花
+    sent_segmentation_labels[0, 2] = SENT_SEGMENTATION_TAGS.index("I")  # 咲
+    sent_segmentation_labels[0, 3] = SENT_SEGMENTATION_TAGS.index("I")  # ガ
+    sent_segmentation_labels[0, 4] = SENT_SEGMENTATION_TAGS.index("I")  # ニ
+    sent_segmentation_labels[0, 5] = SENT_SEGMENTATION_TAGS.index("I")  # を
+    sent_segmentation_labels[0, 6] = SENT_SEGMENTATION_TAGS.index("I")  # 買
+    sent_segmentation_labels[0, 7] = SENT_SEGMENTATION_TAGS.index("I")  # ぅ
+    sent_segmentation_labels[1, 1] = SENT_SEGMENTATION_TAGS.index("B")  # う
+    sent_segmentation_labels[1, 2] = SENT_SEGMENTATION_TAGS.index("I")  # ま
+    sent_segmentation_labels[1, 3] = SENT_SEGMENTATION_TAGS.index("I")  # そ
+    sent_segmentation_labels[1, 4] = SENT_SEGMENTATION_TAGS.index("I")  # ー
+    sent_segmentation_labels[1, 5] = SENT_SEGMENTATION_TAGS.index("I")  # で
+    sent_segmentation_labels[1, 6] = SENT_SEGMENTATION_TAGS.index("I")  # す
+    sent_segmentation_labels[1, 7] = SENT_SEGMENTATION_TAGS.index("I")  # ね
+    sent_segmentation_labels[1, 8] = SENT_SEGMENTATION_TAGS.index("I")  # 〜
+    sent_segmentation_labels[1, 9] = SENT_SEGMENTATION_TAGS.index("I")  # 〜
+    assert dataset[0].sent_segmentation_labels == sent_segmentation_labels[0].tolist()
+    assert dataset[1].sent_segmentation_labels == sent_segmentation_labels[1].tolist()
diff --git a/tests/datamodule/datasets/test_senter_inference_dataset.py b/tests/datamodule/datasets/test_senter_inference_dataset.py
new file mode 100644
index 00000000..513c07ad
--- /dev/null
+++ b/tests/datamodule/datasets/test_senter_inference_dataset.py
@@ -0,0 +1,23 @@
+from omegaconf import ListConfig
+from transformers import PreTrainedTokenizerBase
+
+from kwja.datamodule.datasets import SenterInferenceDataset
+
+
+def test_init(char_tokenizer: PreTrainedTokenizerBase):
+    _ = SenterInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length=512)
+
+
+def test_len(char_tokenizer: PreTrainedTokenizerBase):
+    dataset = SenterInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length=512)
+    assert len(dataset) == 2
+
+
+def test_getitem(char_tokenizer: PreTrainedTokenizerBase):
+    max_seq_length = 512
+    dataset = SenterInferenceDataset(ListConfig(["テスト", "サンプル"]), char_tokenizer, max_seq_length)
+    for i in range(len(dataset)):
+        feature = dataset[i]
+        assert feature.example_ids == i
+        assert len(feature.input_ids) == max_seq_length
+        assert len(feature.attention_mask) == max_seq_length
diff --git a/tests/datamodule/datasets/test_seq2seq_dataset.py b/tests/datamodule/datasets/test_seq2seq_dataset.py
index 142961df..63b7cc04 100644
--- a/tests/datamodule/datasets/test_seq2seq_dataset.py
+++ b/tests/datamodule/datasets/test_seq2seq_dataset.py
@@ -55,7 +55,7 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "/",
             "た",
             "ろう",
-            "<br>",  # 太郎
+            "<extra_id_0>",  # 太郎
             "▁",
             "と",
             "▁",
@@ -63,8 +63,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "と",
             "▁",
-            "<no_canon>",
-            "<br>",  # と
+            "<extra_id_2>",
+            "<extra_id_0>",  # と
             "▁",
             "次郎",
             "▁",
@@ -77,7 +77,7 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "/",
             "じ",
             "ろう",
-            "<br>",  # 次郎
+            "<extra_id_0>",  # 次郎
             "▁",
             "は",
             "▁",
@@ -85,8 +85,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "は",
             "▁",
-            "<no_canon>",
-            "<br>",  # は
+            "<extra_id_2>",
+            "<extra_id_0>",  # は
             "▁",
             "よく",
             "▁",
@@ -94,8 +94,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "よく",
             "▁",
-            "<no_canon>",
-            "<br>",  # よく
+            "<extra_id_2>",
+            "<extra_id_0>",  # よく
             "▁",
             "けん",
             "か",
@@ -111,7 +111,7 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "/",
             "けん",
             "か",
-            "<br>",  # けんか
+            "<extra_id_0>",  # けんか
             "▁",
             "する",
             "▁",
@@ -119,8 +119,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "する",
             "▁",
-            "<no_canon>",
-            "<br>",  # する
+            "<extra_id_2>",
+            "<extra_id_0>",  # する
             "
",
         ],
         [
@@ -132,8 +132,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "辛い",
             "▁",
-            "<no_canon>",
-            "<br>",  # 辛い
+            "<extra_id_2>",
+            "<extra_id_0>",  # 辛い
             "▁",
             "ラーメン",
             "▁",
@@ -143,8 +143,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "ラーメン",
             "▁",
-            "<no_canon>",
-            "<br>",  # ラーメン
+            "<extra_id_2>",
+            "<extra_id_0>",  # ラーメン
             "▁",
             "が",
             "▁",
@@ -152,8 +152,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "が",
             "▁",
-            "<no_canon>",
-            "<br>",  # が
+            "<extra_id_2>",
+            "<extra_id_0>",  # が
             "▁",
             "好きな",
             "▁",
@@ -163,8 +163,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "好き",
             "だ",
             "▁",
-            "<no_canon>",
-            "<br>",  # 好きな
+            "<extra_id_2>",
+            "<extra_id_0>",  # 好きな
             "▁",
             "ので",
             "▁",
@@ -172,8 +172,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "のだ",
             "▁",
-            "<no_canon>",
-            "<br>",  # ので
+            "<extra_id_2>",
+            "<extra_id_0>",  # ので
             "▁",
             "頼",
             "み",
@@ -183,8 +183,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "頼む",
             "▁",
-            "<no_canon>",
-            "<br>",  # 頼み
+            "<extra_id_2>",
+            "<extra_id_0>",  # 頼み
             "▁",
             "ました",
             "▁",
@@ -192,8 +192,8 @@ def test_encode(fixture_data_dir: Path, seq2seq_tokenizer: PreTrainedTokenizerBa
             "▁",
             "ます",
             "▁",
-            "<no_canon>",
-            "<br>",  # ました
+            "<extra_id_2>",
+            "<extra_id_0>",  # ました
             "
",
         ],
     ]
diff --git a/tests/datamodule/datasets/test_seq2seq_inference_dataset.py b/tests/datamodule/datasets/test_seq2seq_inference_dataset.py
index f0f38479..28cf55ef 100644
--- a/tests/datamodule/datasets/test_seq2seq_inference_dataset.py
+++ b/tests/datamodule/datasets/test_seq2seq_inference_dataset.py
@@ -1,36 +1,67 @@
-from omegaconf import ListConfig
+import tempfile
+from pathlib import Path
+from textwrap import dedent
+
 from transformers import PreTrainedTokenizerBase
 
 from kwja.datamodule.datasets import Seq2SeqInferenceDataset
 
 
 def test_init(seq2seq_tokenizer: PreTrainedTokenizerBase):
-    _ = Seq2SeqInferenceDataset(
-        ListConfig(["テスト", "サンプル"]),
-        seq2seq_tokenizer,
-        max_src_length=128,
-        max_tgt_length=512,
-    )
+    _ = Seq2SeqInferenceDataset(seq2seq_tokenizer, max_src_length=128, max_tgt_length=512)
 
 
 def test_len(seq2seq_tokenizer: PreTrainedTokenizerBase):
+    senter_text = dedent(
+        """\
+        # S-ID:test-0-0
+        今日は晴れだ
+        # S-ID:test-0-1
+        散歩に行こう
+        # S-ID:test-1-0
+        今日は雨だ
+        # S-ID:test-1-1
+        家でゆっくりしよう
+        """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
+
     dataset = Seq2SeqInferenceDataset(
-        ListConfig(["テスト", "サンプル"]),
         seq2seq_tokenizer,
         max_src_length=128,
         max_tgt_length=512,
+        senter_file=Path(senter_file.name),
     )
-    assert len(dataset) == 2
+    assert len(dataset) == 4
 
 
 def test_getitem(seq2seq_tokenizer: PreTrainedTokenizerBase):
-    max_src_length = 128
-    texts = ["テスト", "サンプル"]
+    max_src_length = 64
+    max_tgt_length = 512
+    texts = ["今日は晴れだ", "散歩に行こう", "今日は雨だ", "家でゆっくりしよう"]
+    senter_text = dedent(
+        """\
+        # S-ID:test-0-0
+        今日は晴れだ
+        # S-ID:test-0-1
+        散歩に行こう
+        # S-ID:test-1-0
+        今日は雨だ
+        # S-ID:test-1-1
+        家でゆっくりしよう
+        """
+    )
+    senter_file = tempfile.NamedTemporaryFile("wt")
+    senter_file.write(senter_text)
+    senter_file.seek(0)
+
     dataset = Seq2SeqInferenceDataset(
-        ListConfig(texts),
         seq2seq_tokenizer,
         max_src_length=max_src_length,
-        max_tgt_length=512,
+        max_tgt_length=max_tgt_length,
+        senter_file=Path(senter_file.name),
     )
     for i in range(len(dataset)):
         feature = dataset[i]
diff --git a/tests/datamodule/datasets/test_word_dataset.py b/tests/datamodule/datasets/test_word_dataset.py
index 3543d31d..faf841af 100644
--- a/tests/datamodule/datasets/test_word_dataset.py
+++ b/tests/datamodule/datasets/test_word_dataset.py
@@ -39,16 +39,17 @@ def test_getitem(fixture_data_dir: Path, word_tokenizer: PreTrainedTokenizerBase
     dataset = WordDataset(str(path), word_tokenizer, max_seq_length, document_split_stride=1, **dataset_kwargs)
     num_cohesion_rels = len([r for utils in dataset.cohesion_task2utils.values() for r in utils.rels])
     for i in range(len(dataset)):
-        document = dataset.documents[i]
+        doc_id = dataset.examples[i].doc_id
+        assert doc_id is not None, "doc_id isn't set"
+        document = dataset.doc_id2document[doc_id]
         feature = dataset[i]
         assert feature.example_ids == i
         assert len(feature.input_ids) == max_seq_length
         assert len(feature.attention_mask) == max_seq_length
-        assert len(feature.target_mask) == max_seq_length
         assert np.array(feature.subword_map).shape == (max_seq_length, max_seq_length)
-        assert (np.array(feature.subword_map).sum(axis=1) != 0).sum() == len(
-            document.morphemes
-        ) + dataset.num_special_tokens
+        assert (np.array(feature.subword_map).sum(axis=1) != 0).sum() == len(document.morphemes) + len(
+            dataset.special_tokens
+        )
         assert len(feature.reading_labels) == max_seq_length
         assert np.array(feature.reading_subword_map).shape == (max_seq_length, max_seq_length)
         assert (np.array(feature.reading_subword_map).sum(axis=1) != 0).sum() == len(document.morphemes)
@@ -58,6 +59,7 @@ def test_getitem(fixture_data_dir: Path, word_tokenizer: PreTrainedTokenizerBase
         assert len(feature.conjform_labels) == max_seq_length
         assert np.array(feature.word_feature_labels).shape == (max_seq_length, len(WORD_FEATURES))
         assert len(feature.ne_labels) == max_seq_length
+        assert len(feature.ne_mask) == max_seq_length
         assert np.array(feature.base_phrase_feature_labels).shape == (max_seq_length, len(BASE_PHRASE_FEATURES))
         assert len(feature.dependency_labels) == max_seq_length
         assert np.array(feature.dependency_mask).shape == (max_seq_length, max_seq_length)
@@ -227,7 +229,7 @@ def test_encode(fixture_data_dir: Path, word_tokenizer: PreTrainedTokenizerBase,
     assert word_feature_labels[0].tolist() == dataset[0].word_feature_labels
     assert word_feature_labels[1].tolist() == dataset[1].word_feature_labels
 
-    ne_labels = torch.full((num_examples, max_seq_length), IGNORE_INDEX, dtype=torch.long)
+    ne_labels = torch.full((num_examples, max_seq_length), NE_TAGS.index("O"), dtype=torch.long)
     ne_labels[0, 0] = NE_TAGS.index("B-PERSON")  # 太郎
     ne_labels[0, 1] = NE_TAGS.index("O")  # と
     ne_labels[0, 2] = NE_TAGS.index("B-PERSON")  # 次郎
@@ -284,14 +286,16 @@ def test_encode(fixture_data_dir: Path, word_tokenizer: PreTrainedTokenizerBase,
     dependency_labels[0, 2] = 5  # 次郎 -> けんか
     dependency_labels[0, 3] = 2  # は -> 次郎
     dependency_labels[0, 4] = 5  # よく -> けんか
-    dependency_labels[0, 5] = dataset.special_token2index["[ROOT]"]  # けんか -> [ROOT]
+    # けんか -> [ROOT]
+    dependency_labels[0, 5] = dataset.examples[0].special_token_indexer.get_morpheme_level_index("[ROOT]")
     dependency_labels[0, 6] = 5  # する -> けんか
     dependency_labels[1, 0] = 1  # 辛い -> ラーメン
     dependency_labels[1, 1] = 3  # ラーメン -> 好きな
     dependency_labels[1, 2] = 1  # が -> ラーメン
     dependency_labels[1, 3] = 5  # 好きな -> 頼み
     dependency_labels[1, 4] = 3  # ので -> 好きな
-    dependency_labels[1, 5] = dataset.special_token2index["[ROOT]"]  # 頼み -> [ROOT]
+    # 頼み -> [ROOT]
+    dependency_labels[1, 5] = dataset.examples[1].special_token_indexer.get_morpheme_level_index("[ROOT]")
     dependency_labels[1, 6] = 5  # ました -> 頼み
     assert dependency_labels[0].tolist() == dataset[0].dependency_labels
     assert dependency_labels[1].tolist() == dataset[1].dependency_labels
@@ -316,28 +320,30 @@ def test_encode(fixture_data_dir: Path, word_tokenizer: PreTrainedTokenizerBase,
 
     flatten_rels = [r for cohesion_utils in dataset.cohesion_task2utils.values() for r in cohesion_utils.rels]
     cohesion_labels = torch.zeros((num_examples, len(flatten_rels), max_seq_length, max_seq_length), dtype=torch.long)
-    cohesion_labels[0, flatten_rels.index("ノ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ノ 太郎
-    cohesion_labels[0, flatten_rels.index("="), 0, dataset.special_token2index["[NA]"]] = 1  # 太郎
-    cohesion_labels[0, flatten_rels.index("ノ"), 2, dataset.special_token2index["[NULL]"]] = 1  # φ ノ 次郎
-    cohesion_labels[0, flatten_rels.index("="), 2, dataset.special_token2index["[NA]"]] = 1  # 次郎
+    _alias = dataset.examples[0].special_token_indexer.get_morpheme_level_index
+    cohesion_labels[0, flatten_rels.index("ノ"), 0, _alias("[NULL]")] = 1  # φ ノ 太郎
+    cohesion_labels[0, flatten_rels.index("="), 0, _alias("[NA]")] = 1  # 太郎
+    cohesion_labels[0, flatten_rels.index("ノ"), 2, _alias("[NULL]")] = 1  # φ ノ 次郎
+    cohesion_labels[0, flatten_rels.index("="), 2, _alias("[NA]")] = 1  # 次郎
     cohesion_labels[0, flatten_rels.index("ガ"), 5, 2] = 1  # 次郎 ガ けんか
-    cohesion_labels[0, flatten_rels.index("ヲ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ けんか
-    cohesion_labels[0, flatten_rels.index("ニ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ニ けんか
-    cohesion_labels[0, flatten_rels.index("ガ２"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ けんか
+    cohesion_labels[0, flatten_rels.index("ヲ"), 5, _alias("[NULL]")] = 1  # φ ヲ けんか
+    cohesion_labels[0, flatten_rels.index("ニ"), 5, _alias("[NULL]")] = 1  # φ ニ けんか
+    cohesion_labels[0, flatten_rels.index("ガ２"), 5, _alias("[NULL]")] = 1  # φ ガ２ けんか
+    _alias = dataset.examples[1].special_token_indexer.get_morpheme_level_index
     cohesion_labels[1, flatten_rels.index("ガ"), 0, 1] = 1  # ラーメン ガ 辛い
-    cohesion_labels[1, flatten_rels.index("ヲ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ 辛い
-    cohesion_labels[1, flatten_rels.index("ニ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 辛い
-    cohesion_labels[1, flatten_rels.index("ガ２"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ 辛い
-    cohesion_labels[1, flatten_rels.index("ノ"), 1, dataset.special_token2index["[NULL]"]] = 1  # φ ノ ラーメン
-    cohesion_labels[1, flatten_rels.index("="), 1, dataset.special_token2index["[NA]"]] = 1  # ラーメン
+    cohesion_labels[1, flatten_rels.index("ヲ"), 0, _alias("[NULL]")] = 1  # φ ヲ 辛い
+    cohesion_labels[1, flatten_rels.index("ニ"), 0, _alias("[NULL]")] = 1  # φ ニ 辛い
+    cohesion_labels[1, flatten_rels.index("ガ２"), 0, _alias("[NULL]")] = 1  # φ ガ２ 辛い
+    cohesion_labels[1, flatten_rels.index("ノ"), 1, _alias("[NULL]")] = 1  # φ ノ ラーメン
+    cohesion_labels[1, flatten_rels.index("="), 1, _alias("[NA]")] = 1  # ラーメン
     cohesion_labels[1, flatten_rels.index("ガ"), 3, 1] = 1  # ラーメン ガ 好き
-    cohesion_labels[1, flatten_rels.index("ヲ"), 3, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ 好き
-    cohesion_labels[1, flatten_rels.index("ニ"), 3, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 好き
-    cohesion_labels[1, flatten_rels.index("ガ２"), 3, dataset.special_token2index["著者"]] = 1  # 著者 ガ２ 好き
-    cohesion_labels[1, flatten_rels.index("ガ"), 5, dataset.special_token2index["著者"]] = 1  # 著者 ガ 頼み
+    cohesion_labels[1, flatten_rels.index("ヲ"), 3, _alias("[NULL]")] = 1  # φ ヲ 好き
+    cohesion_labels[1, flatten_rels.index("ニ"), 3, _alias("[NULL]")] = 1  # φ ニ 好き
+    cohesion_labels[1, flatten_rels.index("ガ２"), 3, _alias("[著者]")] = 1  # 著者 ガ２ 好き
+    cohesion_labels[1, flatten_rels.index("ガ"), 5, _alias("[著者]")] = 1  # 著者 ガ 頼み
     cohesion_labels[1, flatten_rels.index("ヲ"), 5, 1] = 1  # ラーメン ヲ 頼み
-    cohesion_labels[1, flatten_rels.index("ニ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 頼み
-    cohesion_labels[1, flatten_rels.index("ガ２"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ 頼み
+    cohesion_labels[1, flatten_rels.index("ニ"), 5, _alias("[NULL]")] = 1  # φ ニ 頼み
+    cohesion_labels[1, flatten_rels.index("ガ２"), 5, _alias("[NULL]")] = 1  # φ ガ２ 頼み
     assert cohesion_labels[0].tolist() == dataset[0].cohesion_labels
     assert cohesion_labels[1].tolist() == dataset[1].cohesion_labels
 
@@ -516,7 +522,7 @@ def test_split_into_words_encode(
     assert word_feature_labels[0].tolist() == dataset[0].word_feature_labels
     assert word_feature_labels[1].tolist() == dataset[1].word_feature_labels
 
-    ne_labels = torch.full((num_examples, max_seq_length), IGNORE_INDEX, dtype=torch.long)
+    ne_labels = torch.full((num_examples, max_seq_length), NE_TAGS.index("O"), dtype=torch.long)
     ne_labels[0, 0] = NE_TAGS.index("B-PERSON")  # 太郎
     ne_labels[0, 1] = NE_TAGS.index("O")  # と
     ne_labels[0, 2] = NE_TAGS.index("B-PERSON")  # 次郎
@@ -573,14 +579,16 @@ def test_split_into_words_encode(
     dependency_labels[0, 2] = 5  # 次郎 -> けんか
     dependency_labels[0, 3] = 2  # は -> 次郎
     dependency_labels[0, 4] = 5  # よく -> けんか
-    dependency_labels[0, 5] = dataset.special_token2index["[ROOT]"]  # けんか -> [ROOT]
+    # けんか -> [ROOT]
+    dependency_labels[0, 5] = dataset.examples[0].special_token_indexer.get_morpheme_level_index("[ROOT]")
     dependency_labels[0, 6] = 5  # する -> けんか
     dependency_labels[1, 0] = 1  # 辛い -> ラーメン
     dependency_labels[1, 1] = 3  # ラーメン -> 好きな
     dependency_labels[1, 2] = 1  # が -> ラーメン
     dependency_labels[1, 3] = 5  # 好きな -> 頼み
     dependency_labels[1, 4] = 3  # ので -> 好きな
-    dependency_labels[1, 5] = dataset.special_token2index["[ROOT]"]  # 頼み -> [ROOT]
+    # 頼み -> [ROOT]
+    dependency_labels[1, 5] = dataset.examples[1].special_token_indexer.get_morpheme_level_index("[ROOT]")
     dependency_labels[1, 6] = 5  # ました -> 頼み
     assert dependency_labels[0].tolist() == dataset[0].dependency_labels
     assert dependency_labels[1].tolist() == dataset[1].dependency_labels
@@ -605,28 +613,30 @@ def test_split_into_words_encode(
 
     flatten_rels = [r for cohesion_utils in dataset.cohesion_task2utils.values() for r in cohesion_utils.rels]
     cohesion_labels = torch.zeros((num_examples, len(flatten_rels), max_seq_length, max_seq_length), dtype=torch.long)
-    cohesion_labels[0, flatten_rels.index("ノ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ノ 太郎
-    cohesion_labels[0, flatten_rels.index("="), 0, dataset.special_token2index["[NA]"]] = 1  # 太郎
-    cohesion_labels[0, flatten_rels.index("ノ"), 2, dataset.special_token2index["[NULL]"]] = 1  # φ ノ 次郎
-    cohesion_labels[0, flatten_rels.index("="), 2, dataset.special_token2index["[NA]"]] = 1  # 次郎
+    _alias = dataset.examples[0].special_token_indexer.get_morpheme_level_index
+    cohesion_labels[0, flatten_rels.index("ノ"), 0, _alias("[NULL]")] = 1  # φ ノ 太郎
+    cohesion_labels[0, flatten_rels.index("="), 0, _alias("[NA]")] = 1  # 太郎
+    cohesion_labels[0, flatten_rels.index("ノ"), 2, _alias("[NULL]")] = 1  # φ ノ 次郎
+    cohesion_labels[0, flatten_rels.index("="), 2, _alias("[NA]")] = 1  # 次郎
     cohesion_labels[0, flatten_rels.index("ガ"), 5, 2] = 1  # 次郎 ガ けんか
-    cohesion_labels[0, flatten_rels.index("ヲ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ けんか
-    cohesion_labels[0, flatten_rels.index("ニ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ニ けんか
-    cohesion_labels[0, flatten_rels.index("ガ２"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ けんか
+    cohesion_labels[0, flatten_rels.index("ヲ"), 5, _alias("[NULL]")] = 1  # φ ヲ けんか
+    cohesion_labels[0, flatten_rels.index("ニ"), 5, _alias("[NULL]")] = 1  # φ ニ けんか
+    cohesion_labels[0, flatten_rels.index("ガ２"), 5, _alias("[NULL]")] = 1  # φ ガ２ けんか
+    _alias = dataset.examples[1].special_token_indexer.get_morpheme_level_index
     cohesion_labels[1, flatten_rels.index("ガ"), 0, 1] = 1  # ラーメン ガ 辛い
-    cohesion_labels[1, flatten_rels.index("ヲ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ 辛い
-    cohesion_labels[1, flatten_rels.index("ニ"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 辛い
-    cohesion_labels[1, flatten_rels.index("ガ２"), 0, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ 辛い
-    cohesion_labels[1, flatten_rels.index("ノ"), 1, dataset.special_token2index["[NULL]"]] = 1  # φ ノ ラーメン
-    cohesion_labels[1, flatten_rels.index("="), 1, dataset.special_token2index["[NA]"]] = 1  # ラーメン
+    cohesion_labels[1, flatten_rels.index("ヲ"), 0, _alias("[NULL]")] = 1  # φ ヲ 辛い
+    cohesion_labels[1, flatten_rels.index("ニ"), 0, _alias("[NULL]")] = 1  # φ ニ 辛い
+    cohesion_labels[1, flatten_rels.index("ガ２"), 0, _alias("[NULL]")] = 1  # φ ガ２ 辛い
+    cohesion_labels[1, flatten_rels.index("ノ"), 1, _alias("[NULL]")] = 1  # φ ノ ラーメン
+    cohesion_labels[1, flatten_rels.index("="), 1, _alias("[NA]")] = 1  # ラーメン
     cohesion_labels[1, flatten_rels.index("ガ"), 3, 1] = 1  # ラーメン ガ 好き
-    cohesion_labels[1, flatten_rels.index("ヲ"), 3, dataset.special_token2index["[NULL]"]] = 1  # φ ヲ 好き
-    cohesion_labels[1, flatten_rels.index("ニ"), 3, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 好き
-    cohesion_labels[1, flatten_rels.index("ガ２"), 3, dataset.special_token2index["著者"]] = 1  # 著者 ガ２ 好き
-    cohesion_labels[1, flatten_rels.index("ガ"), 5, dataset.special_token2index["著者"]] = 1  # 著者 ガ 頼み
+    cohesion_labels[1, flatten_rels.index("ヲ"), 3, _alias("[NULL]")] = 1  # φ ヲ 好き
+    cohesion_labels[1, flatten_rels.index("ニ"), 3, _alias("[NULL]")] = 1  # φ ニ 好き
+    cohesion_labels[1, flatten_rels.index("ガ２"), 3, _alias("[著者]")] = 1  # 著者 ガ２ 好き
+    cohesion_labels[1, flatten_rels.index("ガ"), 5, _alias("[著者]")] = 1  # 著者 ガ 頼み
     cohesion_labels[1, flatten_rels.index("ヲ"), 5, 1] = 1  # ラーメン ヲ 頼み
-    cohesion_labels[1, flatten_rels.index("ニ"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ニ 頼み
-    cohesion_labels[1, flatten_rels.index("ガ２"), 5, dataset.special_token2index["[NULL]"]] = 1  # φ ガ２ 頼み
+    cohesion_labels[1, flatten_rels.index("ニ"), 5, _alias("[NULL]")] = 1  # φ ニ 頼み
+    cohesion_labels[1, flatten_rels.index("ガ２"), 5, _alias("[NULL]")] = 1  # φ ガ２ 頼み
     assert cohesion_labels[0].tolist() == dataset[0].cohesion_labels
     assert cohesion_labels[1].tolist() == dataset[1].cohesion_labels
 
diff --git a/tests/datamodule/datasets/test_word_inference_dataset.py b/tests/datamodule/datasets/test_word_inference_dataset.py
index 0cec1e2f..afee3950 100644
--- a/tests/datamodule/datasets/test_word_inference_dataset.py
+++ b/tests/datamodule/datasets/test_word_inference_dataset.py
@@ -90,7 +90,7 @@ def test_getitem(word_tokenizer: PreTrainedTokenizerBase, dataset_kwargs: Dict[s
         assert feature.example_ids == i
         assert len(feature.input_ids) == max_seq_length
         assert len(feature.attention_mask) == max_seq_length
-        assert len(feature.target_mask) == max_seq_length
+        assert len(feature.ne_mask) == max_seq_length
         assert np.array(feature.subword_map).shape == (max_seq_length, max_seq_length)
         assert np.array(feature.reading_subword_map).shape == (max_seq_length, max_seq_length)
         assert np.array(feature.dependency_mask).shape == (max_seq_length, max_seq_length)
diff --git a/tests/metrics/test_senter_module_metric.py b/tests/metrics/test_senter_module_metric.py
new file mode 100644
index 00000000..e287a1f5
--- /dev/null
+++ b/tests/metrics/test_senter_module_metric.py
@@ -0,0 +1,55 @@
+from pathlib import Path
+
+import pytest
+import torch
+from transformers import PreTrainedTokenizerBase
+
+from kwja.datamodule.datasets import SenterDataset
+from kwja.metrics import SenterModuleMetric
+from kwja.utils.constants import IGNORE_INDEX, SENT_SEGMENTATION_TAGS
+
+
+def test_char_module_metric(fixture_data_dir: Path, char_tokenizer: PreTrainedTokenizerBase) -> None:
+    metric = SenterModuleMetric()
+
+    path = fixture_data_dir / "datasets" / "char_files"
+    max_seq_length = 20
+    dataset = SenterDataset(str(path), char_tokenizer, max_seq_length=max_seq_length)
+    metric.set_properties({"dataset": dataset})
+
+    metric.update(
+        {
+            "example_ids": torch.empty(0),  # dummy
+            "sent_segmentation_predictions": torch.empty(0),
+            "word_norm_op_predictions": torch.empty(0),
+            "word_norm_op_labels": torch.empty(0),
+        }
+    )
+
+    num_examples = len(dataset)
+    metric.example_ids = torch.arange(num_examples, dtype=torch.long)
+
+    metric.sent_segmentation_predictions = torch.full((num_examples, max_seq_length), IGNORE_INDEX, dtype=torch.long)
+    # [:, 0] = [CLS]
+    metric.sent_segmentation_predictions[0, 1] = SENT_SEGMENTATION_TAGS.index("B")  # 花
+    metric.sent_segmentation_predictions[0, 2] = SENT_SEGMENTATION_TAGS.index("I")  # 咲
+    metric.sent_segmentation_predictions[0, 3] = SENT_SEGMENTATION_TAGS.index("I")  # ガ
+    metric.sent_segmentation_predictions[0, 4] = SENT_SEGMENTATION_TAGS.index("I")  # ニ
+    metric.sent_segmentation_predictions[0, 5] = SENT_SEGMENTATION_TAGS.index("I")  # を
+    metric.sent_segmentation_predictions[0, 6] = SENT_SEGMENTATION_TAGS.index("I")  # 買
+    metric.sent_segmentation_predictions[0, 7] = SENT_SEGMENTATION_TAGS.index("I")  # ぅ
+    metric.sent_segmentation_predictions[1, 1] = SENT_SEGMENTATION_TAGS.index("B")  # う
+    metric.sent_segmentation_predictions[1, 2] = SENT_SEGMENTATION_TAGS.index("I")  # ま
+    metric.sent_segmentation_predictions[1, 3] = SENT_SEGMENTATION_TAGS.index("I")  # そ
+    metric.sent_segmentation_predictions[1, 4] = SENT_SEGMENTATION_TAGS.index("I")  # ー
+    metric.sent_segmentation_predictions[1, 5] = SENT_SEGMENTATION_TAGS.index("I")  # で
+    metric.sent_segmentation_predictions[1, 6] = SENT_SEGMENTATION_TAGS.index("I")  # す
+    metric.sent_segmentation_predictions[1, 7] = SENT_SEGMENTATION_TAGS.index("I")  # ね
+    metric.sent_segmentation_predictions[1, 8] = SENT_SEGMENTATION_TAGS.index("I")  # 〜
+    metric.sent_segmentation_predictions[1, 9] = SENT_SEGMENTATION_TAGS.index("B")  # 〜 (gold: I)
+
+    metrics = metric.compute()
+
+    assert metrics["sent_segmentation_accuracy"] == pytest.approx(15 / 16)
+    # tp = 1, fp = 2, fn = 2 (span-level)
+    assert metrics["sent_segmentation_f1"] == pytest.approx((2 * 1 / 3 * 1 / 2) / (1 / 3 + 1 / 2))
diff --git a/tests/metrics/test_word_module_metric.py b/tests/metrics/test_word_module_metric.py
index 54f7ef77..4f4a6e6f 100644
--- a/tests/metrics/test_word_module_metric.py
+++ b/tests/metrics/test_word_module_metric.py
@@ -257,20 +257,23 @@ def test_word_module_metric(
 
     dependency_topk = 2
     metric.dependency_predictions = torch.zeros((num_examples, max_seq_length, dependency_topk), dtype=torch.long)
-    r = dataset.special_token2index["[ROOT]"]
     metric.dependency_predictions[0, 0] = torch.as_tensor([2, 5])  # 太郎 -> 次郎, けんか
     metric.dependency_predictions[0, 1] = torch.as_tensor([0, 5])  # と -> 太郎, けんか
     metric.dependency_predictions[0, 2] = torch.as_tensor([5, 6])  # 次郎 -> けんか, する
     metric.dependency_predictions[0, 3] = torch.as_tensor([2, 5])  # は -> 次郎, けんか
     metric.dependency_predictions[0, 4] = torch.as_tensor([5, 6])  # よく -> けんか, する
-    metric.dependency_predictions[0, 5] = torch.as_tensor([r, 6])  # けんか -> [ROOT], する
+    metric.dependency_predictions[0, 5] = torch.as_tensor(
+        [dataset.examples[0].special_token_indexer.get_morpheme_level_index("[ROOT]"), 6]
+    )  # けんか -> [ROOT], する
     metric.dependency_predictions[0, 6] = torch.as_tensor([5, 4])  # する -> けんか, よく
     metric.dependency_predictions[1, 0] = torch.as_tensor([1, 3])  # 辛い -> ラーメン, 好きな
     metric.dependency_predictions[1, 1] = torch.as_tensor([3, 5])  # ラーメン -> 好きな, 頼み
     metric.dependency_predictions[1, 2] = torch.as_tensor([1, 3])  # が -> ラーメン, 好きな
     metric.dependency_predictions[1, 3] = torch.as_tensor([5, 6])  # 好きな -> 頼み, ました
     metric.dependency_predictions[1, 4] = torch.as_tensor([3, 5])  # ので -> 好きな, 頼み
-    metric.dependency_predictions[1, 5] = torch.as_tensor([r, 6])  # 頼み -> [ROOT], ました
+    metric.dependency_predictions[1, 5] = torch.as_tensor(
+        [dataset.examples[1].special_token_indexer.get_morpheme_level_index("[ROOT]"), 6]
+    )  # 頼み -> [ROOT], ました
     metric.dependency_predictions[1, 6] = torch.as_tensor([5, 3])  # ました -> 頼み, 好きな
 
     metric.dependency_type_predictions = torch.zeros((num_examples, max_seq_length, dependency_topk), dtype=torch.long)
@@ -295,14 +298,22 @@ def test_word_module_metric(
     metric.cohesion_logits = torch.zeros(
         (num_examples, len(flatten_rels), max_seq_length, max_seq_length), dtype=torch.float
     )
-    for i, rel in enumerate(flatten_rels):
-        j = dataset.special_token2index["[NA]"] if rel == "=" else dataset.special_token2index["[NULL]"]
-        metric.cohesion_logits[:, i, :, j] = 1.0
+    for i in range(num_examples):
+        for j, rel in enumerate(flatten_rels):
+            if rel == "=":
+                k = dataset.examples[i].special_token_indexer.get_morpheme_level_index("[NA]")
+            else:
+                k = dataset.examples[i].special_token_indexer.get_morpheme_level_index("[NULL]")
+            metric.cohesion_logits[i, j, :, k] = 1.0
     metric.cohesion_logits[0, flatten_rels.index("ガ"), 5, 2] = 2.0  # 次郎 ガ けんか
     metric.cohesion_logits[1, flatten_rels.index("ガ"), 0, 1] = 2.0  # ラーメン ガ 辛い
     metric.cohesion_logits[1, flatten_rels.index("ガ"), 3, 1] = 2.0  # ラーメン ガ 好き
-    metric.cohesion_logits[1, flatten_rels.index("ガ２"), 3, dataset.special_token2index["著者"]] = 2.0  # 著者 ガ２ 好き
-    metric.cohesion_logits[1, flatten_rels.index("ガ"), 5, dataset.special_token2index["著者"]] = 2.0  # 著者 ガ 頼み
+    metric.cohesion_logits[
+        1, flatten_rels.index("ガ２"), 3, dataset.examples[1].special_token_indexer.get_morpheme_level_index("[著者]")
+    ] = 2.0  # 著者 ガ２ 好き
+    metric.cohesion_logits[
+        1, flatten_rels.index("ガ"), 5, dataset.examples[1].special_token_indexer.get_morpheme_level_index("[著者]")
+    ] = 2.0  # 著者 ガ 頼み
     metric.cohesion_logits[1, flatten_rels.index("ヲ"), 5, 1] = 2.0  # ラーメン ヲ 頼み
 
     metric.discourse_predictions = torch.zeros((num_examples, max_seq_length, max_seq_length), dtype=torch.long)
diff --git a/tests/modules/components/test_crf.py b/tests/modules/components/test_crf.py
index f59b1a7b..f76970ce 100644
--- a/tests/modules/components/test_crf.py
+++ b/tests/modules/components/test_crf.py
@@ -53,6 +53,25 @@ def test_forward(batch_size: int, seq_length: int, reduction: str) -> None:
         assert llh.shape == (batch_size,)
 
 
+@pytest.mark.parametrize(
+    "batch_size, seq_length, reduction",
+    [
+        (2, 3, "token_mean"),
+        (2, 3, "mean"),
+        (2, 3, "sum"),
+        (2, 3, "none"),
+    ],
+)
+def test_loss_mask(batch_size: int, seq_length: int, reduction: str) -> None:
+    crf = CRF(NE_TAGS)
+
+    emissions = torch.randn((batch_size, seq_length, crf.num_tags), dtype=torch.float)
+    tags = torch.zeros((batch_size, seq_length), dtype=torch.long)
+    mask = torch.full((batch_size, seq_length), False, dtype=torch.bool)
+    llh = crf(emissions, tags, mask=mask, reduction=reduction)
+    assert llh.sum().item() == 0
+
+
 @pytest.mark.parametrize(
     "batch_size, seq_length, target_span",
     [
@@ -70,8 +89,8 @@ def test_viterbi_decode(batch_size: int, seq_length: int, target_span: Tuple[int
     ne_index = [i for i, tag in enumerate(NE_TAGS) if tag.startswith("B-")][0]
     emissions = torch.zeros((batch_size, seq_length, crf.num_tags), dtype=torch.float)
     emissions[:, :, ne_index] = 1.0
-    mask = torch.zeros((batch_size, seq_length), dtype=torch.long)
-    mask[:, torch.arange(*target_span)] = 1
+    mask = torch.full((batch_size, seq_length), False, dtype=torch.bool)
+    mask[:, torch.arange(*target_span)] = True
     decoded = crf.viterbi_decode(emissions, mask)
     assert decoded.shape == (batch_size, seq_length)
     assert (decoded[:, torch.arange(*target_span)] == ne_index).all()
diff --git a/tests/modules/components/test_head.py b/tests/modules/components/test_head.py
index 6901fff2..c2e5bfa7 100644
--- a/tests/modules/components/test_head.py
+++ b/tests/modules/components/test_head.py
@@ -1,34 +1,71 @@
 import pytest
 import torch
 
-from kwja.modules.components.head import SequenceLabelingHead, WordSelectionHead
+from kwja.modules.components.head import (
+    LoRASequenceMultiLabelingHead,
+    RelationWiseWordSelectionHead,
+    SequenceLabelingHead,
+    WordSelectionHead,
+)
+
+
+@pytest.mark.parametrize(
+    "num_labels, hidden_size, hidden_dropout_prob",
+    [
+        (2, 3, 0.0),
+        (10, 2, 0.1),
+    ],
+)
+def test_sequential_labeling_head(num_labels: int, hidden_size: int, hidden_dropout_prob: float) -> None:
+    head = SequenceLabelingHead(num_labels, hidden_size, hidden_dropout_prob)
+    batch_size, seq_length = 2, 5
+    input_ = torch.ones(batch_size, seq_length, hidden_size)
+    output = head(input_)
+    assert output.size() == (batch_size, seq_length, num_labels)
 
 
 @pytest.mark.parametrize(
-    "num_labels, hidden_size, hidden_dropout_prob, multi_label",
+    "num_labels, hidden_size, hidden_dropout_prob, rank",
     [
-        (2, 3, 0.0, False),
-        (2, 3, 0.0, True),
+        (2, 3, 0.0, 4),
+        (10, 2, 0.1, 2),
     ],
 )
-def test_sequential_labeling_head(
-    num_labels: int,
-    hidden_size: int,
-    hidden_dropout_prob: float,
-    multi_label: bool,
+def test_lora_sequential_multi_labeling_head(
+    num_labels: int, hidden_size: int, hidden_dropout_prob: float, rank: bool
 ) -> None:
-    head = SequenceLabelingHead(num_labels, hidden_size, hidden_dropout_prob, multi_label)
-    batch_size = 2
-    inp = torch.ones(batch_size, hidden_size)
-    out = head(inp)
-    assert out.size() == (batch_size, num_labels)
-
-
-@pytest.mark.parametrize("num_relations, hidden_size, hidden_dropout_prob", [(2, 3, 0.0)])
-def test_word_selection_head(num_relations: int, hidden_size: int, hidden_dropout_prob: float):
-    head = WordSelectionHead(num_relations, hidden_size, hidden_dropout_prob)
-    batch_size = 2
-    seq_length = 5
-    inp = torch.ones(batch_size, seq_length, hidden_size)
-    out = head(inp)
-    assert out.size() == (batch_size, seq_length, seq_length, num_relations)
+    head = LoRASequenceMultiLabelingHead(num_labels, hidden_size, hidden_dropout_prob, rank=rank)
+    batch_size, seq_length = 2, 5
+    input_ = torch.ones(batch_size, seq_length, hidden_size)
+    output = head(input_)
+    assert output.size() == (batch_size, seq_length, num_labels)
+
+
+@pytest.mark.parametrize(
+    "num_labels, hidden_size, hidden_dropout_prob",
+    [
+        (5, 3, 0.0),
+        (1, 2, 0.1),
+    ],
+)
+def test_word_selection_head(num_labels: int, hidden_size: int, hidden_dropout_prob: float):
+    head = WordSelectionHead(num_labels, hidden_size, hidden_dropout_prob)
+    batch_size, seq_length = 2, 5
+    input_ = torch.ones(batch_size, seq_length, hidden_size)
+    output = head(input_)
+    assert output.size() == (batch_size, seq_length, seq_length, num_labels)
+
+
+@pytest.mark.parametrize(
+    "num_labels, hidden_size, hidden_dropout_prob",
+    [
+        (5, 3, 0.0),
+        (1, 2, 0.1),
+    ],
+)
+def test_relation_wise_word_selection_head(num_labels: int, hidden_size: int, hidden_dropout_prob: float):
+    head = RelationWiseWordSelectionHead(num_labels, hidden_size, hidden_dropout_prob)
+    batch_size, seq_length = 2, 5
+    input_ = torch.ones(batch_size, seq_length, hidden_size)
+    output = head(input_)
+    assert output.size() == (batch_size, seq_length, seq_length, num_labels)
diff --git a/tests/modules/components/test_logits_processor.py b/tests/modules/components/test_logits_processor.py
index 7b97eb7c..44dc8de8 100644
--- a/tests/modules/components/test_logits_processor.py
+++ b/tests/modules/components/test_logits_processor.py
@@ -1,6 +1,6 @@
 import json
 from pathlib import Path
-from typing import List, Set
+from typing import Dict, List, Set
 
 import pytest
 import torch
@@ -9,20 +9,20 @@
 
 from kwja.datamodule.datasets.seq2seq import get_seq2seq_format
 from kwja.modules.components.logits_processor import ForcedSurfLogitsProcessor, get_char2tokens
-from kwja.utils.constants import NEW_LINE_TOKEN
+from kwja.utils.constants import FULL_SPACE_TOKEN, NEW_LINE_TOKEN
+
+SPECIAL_TOKENS: List[str] = [f"<extra_id_{idx}>" for idx in range(100)]
 
 
 def test_get_char2tokens():
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
+    mt5_tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+        pretrained_model_name_or_path="google/mt5-small",
+        additional_special_tokens=SPECIAL_TOKENS,
     )
-
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
-
-    assert len(char2tokens) == 19455
-    assert len(char2underscore_tokens) == 1665
-
-    assert char2tokens["京"] == {
+    mt5_char2tokens, mt5_char2underscore_tokens = get_char2tokens(mt5_tokenizer)
+    assert len(mt5_char2tokens) == 19455
+    assert len(mt5_char2underscore_tokens) == 1665
+    assert mt5_char2tokens["京"] == {
         "京东": 165392,
         "京娱乐": 178804,
         "京都府": 166766,
@@ -31,134 +31,187 @@ def test_get_char2tokens():
         "京都": 51389,
         "京区": 208641,
     }
-    assert char2underscore_tokens["京"] == {"▁京公网安备": 234066}
+    assert mt5_char2underscore_tokens["京"] == {"▁京公网安备": 234066}
+
+    t5_tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+        pretrained_model_name_or_path="retrieva-jp/t5-small-long",
+        additional_special_tokens=SPECIAL_TOKENS,
+    )
+    t5_char2tokens, t5_char2underscore_tokens = get_char2tokens(t5_tokenizer)
+    assert len(t5_char2tokens) == 4289
+    assert len(t5_char2underscore_tokens) == 150
+    assert t5_char2tokens["京"] == {
+        "京都府": 3411,
+        "京都府出身": 26029,
+        "京橋": 22889,
+        "京急": 26569,
+        "京都府京都市": 22480,
+        "京都": 743,
+        "京成": 13372,
+        "京浜": 18564,
+        "京都帝国大学": 20474,
+        "京セラ": 29651,
+        "京都大学": 7089,
+        "京都府立": 27876,
+        "京畿道": 23298,
+        "京王": 14545,
+        "京極": 21867,
+        "京都市": 8841,
+        "京": 1351,
+        "京阪": 14311,
+        "京都市立": 24756,
+    }
+    assert t5_char2underscore_tokens["京"] == {}
 
 
 def test_get_generated_surfs(fixture_data_dir: Path) -> None:
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
-    )
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
-
-    test_case_dir: Path = fixture_data_dir / "modules" / "juman"
-    for path in test_case_dir.glob("*.juman"):
-        with path.open() as f:
-            sentence: Sentence = Sentence.from_jumanpp(f.read())
-            processor = ForcedSurfLogitsProcessor(
-                texts=[sentence.text],
-                tokenizer=tokenizer,
-                char2tokens=char2tokens,
-                char2underscore_tokens=char2underscore_tokens,
-            )
-            tgt_encoding: BatchEncoding = tokenizer(
-                get_seq2seq_format(sentence).replace("\n", NEW_LINE_TOKEN),
-                truncation=False,
-                max_length=512,
-                return_tensors="pt",
-            )
-            assert processor.texts[0] == processor.get_generated_surfs(tgt_encoding.input_ids)[0].replace("
", "")
+    for pretrained_model_name_or_path in ["google/mt5-small", "retrieva-jp/t5-small-long"]:
+        tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            pretrained_model_name_or_path=pretrained_model_name_or_path,
+            additional_special_tokens=SPECIAL_TOKENS,
+        )
+        char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
+
+        test_case_dir: Path = fixture_data_dir / "modules" / "juman"
+        for path in test_case_dir.glob("*.juman"):
+            with path.open() as f:
+                sentence: Sentence = Sentence.from_jumanpp(f.read())
+                processor = ForcedSurfLogitsProcessor(
+                    texts=[sentence.text.strip().replace("\u3000", FULL_SPACE_TOKEN)],
+                    tokenizer=tokenizer,
+                    char2tokens=char2tokens,
+                    char2underscore_tokens=char2underscore_tokens,
+                )
+                tgt_encoding: BatchEncoding = tokenizer(
+                    get_seq2seq_format(sentence).replace("\n", NEW_LINE_TOKEN),
+                    truncation=False,
+                    max_length=512,
+                    return_tensors="pt",
+                )
+                assert processor.texts[0] == processor.get_generated_surfs(tgt_encoding.input_ids)[0].replace(
+                    "
", ""
+                )
 
 
 @pytest.mark.parametrize("input_text, permitted_tokens", [("研究をする", ["研究", "研"])])
 def test_get_permitted_token_ids(input_text: str, permitted_tokens: List[str]) -> None:
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
-    )
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
+    for pretrained_model_name_or_path in ["google/mt5-small", "retrieva-jp/t5-small-long"]:
+        tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            pretrained_model_name_or_path=pretrained_model_name_or_path,
+            additional_special_tokens=SPECIAL_TOKENS,
+        )
+        char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
 
-    processor = ForcedSurfLogitsProcessor(
-        texts=[input_text],
-        tokenizer=tokenizer,
-        char2tokens=char2tokens,
-        char2underscore_tokens=char2underscore_tokens,
-    )
+        processor = ForcedSurfLogitsProcessor(
+            texts=[input_text],
+            tokenizer=tokenizer,
+            char2tokens=char2tokens,
+            char2underscore_tokens=char2underscore_tokens,
+        )
 
-    permitted_token_ids: Set[int] = processor.get_permitted_token_ids(input_text)
-    sorted_permitted_token_ids: List[int] = sorted(list(permitted_token_ids))
-    assert permitted_tokens == tokenizer.convert_ids_to_tokens(sorted_permitted_token_ids)
+        permitted_token_ids: Set[int] = processor.get_permitted_token_ids(input_text)
+        sorted_permitted_token_ids: List[int] = sorted(list(permitted_token_ids))
+        assert permitted_tokens == tokenizer.convert_ids_to_tokens(sorted_permitted_token_ids)
 
 
-@pytest.mark.parametrize("input_text, permitted_underscore_tokens", [("楽天市場", ["▁楽天"])])
+@pytest.mark.parametrize("input_text, permitted_underscore_tokens", [("新古品", ["▁新"])])
 def test_get_permitted_underscore_token_ids(input_text: str, permitted_underscore_tokens: List[str]) -> None:
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
-    )
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
+    for pretrained_model_name_or_path in ["google/mt5-small", "retrieva-jp/t5-small-long"]:
+        tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            pretrained_model_name_or_path=pretrained_model_name_or_path,
+            additional_special_tokens=SPECIAL_TOKENS,
+        )
+        char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
 
-    processor = ForcedSurfLogitsProcessor(
-        texts=[input_text],
-        tokenizer=tokenizer,
-        char2tokens=char2tokens,
-        char2underscore_tokens=char2underscore_tokens,
-    )
+        processor = ForcedSurfLogitsProcessor(
+            texts=[input_text],
+            tokenizer=tokenizer,
+            char2tokens=char2tokens,
+            char2underscore_tokens=char2underscore_tokens,
+        )
 
-    permitted_underscore_token_ids: Set[int] = processor.get_permitted_underscore_token_ids(input_text)
-    sorted_permitted_underscore_token_ids: List[int] = sorted(list(permitted_underscore_token_ids))
-    assert permitted_underscore_tokens == tokenizer.convert_ids_to_tokens(sorted_permitted_underscore_token_ids)
+        permitted_underscore_token_ids: Set[int] = processor.get_permitted_underscore_token_ids(input_text)
+        sorted_permitted_underscore_token_ids: List[int] = sorted(list(permitted_underscore_token_ids))
+        assert permitted_underscore_tokens == tokenizer.convert_ids_to_tokens(sorted_permitted_underscore_token_ids)
 
 
 @pytest.mark.parametrize("input_text, permitted_consecutive_tokens", [("研究をする", ["研究", "研"])])
 def test_get_permitted_consecutive_token_ids(input_text: str, permitted_consecutive_tokens: List[str]) -> None:
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
-    )
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
-
-    processor = ForcedSurfLogitsProcessor(
-        texts=[input_text],
-        tokenizer=tokenizer,
-        char2tokens=char2tokens,
-        char2underscore_tokens=char2underscore_tokens,
-    )
-
-    permitted_consecutive_token_ids: Set[int] = processor.get_permitted_consecutive_token_ids(input_text)
-    sorted_permitted_consecutive_token_ids: List[int] = sorted(list(permitted_consecutive_token_ids))
-    permitted_tokens: List[str] = tokenizer.convert_ids_to_tokens(sorted_permitted_consecutive_token_ids)
-    permitted_tokens_with_underscore: List[str] = []
-    permitted_tokens_without_underscore: List[str] = []
-    for token in permitted_tokens:
-        if token.startswith("▁"):
-            permitted_tokens_with_underscore.append(token)
-        else:
-            permitted_tokens_without_underscore.append(token)
-    assert len(permitted_tokens_with_underscore) == 56369
-    assert permitted_tokens_without_underscore == permitted_consecutive_tokens
-
-
-def test_get_batch_banned_token_ids(fixture_data_dir: Path):
-    tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
-        pretrained_model_name_or_path="google/mt5-small", additional_special_tokens=["<br>", "<no_read>", "<no_canon>"]
-    )
-    char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
-
-    underscore_tokens: List[str] = [x for x in tokenizer.get_vocab() if x.startswith("▁")]
-    assert len(underscore_tokens) == 56369
+    for pretrained_model_name_or_path in ["google/mt5-small", "retrieva-jp/t5-small-long"]:
+        tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            pretrained_model_name_or_path=pretrained_model_name_or_path,
+            additional_special_tokens=SPECIAL_TOKENS,
+        )
+        char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
 
-    test_case_path: Path = fixture_data_dir / "modules" / "permitted_tokens.json"
-    with open(test_case_path) as f:
-        test_cases = json.load(f)
-    for test_id, test_case in test_cases.items():
-        surf_logits_processor = ForcedSurfLogitsProcessor(
-            texts=[test_case["text"]],
+        processor = ForcedSurfLogitsProcessor(
+            texts=[input_text],
             tokenizer=tokenizer,
             char2tokens=char2tokens,
             char2underscore_tokens=char2underscore_tokens,
         )
-        input_ids: torch.LongTensor = torch.LongTensor([tokenizer.convert_tokens_to_ids(test_case["input_tokens"])])
-        orig_scores: torch.Tensor = torch.full((1, tokenizer.vocab_size), 0.5).float()
-        warped_scores: torch.Tensor = surf_logits_processor(
-            input_ids=input_ids,
-            scores=orig_scores,
+
+        permitted_consecutive_token_ids: Set[int] = processor.get_permitted_consecutive_token_ids(input_text)
+        sorted_permitted_consecutive_token_ids: List[int] = sorted(list(permitted_consecutive_token_ids))
+        permitted_tokens: List[str] = tokenizer.convert_ids_to_tokens(sorted_permitted_consecutive_token_ids)
+        permitted_tokens_with_underscore: List[str] = []
+        permitted_tokens_without_underscore: List[str] = []
+        for token in permitted_tokens:
+            if token.startswith("▁"):
+                permitted_tokens_with_underscore.append(token)
+            else:
+                permitted_tokens_without_underscore.append(token)
+        assert permitted_tokens_without_underscore == permitted_consecutive_tokens
+
+
+def test_get_batch_banned_token_ids(fixture_data_dir: Path):
+    model2pretrained_model_name_or_path: Dict[str, str] = {
+        "mt5": "google/mt5-small",
+        "t5": "retrieva-jp/t5-small-long",
+    }
+    for model, pretrained_model_name_or_path in model2pretrained_model_name_or_path.items():
+        tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(
+            pretrained_model_name_or_path=pretrained_model_name_or_path,
+            additional_special_tokens=SPECIAL_TOKENS,
         )
-        assert warped_scores.shape == orig_scores.shape
-        permitted_tokens: List[str] = []
-        for token_id, score in enumerate(warped_scores.tolist()[0]):
-            if score == 0.5:
-                permitted_tokens.append(tokenizer.convert_ids_to_tokens(token_id))
-        if len(permitted_tokens) == tokenizer.vocab_size:
-            permitted_tokens = []
-        if test_case["is_consecutive"] is True:
-            gold_permitted_tokens: List[str] = sorted(list(set(test_case["permitted_tokens"] + underscore_tokens)))
+        char2tokens, char2underscore_tokens = get_char2tokens(tokenizer)
+        underscore_tokens: List[str] = [x for x in tokenizer.get_vocab() if x.startswith("▁")]
+        if model == "mt5":
+            assert len(underscore_tokens) == 56369
+        elif model == "t5":
+            assert len(underscore_tokens) == 531
         else:
-            gold_permitted_tokens = sorted(test_case["permitted_tokens"])
-        assert sorted(list(permitted_tokens)) == gold_permitted_tokens
+            raise ValueError(f"model: {model}")
+
+        test_case_path: Path = fixture_data_dir / "modules" / "permitted_tokens.json"
+        with open(test_case_path) as f:
+            test_cases = json.load(f)
+        for test_id, test_case in test_cases.items():
+            surf_logits_processor = ForcedSurfLogitsProcessor(
+                texts=[test_case["text"]],
+                tokenizer=tokenizer,
+                char2tokens=char2tokens,
+                char2underscore_tokens=char2underscore_tokens,
+            )
+            input_ids: torch.LongTensor = torch.LongTensor(
+                [tokenizer.convert_tokens_to_ids(test_case[model]["input_tokens"])]
+            )
+            orig_scores: torch.Tensor = torch.full((1, tokenizer.vocab_size), 0.5).float()
+            warped_scores: torch.Tensor = surf_logits_processor(
+                input_ids=input_ids,
+                scores=orig_scores,
+            )
+            assert warped_scores.shape == orig_scores.shape
+            permitted_tokens: List[str] = []
+            for token_id, score in enumerate(warped_scores.tolist()[0]):
+                if score == 0.5:
+                    permitted_tokens.append(tokenizer.convert_ids_to_tokens(token_id))
+            if len(permitted_tokens) == tokenizer.vocab_size:
+                permitted_tokens = []
+            if test_case["is_consecutive"] is True:
+                gold_permitted_tokens: List[str] = sorted(
+                    list(set(test_case[model]["permitted_tokens"] + underscore_tokens))
+                )
+            else:
+                gold_permitted_tokens = sorted(test_case[model]["permitted_tokens"])
+            assert sorted(list(permitted_tokens)) == gold_permitted_tokens
diff --git a/tests/modules/functions/test_loss.py b/tests/modules/functions/test_loss.py
index 9ca52387..cba9ed2f 100644
--- a/tests/modules/functions/test_loss.py
+++ b/tests/modules/functions/test_loss.py
@@ -20,12 +20,11 @@ def test_compute_token_mean_loss(input_: torch.Tensor, target: torch.Tensor, exp
 
 
 @pytest.mark.parametrize(
-    "input_, target, mask, expected",
+    "input_, target, expected",
     [
         (
             torch.tensor([[[0.5, 0.5], [1.0, 0.5]]], dtype=torch.float),  # (1, 2, 2)
             torch.tensor([[[1.0, 0.0], [1.0, 0.0]]], dtype=torch.float),  # (1, 2, 2)
-            torch.tensor([[[True, True], [True, True]]], dtype=torch.bool),  # (1, 2, 2)
             torch.tensor(1.0397207708399179, dtype=torch.float),
         ),
     ],
@@ -33,10 +32,9 @@ def test_compute_token_mean_loss(input_: torch.Tensor, target: torch.Tensor, exp
 def test_compute_multi_label_token_mean_loss(
     input_: torch.Tensor,
     target: torch.Tensor,
-    mask: torch.Tensor,
     expected: torch.Tensor,
 ) -> None:
-    assert torch.isclose(compute_multi_label_token_mean_loss(input_, target, mask), expected)
+    assert torch.isclose(compute_multi_label_token_mean_loss(input_, target), expected)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/modules/test_char_module.py b/tests/modules/test_char_module.py
index 21d02bf5..691efe3b 100644
--- a/tests/modules/test_char_module.py
+++ b/tests/modules/test_char_module.py
@@ -1,12 +1,13 @@
 from pathlib import Path
 
 import hydra
+import pytorch_lightning as pl
 from hydra import compose, initialize
 from hydra.core.hydra_config import HydraConfig
 from omegaconf import OmegaConf
 from torch.utils.data import DataLoader
 
-from kwja.datamodule.datamodule import dataclass_data_collator
+from kwja.datamodule.datamodule import token_dataclass_data_collator
 from kwja.datamodule.datasets import CharDataset
 from kwja.modules import CharModule
 
@@ -22,9 +23,9 @@ def test_init() -> None:
 
 
 def test_steps(fixture_data_dir: Path) -> None:
-    trainer = hydra.utils.instantiate(
+    trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         enable_checkpointing=False,
         devices=1,
         accelerator="cpu",
@@ -33,12 +34,13 @@ def test_steps(fixture_data_dir: Path) -> None:
     path = fixture_data_dir / "datasets" / "char_files"
     char_tokenizer = hydra.utils.instantiate(cfg.datamodule.train.kyoto.tokenizer)
     dataset = CharDataset(str(path), char_tokenizer, cfg.max_seq_length, denormalize_probability=0.0)
-    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=dataclass_data_collator)
+    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=token_dataclass_data_collator)
+    val_dataloaders = {"dummy": data_loader}
 
     cfg.datamodule.valid = {"dummy": ""}
     cfg.datamodule.test = {"dummy": ""}
     module = CharModule(cfg)
 
-    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=data_loader)
-    trainer.test(model=module, dataloaders=data_loader)
-    trainer.predict(model=module, dataloaders=data_loader)
+    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=val_dataloaders)
+    trainer.test(model=module, dataloaders=val_dataloaders)
+    trainer.predict(model=module, dataloaders=val_dataloaders)
diff --git a/tests/modules/test_senter_module.py b/tests/modules/test_senter_module.py
new file mode 100644
index 00000000..f1ea1a36
--- /dev/null
+++ b/tests/modules/test_senter_module.py
@@ -0,0 +1,46 @@
+from pathlib import Path
+
+import hydra
+import pytorch_lightning as pl
+from hydra import compose, initialize
+from hydra.core.hydra_config import HydraConfig
+from omegaconf import OmegaConf
+from torch.utils.data import DataLoader
+
+from kwja.datamodule.datamodule import token_dataclass_data_collator
+from kwja.datamodule.datasets import SenterDataset
+from kwja.modules import SenterModule
+
+with initialize(version_base=None, config_path="../../configs"):
+    cfg = compose(config_name="senter_module.debug", return_hydra_config=True, overrides=["max_seq_length=32"])
+    HydraConfig.instance().set_config(cfg)
+    OmegaConf.set_readonly(cfg.hydra, False)
+    OmegaConf.resolve(cfg)
+
+
+def test_init() -> None:
+    _ = SenterModule(cfg)
+
+
+def test_steps(fixture_data_dir: Path) -> None:
+    trainer: pl.Trainer = hydra.utils.instantiate(
+        cfg.trainer,
+        logger=False,
+        enable_checkpointing=False,
+        devices=1,
+        accelerator="cpu",
+    )
+
+    path = fixture_data_dir / "datasets" / "char_files"
+    char_tokenizer = hydra.utils.instantiate(cfg.datamodule.train.kyoto.tokenizer)
+    dataset = SenterDataset(str(path), char_tokenizer, cfg.max_seq_length)
+    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=token_dataclass_data_collator)
+    val_dataloaders = {"dummy": data_loader}
+
+    cfg.datamodule.valid = {"dummy": ""}
+    cfg.datamodule.test = {"dummy": ""}
+    module = SenterModule(cfg)
+
+    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=val_dataloaders)
+    trainer.test(model=module, dataloaders=val_dataloaders)
+    trainer.predict(model=module, dataloaders=val_dataloaders)
diff --git a/tests/modules/test_seq2seq_module.py b/tests/modules/test_seq2seq_module.py
index 59f5044d..ed55e90c 100644
--- a/tests/modules/test_seq2seq_module.py
+++ b/tests/modules/test_seq2seq_module.py
@@ -1,12 +1,13 @@
 from pathlib import Path
 
 import hydra
+import pytorch_lightning as pl
 from hydra import compose, initialize
 from hydra.core.hydra_config import HydraConfig
 from omegaconf import OmegaConf
 from torch.utils.data import DataLoader
 
-from kwja.datamodule.datamodule import dataclass_data_collator
+from kwja.datamodule.datamodule import token_dataclass_data_collator
 from kwja.datamodule.datasets import Seq2SeqDataset
 from kwja.modules import Seq2SeqModule
 
@@ -26,9 +27,9 @@ def test_init() -> None:
 
 
 def test_steps(fixture_data_dir: Path) -> None:
-    trainer = hydra.utils.instantiate(
+    trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         enable_checkpointing=False,
         devices=1,
         accelerator="cpu",
@@ -37,12 +38,13 @@ def test_steps(fixture_data_dir: Path) -> None:
     path = fixture_data_dir / "datasets" / "word_files"
     seq2seq_tokenizer = hydra.utils.instantiate(cfg.datamodule.train.kyoto.tokenizer)
     dataset = Seq2SeqDataset(str(path), seq2seq_tokenizer, cfg.max_src_length, cfg.max_tgt_length)
-    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=dataclass_data_collator)
+    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=token_dataclass_data_collator)
+    val_dataloaders = {"dummy": data_loader}
 
     cfg.datamodule.valid = {"dummy": ""}
     cfg.datamodule.test = {"dummy": ""}
     module = Seq2SeqModule(cfg)
 
-    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=data_loader)
-    trainer.test(model=module, dataloaders=data_loader)
-    trainer.predict(model=module, dataloaders=data_loader)
+    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=val_dataloaders)
+    trainer.test(model=module, dataloaders=val_dataloaders)
+    trainer.predict(model=module, dataloaders=val_dataloaders)
diff --git a/tests/modules/test_typo_module.py b/tests/modules/test_typo_module.py
index 926b75aa..39ff67a1 100644
--- a/tests/modules/test_typo_module.py
+++ b/tests/modules/test_typo_module.py
@@ -1,12 +1,13 @@
 from pathlib import Path
 
 import hydra
+import pytorch_lightning as pl
 from hydra import compose, initialize
 from hydra.core.hydra_config import HydraConfig
 from omegaconf import OmegaConf
 from torch.utils.data import DataLoader
 
-from kwja.datamodule.datamodule import dataclass_data_collator
+from kwja.datamodule.datamodule import token_dataclass_data_collator
 from kwja.datamodule.datasets import TypoDataset
 from kwja.modules import TypoModule
 
@@ -22,9 +23,9 @@ def test_init() -> None:
 
 
 def test_run(fixture_data_dir: Path) -> None:
-    trainer = hydra.utils.instantiate(
+    trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         enable_checkpointing=False,
         devices=1,
         accelerator="cpu",
@@ -33,10 +34,11 @@ def test_run(fixture_data_dir: Path) -> None:
     path = fixture_data_dir / "datasets" / "typo_files"
     typo_tokenizer = hydra.utils.instantiate(cfg.datamodule.train.jwtd.tokenizer)
     dataset = TypoDataset(str(path), typo_tokenizer, cfg.max_seq_length)
-    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=dataclass_data_collator)
+    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=token_dataclass_data_collator)
+    val_dataloaders = {"jwtd": data_loader}
 
     module = TypoModule(cfg)
 
-    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=data_loader)
-    trainer.test(model=module, dataloaders=data_loader)
-    trainer.predict(model=module, dataloaders=data_loader)
+    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=val_dataloaders)
+    trainer.test(model=module, dataloaders=val_dataloaders)
+    trainer.predict(model=module, dataloaders=val_dataloaders)
diff --git a/tests/modules/test_word_module.py b/tests/modules/test_word_module.py
index b3419b28..9461ff97 100644
--- a/tests/modules/test_word_module.py
+++ b/tests/modules/test_word_module.py
@@ -1,12 +1,13 @@
 from pathlib import Path
 
 import hydra
+import pytorch_lightning as pl
 from hydra import compose, initialize
 from hydra.core.hydra_config import HydraConfig
 from omegaconf import ListConfig, OmegaConf
 from torch.utils.data import DataLoader
 
-from kwja.datamodule.datamodule import dataclass_data_collator
+from kwja.datamodule.datamodule import word_dataclass_data_collator
 from kwja.datamodule.datasets import WordDataset
 from kwja.modules import WordModule
 
@@ -14,7 +15,6 @@
     cfg = compose(config_name="word_module.debug", return_hydra_config=True, overrides=["max_seq_length=32"])
     HydraConfig.instance().set_config(cfg)
     OmegaConf.set_readonly(cfg.hydra, False)
-    OmegaConf.register_new_resolver("concat", lambda x, y: x + y, replace=True)
     OmegaConf.resolve(cfg)
 
 
@@ -23,9 +23,9 @@ def test_init() -> None:
 
 
 def test_steps(fixture_data_dir: Path) -> None:
-    trainer = hydra.utils.instantiate(
+    trainer: pl.Trainer = hydra.utils.instantiate(
         cfg.trainer,
-        logger=None,
+        logger=False,
         enable_checkpointing=False,
         devices=1,
         accelerator="cpu",
@@ -45,12 +45,13 @@ def test_steps(fixture_data_dir: Path) -> None:
         br_cases=ListConfig(cfg.br_cases),
         special_tokens=ListConfig(cfg.special_tokens),
     )
-    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=dataclass_data_collator)
+    data_loader = DataLoader(dataset, batch_size=len(dataset), collate_fn=word_dataclass_data_collator)
+    val_dataloaders = {"dummy": data_loader}
 
     cfg.datamodule.valid = {"dummy": ""}
     cfg.datamodule.test = {"dummy": ""}
     module = WordModule(cfg)
 
-    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=data_loader)
-    trainer.test(model=module, dataloaders=data_loader)
-    trainer.predict(model=module, dataloaders=data_loader)
+    trainer.fit(model=module, train_dataloaders=data_loader, val_dataloaders=val_dataloaders)
+    trainer.test(model=module, dataloaders=val_dataloaders)
+    trainer.predict(model=module, dataloaders=val_dataloaders)
diff --git a/tests/utils/test_seq2seq_format.py b/tests/utils/test_seq2seq_format.py
index 6af3b1a0..1d4e9612 100644
--- a/tests/utils/test_seq2seq_format.py
+++ b/tests/utils/test_seq2seq_format.py
@@ -6,7 +6,7 @@
 from transformers import PreTrainedTokenizerBase
 
 from kwja.callbacks.seq2seq_module_writer import Seq2SeqModuleWriter
-from kwja.utils.constants import NEW_LINE_TOKEN, NO_CANON_TOKEN, NO_READING_TOKEN
+from kwja.utils.constants import FULL_SPACE_TOKEN, NEW_LINE_TOKEN, NO_CANON_TOKEN
 from kwja.utils.seq2seq_format import get_sent_from_seq2seq_format, get_seq2seq_format
 
 input_seq2seq_formats = [
@@ -67,10 +67,19 @@
         f"""\
         後 あと 後 後/あと
         一 ついたち 一 一/いち
-        日 {NO_READING_TOKEN} 日 日/にち
+        日 {FULL_SPACE_TOKEN} 日 日/にち
         まで まで まで まで/まで
         ！ ！ ！ ！/！
         ？ ？ ？ ？/？
+        . . . ./.
+        / / / ///
+        """
+    ),
+    dedent(
+        f"""\
+        ＪＵＭＰ ＪＵＭＰ ＪＵＭＰ ＪＵＭＰ/ＪＵＭＰ
+        {FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {NO_CANON_TOKEN}
+        ＣＯＭＩＣＳ ＣＯＭＩＣＳ ＣＯＭＩＣＳ ＣＯＭＩＣＳ/ＣＯＭＩＣＳ
         """
     ),
 ]
@@ -92,7 +101,7 @@
     dedent(
         """\
         また また また 又/また
-        , , , ,/,
+        ,,,,/,
         校区 こうく 校区 校区/こうく
         で で で で/で
         行わ おこなわ 行う 行う/おこなう
@@ -119,7 +128,7 @@
         核 かく 核 核/かく
         の の の の/の
         歴史 れきし 歴史 歴史/れきし
-        … … … …/…
+        ............/...
         ヒロシマ ひろしま ヒロシマ ヒロシマ/ひろしま
         、 、 、 、/、
         ナガサキ ながさき ナガサキ ナガサキ/ながさき
@@ -133,10 +142,19 @@
         f"""\
         後 あと 後 後/あと
         一 ついたち 一 一/いち
-        日 {NO_READING_TOKEN} 日 日/にち
+        日 {FULL_SPACE_TOKEN} 日 日/にち
         まで まで まで まで/まで
-        ! ! ! !/!
-        ? ? ? ?/?
+        !!!!/!
+        ????/?
+        ..../.
+        / / / ///
+        """
+    ),
+    dedent(
+        f"""\
+        ＪＵＭＰ ＪＵＭＰ ＪＵＭＰ ＪＵＭＰ/ＪＵＭＰ
+        {FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {FULL_SPACE_TOKEN} {NO_CANON_TOKEN}
+        ＣＯＭＩＣＳ ＣＯＭＩＣＳ ＣＯＭＩＣＳ ＣＯＭＩＣＳ/ＣＯＭＩＣＳ
         """
     ),
 ]
@@ -163,10 +181,8 @@ def test_get_sent_from_seq2seq_format(fixture_data_dir: Path, seq2seq_tokenizer:
         actual_sent = get_sent_from_seq2seq_format(shaped_output)
         assert len(actual_sent.morphemes) == len(expected_sent.morphemes)
         for actual_morpheme, expected_morpheme in zip(actual_sent.morphemes, expected_sent.morphemes):
-            if expected_morpheme.reading == "\u3000":
-                expected_reading: str = NO_READING_TOKEN
-            elif "/" in expected_morpheme.reading:
-                expected_reading = expected_morpheme.reading.split("/")[0]
+            if "/" in expected_morpheme.reading and len(expected_morpheme.reading) > 1:
+                expected_reading: str = expected_morpheme.reading.split("/")[0]
             else:
                 expected_reading = expected_morpheme.reading