diff --git a/.gitattributes b/.gitattributes index 537137ab5..c0428316d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ +test/**/__snapshots__/**/*.json linguist-generated=true + * text=auto *.png -text -*.wav -text \ No newline at end of file +*.wav -text diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 3b810366c..8ce46a1c0 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -15,8 +15,8 @@ on: env: IMAGE_NAME: ${{ vars.DOCKERHUB_USERNAME }}/voicevox_engine PYTHON_VERSION: "3.11.3" - VOICEVOX_RESOURCE_VERSION: "0.14.4" - VOICEVOX_CORE_VERSION: "0.14.5" + VOICEVOX_RESOURCE_VERSION: "0.16.0" + VOICEVOX_CORE_VERSION: "0.15.0" defaults: run: @@ -32,7 +32,7 @@ jobs: id: vars run: | : # releaseタグ名か、workflow_dispatchでのバージョン名か、latestが入る - echo "version_or_latest=${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }}" >> $GITHUB_OUTPUT + echo "version_or_latest=${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }}" >> "$GITHUB_OUTPUT" build-docker: needs: [config] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f88c238e..06501d043 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,8 @@ on: env: PYTHON_VERSION: "3.11.3" - VOICEVOX_RESOURCE_VERSION: "0.14.4" - VOICEVOX_CORE_VERSION: "0.14.5" + VOICEVOX_RESOURCE_VERSION: "0.16.0" + VOICEVOX_CORE_VERSION: "0.15.0" defaults: run: @@ -44,9 +44,9 @@ jobs: id: vars run: | : # release タグ名, または workflow_dispatch でのバージョン名. リリースでない (push event) 場合は空文字列 - echo "version=${{ github.event.release.tag_name || github.event.inputs.version }}" >> $GITHUB_OUTPUT + echo "version=${{ github.event.release.tag_name || github.event.inputs.version }}" >> "$GITHUB_OUTPUT" : # release タグ名, または workflow_dispatch でのバージョン名, または 'latest' - echo "version_or_latest=${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }}" >> $GITHUB_OUTPUT + echo "version_or_latest=${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }}" >> "$GITHUB_OUTPUT" build-and-upload: needs: [config] @@ -108,7 +108,7 @@ jobs: - name: declare variables id: vars run: | - echo "package_name=voicevox_engine-${{ matrix.target }}-${{ needs.config.outputs.version }}" >> $GITHUB_OUTPUT + echo "package_name=voicevox_engine-${{ matrix.target }}-${{ needs.config.outputs.version }}" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4 @@ -420,14 +420,7 @@ jobs: VOICEVOX_CORE_ASSET_NAME: ${{ matrix.voicevox_core_asset_prefix }}-${{ env.VOICEVOX_CORE_VERSION }} run: | curl -L "https://github.com/VOICEVOX/voicevox_core/releases/download/${{ env.VOICEVOX_CORE_VERSION }}/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip" > download/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip - # NOTE: Windows 版コアのみ PowerShell の Compress-Archive コマンドレットを用いて zip を作成している(デフォルト状態では zip コマンドが存在していないため)。 - # このコマンドはバージョンによっては作成した zip 内のパスの区切り文字がバックスラッシュになる。 (cf. https://github.com/PowerShell/Microsoft.PowerShell.Archive/issues/48) - # unzip コマンドはこのような zip ファイルを解凍できるものの、終了コード 1 を報告して CI が落ちてしまう。 - # 回避策として、unzip コマンドの代わりに 7z コマンドを用いて zip ファイルを解凍する。 - # unzip download/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip -d download/ - if [[ ${{ matrix.os }} == windows-* ]]; then - 7z x -o"download" download/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip - elif [[ ${{ matrix.os }} == mac-* ]]; then + if [[ ${{ matrix.os }} == mac-* ]]; then ditto -x -k --sequesterRsrc --rsrc download/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip download/ else unzip download/${{ env.VOICEVOX_CORE_ASSET_NAME }}.zip -d download/ @@ -634,6 +627,17 @@ jobs: ${{ steps.vars.outputs.package_name }}.vvpp.txt commit: ${{ github.sha }} + update-tag-to-current-commit: + if: needs.config.outputs.version != '' + needs: [config, build-and-upload] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Change tag to this commit for refreshing the release # c.f. voicevox_engine#854 + run: | + git tag -f ${{ needs.config.outputs.version }} + git push -f --tag + run-release-test-workflow: if: needs.config.outputs.version != '' needs: [config, build-and-upload] diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 06d547dcf..f485a56e1 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,7 +10,7 @@ jobs: triage: runs-on: ubuntu-latest steps: - - uses: github/issue-labeler@v3 + - uses: github/issue-labeler@v3.3 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" configuration-path: .github/labeler.yml diff --git a/.github/workflows/release-test-docker.yml b/.github/workflows/release-test-docker.yml index da73a3a8f..d30337798 100644 --- a/.github/workflows/release-test-docker.yml +++ b/.github/workflows/release-test-docker.yml @@ -58,9 +58,9 @@ jobs: id: docker_vars run: | if [ "${{ matrix.tag }}" != "" ]; then - echo "image_tag=${{ env.IMAGE_NAME }}:${{ matrix.tag }}-${{ env.VERSION }}" >> $GITHUB_OUTPUT + echo "image_tag=${{ env.IMAGE_NAME }}:${{ matrix.tag }}-${{ env.VERSION }}" >> "$GITHUB_OUTPUT" else - echo "image_tag=${{ env.IMAGE_NAME }}:${{ env.VERSION }}" >> $GITHUB_OUTPUT + echo "image_tag=${{ env.IMAGE_NAME }}:${{ env.VERSION }}" >> "$GITHUB_OUTPUT" fi - name: Docker pull @@ -81,14 +81,14 @@ jobs: max_attempts=10 sleep_interval=5 - for i in $(seq 1 $max_attempts); do - status=$(curl -o /dev/null -s -w '%{http_code}\n' $url) - if [ $status -eq 200 ]; then - echo "Container is ready! Response status code: $status" + for i in $(seq 1 "$max_attempts"); do + status=$(curl -o /dev/null -s -w '%{http_code}\n' "$url") + if [ "$status" -eq 200 ]; then + echo "Container is ready! Response status code: ${status}" exit 0 else - echo "Attempt $i/$max_attempts: Response status code $status" - sleep $sleep_interval + echo "Attempt ${i}/${max_attempts}: Response status code $status" + sleep "${sleep_interval}" fi done exit 1 diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml index d5995ae18..ec680ab10 100644 --- a/.github/workflows/release-test.yml +++ b/.github/workflows/release-test.yml @@ -56,8 +56,8 @@ jobs: - name: declare variables id: vars run: | - echo "release_url=${{ env.REPO_URL }}/releases/download/${{ env.VERSION }}" >> $GITHUB_OUTPUT - echo "package_name=voicevox_engine-${{ matrix.target }}-${{ env.VERSION }}" >> $GITHUB_OUTPUT + echo "release_url=${{ env.REPO_URL }}/releases/download/${{ env.VERSION }}" >> "$GITHUB_OUTPUT" + echo "package_name=voicevox_engine-${{ matrix.target }}-${{ env.VERSION }}" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4 @@ -72,7 +72,7 @@ jobs: curl -L -o "download/list.txt" "${{ steps.vars.outputs.release_url }}/${{ steps.vars.outputs.package_name }}.7z.txt" cat "download/list.txt" | xargs -I '%' curl -L -o "download/%" "${{ steps.vars.outputs.release_url }}/%" 7z x "download/$(head -n1 download/list.txt)" - mv ${{ matrix.target }} dist/ + mv "${{ matrix.target }}" dist/ - name: chmod +x if: startsWith(matrix.target, 'linux') || startsWith(matrix.target, 'macos') diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3666063d5..3dfeb478c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-20.04, windows-latest] # [ubuntu-20.04, macos-latest, windows-latest] + os: [ubuntu-20.04, macos-latest, windows-latest] python: ["3.11.3"] steps: diff --git a/.github/workflows/upload-gh-pages.yml b/.github/workflows/upload-gh-pages.yml index 9e78d0a1b..3efc702b4 100644 --- a/.github/workflows/upload-gh-pages.yml +++ b/.github/workflows/upload-gh-pages.yml @@ -1,3 +1,5 @@ +# API docs HTML ファイルを生成し、`gh-pages` ブランチへの push によって GitHub Pages 上のドキュメントとして公開 + name: upload-docs on: @@ -34,7 +36,7 @@ jobs: - name: Make documents run: | - python make_docs.py + PYTHONPATH=. python build_util/make_docs.py - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/Dockerfile b/Dockerfile index 5a5279235..4613717dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ EOF # assert VOICEVOX_CORE_VERSION >= 0.11.0 (ONNX) ARG TARGETPLATFORM ARG USE_GPU=false -ARG VOICEVOX_CORE_VERSION=0.14.5 +ARG VOICEVOX_CORE_VERSION=0.15.0 RUN < /opt/voicevox_engine/engine_manifest_assets/dependency_licenses.json + gosu user /opt/python/bin/python3 build_util/generate_licenses.py > /opt/voicevox_engine/engine_manifest_assets/dependency_licenses.json cp /opt/voicevox_engine/engine_manifest_assets/dependency_licenses.json /opt/voicevox_engine/licenses.json EOF @@ -274,7 +275,7 @@ RUN <text.txt curl -s \ -X POST \ - "127.0.0.1:50021/audio_query?style_id=1"\ + "127.0.0.1:50021/audio_query?speaker=1"\ --get --data-urlencode text@text.txt \ > query.json @@ -45,24 +65,33 @@ curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @query.json \ - "127.0.0.1:50021/synthesis?style_id=1" \ + "127.0.0.1:50021/synthesis?speaker=1" \ > audio.wav ``` 生成される音声はサンプリングレートが 24000Hz と少し特殊なため、音声プレーヤーによっては再生できない場合があります。 -`style_id` に指定する値は `/speakers` エンドポイントで得られます。 +`speaker` に指定する値は `/speakers` エンドポイントで得られる `style_id` です。互換性のために `speaker` という名前になっています。 + +### 読み方を AquesTalk 風記法で取得・修正 + +#### AquesTalk 風記法 -### 読み方を AquesTalk風記法で取得・修正するサンプルコード + -`/audio_query`のレスポンスにはエンジンが判断した読み方が AquesTalk 風記法([本家の記法](https://www.a-quest.com/archive/manual/siyo_onseikigou.pdf)とは一部異なります)で記録されています。 -記法は次のルールに従います。 +「**AquesTalk 風記法**」はカタカナと記号だけで読み方を指定する記法です。[AquesTalk 本家の記法](https://www.a-quest.com/archive/manual/siyo_onseikigou.pdf)とは一部が異なります。 +AquesTalk 風記法は次のルールに従います: - 全てのカナはカタカナで記述される -- アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。 -- カナの手前に`_`を入れるとそのカナは無声化される -- アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を 1 つ指定する必要がある。 -- アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる +- アクセント句は `/` または `、` で区切る。 `、` で区切った場合に限り無音区間が挿入される。 +- カナの手前に `_` を入れるとそのカナは無声化される +- アクセント位置を `'` で指定する。全てのアクセント句にはアクセント位置を 1 つ指定する必要がある。 +- アクセント句末に `?` (全角)を入れることにより疑問文の発音ができる + +#### AquesTalk 風記法のサンプルコード + +`/audio_query`のレスポンスにはエンジンが判断した読み方が[AquesTalk 風記法](#aquestalk-風記法)で記述されます。 +これを修正することで音声の読み仮名やアクセントを制御できます。 ```bash # 読ませたい文章をutf-8でtext.txtに書き出す @@ -70,7 +99,7 @@ echo -n "ディープラーニングは万能薬ではありません" >text.txt curl -s \ -X POST \ - "127.0.0.1:50021/audio_query?style_id=1" \ + "127.0.0.1:50021/audio_query?speaker=1" \ --get --data-urlencode text@text.txt \ > query.json @@ -82,7 +111,7 @@ cat query.json | grep -o -E "\"kana\":\".*\"" echo -n "ディイプラ'アニングワ/バンノ'オヤクデワ/アリマセ'ン" > kana.txt curl -s \ -X POST \ - "127.0.0.1:50021/accent_phrases?style_id=1&is_kana=true" \ + "127.0.0.1:50021/accent_phrases?speaker=1&is_kana=true" \ --get --data-urlencode text@kana.txt \ > newphrases.json @@ -93,7 +122,7 @@ curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @newquery.json \ - "127.0.0.1:50021/synthesis?style_id=1" \ + "127.0.0.1:50021/synthesis?speaker=1" \ > audio.wav ``` @@ -176,6 +205,14 @@ word_uuid="cce59b5f-86ab-42b9-bb75-9fd3407f1e2d" curl -s -X DELETE "127.0.0.1:50021/user_dict_word/$word_uuid" ``` +#### 辞書のインポート&エクスポート + +エンジンの[設定ページ](http://127.0.0.1:50021/setting)内の「ユーザー辞書のエクスポート&インポート」節で、ユーザー辞書のインポート&エクスポートが可能です。 + +他にも API でユーザー辞書のインポート&エクスポートが可能です。 +インポートには `POST /import_user_dict`、エクスポートには `GET /user_dict` を利用します。 +引数等の詳細は API ドキュメントをご覧ください。 + ### プリセット機能について `presets.yaml`を編集することで話者や話速などのプリセットを使うことができます。 @@ -189,7 +226,7 @@ curl -s -X GET "127.0.0.1:50021/presets" > presets.json preset_id=$(cat presets.json | sed -r 's/^.+"id"\:\s?([0-9]+?).+$/\1/g') style_id=$(cat presets.json | sed -r 's/^.+"style_id"\:\s?([0-9]+?).+$/\1/g') -# AudioQueryの取得 +# 音声合成用のクエリを取得 curl -s \ -X POST \ "127.0.0.1:50021/audio_query_from_preset?preset_id=$preset_id"\ @@ -201,7 +238,7 @@ curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @query.json \ - "127.0.0.1:50021/synthesis?style_id=$style_id" \ + "127.0.0.1:50021/synthesis?speaker=$style_id" \ > audio.wav ``` @@ -209,35 +246,35 @@ curl -s \ - `id`は重複してはいけません - エンジン起動後にファイルを書き換えるとエンジンに反映されます -### 2 人の話者でモーフィングするサンプルコード +### 2 種類のスタイルでモーフィングするサンプルコード -`/synthesis_morphing`では、2 人の話者でそれぞれ合成された音声を元に、モーフィングした音声を生成します。 +`/synthesis_morphing`では、2 種類のスタイルでそれぞれ合成された音声を元に、モーフィングした音声を生成します。 ```bash -echo -n "モーフィングを利用することで、2つの声を混ぜることができます。" > text.txt +echo -n "モーフィングを利用することで、2種類の声を混ぜることができます。" > text.txt curl -s \ -X POST \ - "127.0.0.1:50021/audio_query?style_id=0"\ + "127.0.0.1:50021/audio_query?speaker=8"\ --get --data-urlencode text@text.txt \ > query.json -# 元の話者での合成結果 +# 元のスタイルでの合成結果 curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @query.json \ - "127.0.0.1:50021/synthesis?style_id=0" \ + "127.0.0.1:50021/synthesis?speaker=8" \ > audio.wav export MORPH_RATE=0.5 -# 話者2人分の音声合成+WORLDによる音声分析が入るため時間が掛かるので注意 +# スタイル2種類分の音声合成+WORLDによる音声分析が入るため時間が掛かるので注意 curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @query.json \ - "127.0.0.1:50021/synthesis_morphing?base_speaker=0&target_speaker=1&morph_rate=$MORPH_RATE" \ + "127.0.0.1:50021/synthesis_morphing?base_speaker=8&target_speaker=10&morph_rate=$MORPH_RATE" \ > audio.wav export MORPH_RATE=0.9 @@ -247,7 +284,7 @@ curl -s \ -H "Content-Type: application/json" \ -X POST \ -d @query.json \ - "127.0.0.1:50021/synthesis_morphing?base_speaker=0&target_speaker=1&morph_rate=$MORPH_RATE" \ + "127.0.0.1:50021/synthesis_morphing?base_speaker=8&target_speaker=10&morph_rate=$MORPH_RATE" \ > audio.wav ``` @@ -283,6 +320,14 @@ VOICEVOX ではセキュリティ保護のため`localhost`・`127.0.0.1`・`app 3. 保存ボタンを押して、変更を確定してください。 4. 設定の適用にはエンジンの再起動が必要です。必要に応じて再起動をしてください。 +### データを変更する API を無効化する + +実行時引数`--disable_mutable_api`か環境変数`VV_DISABLE_MUTABLE_API=1`を指定することで、エンジンの設定や辞書などを変更する API を無効にできます。 + +### 文字コード + +リクエスト・レスポンスの文字コードはすべて UTF-8 です。 + ### その他の引数 エンジン起動時に引数を指定できます。詳しいことは`-h`引数でヘルプを確認してください。 @@ -318,59 +363,43 @@ options: --output_log_utf8 指定するとログ出力をUTF-8でおこないます。指定しないと、代わりに環境変数 VV_OUTPUT_LOG_UTF8 の値が使われます。VV_OUTPUT_LOG_UTF8 の値が1の場合はUTF-8で、0または空文字、値がない場合は環境によって自動的に決定されます。 --cors_policy_mode {CorsPolicyMode.all,CorsPolicyMode.localapps} CORSの許可モード。allまたはlocalappsが指定できます。allはすべてを許可します。localappsはオリジン間リソース共有ポリシーを、app://.とlocalhost関連に限定します。その他のオリジンはallow_originオプションで追加できます。デフォルトはlocalapps。 + このオプションは--setting_fileで指定される設定ファイルよりも優先されます。 --allow_origin [ALLOW_ORIGIN ...] 許可するオリジンを指定します。スペースで区切ることで複数指定できます。 + このオプションは--setting_fileで指定される設定ファイルよりも優先されます。 --setting_file SETTING_FILE 設定ファイルを指定できます。 --preset_file PRESET_FILE プリセットファイルを指定できます。指定がない場合、環境変数 VV_PRESET_FILE、--voicevox_dirのpresets.yaml、実行ファイルのディレクトリのpresets.yamlを順に探します。 ``` -## アップデート +### アップデート エンジンディレクトリ内にあるファイルを全て消去し、新しいものに置き換えてください。 -## Docker イメージ +## 開発者・貢献者向けガイド -### CPU - -```bash -docker pull voicevox/voicevox_engine:cpu-ubuntu20.04-latest -docker run --rm -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-ubuntu20.04-latest -``` - -### GPU - -```bash -docker pull voicevox/voicevox_engine:nvidia-ubuntu20.04-latest -docker run --rm --gpus all -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:nvidia-ubuntu20.04-latest -``` - -#### トラブルシューティング - -GPU 版を利用する場合、環境によってエラーが発生することがあります。その場合、`--runtime=nvidia`を`docker run`につけて実行すると解決できることがあります。 - -## 貢献者の方へ +### 貢献者の方へ Issue を解決するプルリクエストを作成される際は、別の方と同じ Issue に取り組むことを避けるため、 Issue 側で取り組み始めたことを伝えるか、最初に Draft プルリクエストを作成してください。 [VOICEVOX 非公式 Discord サーバー](https://discord.gg/WMwWetrzuh)にて、開発の議論や雑談を行っています。気軽にご参加ください。 -## 環境構築 +### 環境構築 `Python 3.11.3` を用いて開発されています。 インストールするには、各 OS ごとの C/C++ コンパイラ、CMake が必要になります。 ```bash -# 開発に必要なライブラリのインストール -python -m pip install -r requirements-dev.txt -r requirements-test.txt - -# とりあえず実行したいだけなら代わりにこちら +# 実行環境のインストール python -m pip install -r requirements.txt + +# 開発環境・テスト環境のインストール +python -m pip install -r requirements-dev.txt -r requirements-test.txt ``` -## 実行 +### 実行 コマンドライン引数の詳細は以下のコマンドで確認してください。 @@ -403,30 +432,28 @@ python run.py --output_log_utf8 # もしくは VV_OUTPUT_LOG_UTF8=1 python run.py ``` -### CPU スレッド数を指定する +#### CPU スレッド数を指定する CPU スレッド数が未指定の場合は、論理コア数の半分か物理コア数が使われます。(殆どの CPU で、これは全体の処理能力の半分です) もし IaaS 上で実行していたり、専用サーバーで実行している場合など、 エンジンが使う処理能力を調節したい場合は、CPU スレッド数を指定することで実現できます。 - 実行時引数で指定する - ```bash python run.py --voicevox_dir=$VOICEVOX_DIR --cpu_num_threads=4 ``` - - 環境変数で指定する ```bash export VV_CPU_NUM_THREADS=4 python run.py --voicevox_dir=$VOICEVOX_DIR ``` -### 過去のバージョンのコアを使う +#### 過去のバージョンのコアを使う VOICEVOX Core 0.5.4 以降のコアを使用する事が可能です。 Mac での libtorch 版コアのサポートはしていません。 -#### 過去のバイナリを指定する +##### 過去のバイナリを指定する 製品版 VOICEVOX もしくはコンパイル済みエンジンのディレクトリを`--voicevox_dir`引数で指定すると、そのバージョンのコアが使用されます。 @@ -440,7 +467,7 @@ Mac では、`DYLD_LIBRARY_PATH`の指定が必要です。 DYLD_LIBRARY_PATH="/path/to/voicevox" python run.py --voicevox_dir="/path/to/voicevox" ``` -#### 音声ライブラリを直接指定する +##### 音声ライブラリを直接指定する [VOICEVOX Core の zip ファイル](https://github.com/VOICEVOX/voicevox_core/releases)を解凍したディレクトリを`--voicelib_dir`引数で指定します。 また、コアのバージョンに合わせて、[libtorch](https://pytorch.org/)や[onnxruntime](https://github.com/microsoft/onnxruntime)のディレクトリを`--runtime_dir`引数で指定します。 @@ -458,7 +485,41 @@ Mac では、`--runtime_dir`引数の代わりに`DYLD_LIBRARY_PATH`の指定が DYLD_LIBRARY_PATH="/path/to/onnx" python run.py --voicelib_dir="/path/to/voicevox_core" ``` -## コードフォーマット +##### ユーザーディレクトリに配置する + +以下のディレクトリにある音声ライブラリは自動で読み込まれます。 + +- ビルド版: `/voicevox-engine/core_libraries/` +- Python 版: `/voicevox-engine-dev/core_libraries/` + +``は OS によって異なります。 + +- Windows: `C:\Users\\AppData\Local\` +- macOS: `/Users//Library/Application\ Support/` +- Linux: `/home//.local/share/` + +### ビルド + +この方法でビルドしたものは、リリースで公開されているものとは異なります。 +また、GPU で利用するには cuDNN や CUDA、DirectML などのライブラリが追加で必要となります。 + +```bash +python -m pip install -r requirements-dev.txt + +OUTPUT_LICENSE_JSON_PATH=licenses.json \ +bash build_util/create_venv_and_generate_licenses.bash + +# モックでビルドする場合 +pyinstaller --noconfirm run.spec + +# 製品版でビルドする場合 +CORE_MODEL_DIR_PATH="/path/to/core_model" \ +LIBCORE_PATH="/path/to/libcore" \ +LIBONNXRUNTIME_PATH="/path/to/libonnxruntime" \ +pyinstaller --noconfirm run.spec +``` + +### コードフォーマット このソフトウェアでは、リモートにプッシュする前にコードフォーマットを確認する仕組み(静的解析ツール)を利用できます。 利用するには、開発に必要なライブラリのインストールに加えて、以下のコマンドを実行してください。 @@ -474,13 +535,19 @@ pre-commit install -t pre-push pysen run format lint ``` -## テスト +### テスト ```bash python -m pytest ``` -## タイポチェック +#### スナップショットの更新 + +```bash +python -m pytest --snapshot-update +``` + +### タイポチェック [typos](https://github.com/crate-ci/typos) を使ってタイポのチェックを行っています。 [typos をインストール](https://github.com/crate-ci/typos#install) した後 @@ -493,39 +560,9 @@ typos もし誤判定やチェックから除外すべきファイルがあれば [設定ファイルの説明](https://github.com/crate-ci/typos#false-positives) に従って`_typos.toml`を編集してください。 -## API ドキュメントの確認 +### 依存関係 -[API ドキュメント](https://voicevox.github.io/voicevox_engine/api/)(実体は`docs/api/index.html`)は自動で更新されます。 -次のコマンドで API ドキュメントを手動で作成することができます。 - -```bash -python make_docs.py -``` - -## ビルド - -この方法でビルドしたものは、リリースで公開されているものとは異なります。 -また、GPU で利用するには cuDNN や CUDA、DirectML などのライブラリが追加で必要となります。 - -```bash -python -m pip install -r requirements-dev.txt - -OUTPUT_LICENSE_JSON_PATH=licenses.json \ -bash build_util/create_venv_and_generate_licenses.bash - -# モックでビルドする場合 -pyinstaller --noconfirm run.spec - -# 製品版でビルドする場合 -CORE_MODEL_DIR_PATH="/path/to/core_model" \ -LIBCORE_PATH="/path/to/libcore" \ -LIBONNXRUNTIME_PATH="/path/to/libonnxruntime" \ -pyinstaller --noconfirm run.spec -``` - -## 依存関係 - -### 更新 +#### 更新 [Poetry](https://python-poetry.org/) を用いて依存ライブラリのバージョンを固定しています。 以下のコマンドで操作できます: @@ -547,7 +584,7 @@ poetry export --without-hashes --with test -o requirements-test.txt poetry export --without-hashes --with license -o requirements-license.txt ``` -### ライセンス +#### ライセンス 依存ライブラリは「コアビルド時にリンクして一体化しても、コア部のコード非公開 OK」なライセンスを持つ必要があります。 主要ライセンスの可否は以下の通りです。 @@ -556,15 +593,7 @@ poetry export --without-hashes --with license -o requirements-license.txt - LGPL: OK (コアと動的分離されているため) - GPL: NG (全関連コードの公開が必要なため) -## ユーザー辞書の更新について - -以下のコマンドで openjtalk のユーザー辞書をコンパイルできます。 - -```bash -python -c "import pyopenjtalk; pyopenjtalk.create_user_dict('default.csv','user.dic')" -``` - -## マルチエンジン機能に関して +### マルチエンジン機能に関して VOICEVOX エディターでは、複数のエンジンを同時に起動することができます。 この機能を利用することで、自作の音声合成エンジンや既存の音声合成エンジンを VOICEVOX エディター上で動かすことが可能です。 @@ -573,12 +602,12 @@ VOICEVOX エディターでは、複数のエンジンを同時に起動する
-### マルチエンジン機能の仕組み +#### マルチエンジン機能の仕組み VOICEVOX API に準拠した複数のエンジンの Web API をポートを分けて起動し、統一的に扱うことでマルチエンジン機能を実現しています。 エディターがそれぞれのエンジンを実行バイナリ経由で起動し、EngineID と結びつけて設定や状態を個別管理します。 -### マルチエンジン機能への対応方法 +#### マルチエンジン機能への対応方法 VOICEVOX API 準拠エンジンを起動する実行バイナリを作ることで対応が可能です。 VOICEVOX ENGINE リポジトリを fork し、一部の機能を改造するのが簡単です。 @@ -593,11 +622,11 @@ VOICEVOX ENGINE リポジトリを fork し、一部の機能を改造するの キャラクター情報は`speaker_info`ディレクトリ内のファイルで管理されています。 ダミーのアイコンなどが用意されているので適宜変更してください。 -音声合成は`voicevox_engine/synthesis_engine/synthesis_engine.py`で行われています。 -VOICEVOX API での音声合成は、エンジン側で音声合成クエリ`AudioQuery`の初期値を作成してユーザーに返し、ユーザーが必要に応じてクエリを編集したあと、エンジンがクエリに従って音声合成することで実現しています。 +音声合成は`voicevox_engine/tts_pipeline/tts_engine.py`で行われています。 +VOICEVOX API での音声合成は、エンジン側で音声合成用のクエリ `AudioQuery` の初期値を作成してユーザーに返し、ユーザーが必要に応じてクエリを編集したあと、エンジンがクエリに従って音声合成することで実現しています。 クエリ作成は`/audio_query`エンドポイントで、音声合成は`/synthesis`エンドポイントで行っており、最低この2つに対応すれば VOICEVOX API に準拠したことになります。 -### マルチエンジン機能対応エンジンの配布方法 +#### マルチエンジン機能対応エンジンの配布方法 VVPP ファイルとして配布するのがおすすめです。 VVPP は「VOICEVOX プラグインパッケージ」の略で、中身はビルドしたエンジンなどを含んだディレクトリの Zip ファイルです。 @@ -611,15 +640,24 @@ VOICEVOX エディターにうまく読み込ませられないときは、エ
-## GitHub Actions +### API ドキュメントの確認 + +[API ドキュメント](https://voicevox.github.io/voicevox_engine/api/)(実体は`docs/api/index.html`)は自動で更新されます。 +次のコマンドで API ドキュメントを手動で作成することができます。 + +```bash +PYTHONPATH=. python build_util/make_docs.py +``` + +### GitHub Actions -### Variables +#### Variables | name | description | | :----------------- | :------------------ | | DOCKERHUB_USERNAME | Docker Hub ユーザ名 | -### Secrets +#### Secrets | name | description | | :-------------- | :---------------------------------------------------------------------- | diff --git a/build_util/check_release_build.py b/build_util/check_release_build.py index 008f25548..a04c53d39 100644 --- a/build_util/check_release_build.py +++ b/build_util/check_release_build.py @@ -34,14 +34,14 @@ def test_release_build(dist_dir: Path, skip_run_process: bool) -> None: # テキスト -> クエリ text = "こんにちは、音声合成の世界へようこそ" req = Request( - base_url + "audio_query?" + urlencode({"style_id": "1", "text": text}), + base_url + "audio_query?" + urlencode({"speaker": "1", "text": text}), method="POST", ) with urlopen(req) as res: query = json.loads(res.read().decode("utf-8")) # クエリ -> 音声 - req = Request(base_url + "synthesis?style_id=1", method="POST") + req = Request(base_url + "synthesis?speaker=1", method="POST") req.add_header("Content-Type", "application/json") req.data = json.dumps(query).encode("utf-8") with urlopen(req) as res: @@ -56,6 +56,7 @@ def test_release_build(dist_dir: Path, skip_run_process: bool) -> None: if not skip_run_process: # プロセスが稼働中であることを確認 + assert process is not None assert process.poll() is None # 停止 diff --git a/build_util/create_venv_and_generate_licenses.bash b/build_util/create_venv_and_generate_licenses.bash index d2c837dbf..fc9dd0dc5 100644 --- a/build_util/create_venv_and_generate_licenses.bash +++ b/build_util/create_venv_and_generate_licenses.bash @@ -17,7 +17,7 @@ else fi pip install -r requirements-license.txt -python generate_licenses.py >$OUTPUT_LICENSE_JSON_PATH +python build_util/generate_licenses.py > "${OUTPUT_LICENSE_JSON_PATH}" deactivate diff --git a/generate_licenses.py b/build_util/generate_licenses.py similarity index 100% rename from generate_licenses.py rename to build_util/generate_licenses.py diff --git a/get_cost_candidates.py b/build_util/get_cost_candidates.py similarity index 93% rename from get_cost_candidates.py rename to build_util/get_cost_candidates.py index 072c4b4d5..2eabb4eeb 100644 --- a/get_cost_candidates.py +++ b/build_util/get_cost_candidates.py @@ -1,9 +1,9 @@ """ -voicevox_engine/part_of_speech_data.pyのcost_candidatesを計算するプログラムです。 +voicevox_engine/user_dict/part_of_speech_data.pyのcost_candidatesを計算するプログラムです。 引数のnaist_jdic_pathには、open_jtalkのsrc/mecab-naist-jdic/naist-jdic.csvを指定してください。 実行例: -python get_cost_candidates.py --naist_jdic_path=/path/to/naist-jdic.csv \ +python build_util/get_cost_candidates.py --naist_jdic_path=/path/to/naist-jdic.csv \ --pos=名詞 \ --pos_detail_1=固有名詞 \ --pos_detail_2=一般 \ diff --git a/build_util/make_docs.py b/build_util/make_docs.py new file mode 100644 index 000000000..14bf53f75 --- /dev/null +++ b/build_util/make_docs.py @@ -0,0 +1,58 @@ +import json +from pathlib import Path + +from voicevox_engine.dev.core.mock import MockCoreWrapper +from voicevox_engine.dev.tts_engine.mock import MockTTSEngine +from voicevox_engine.preset.PresetManager import PresetManager +from voicevox_engine.setting.SettingLoader import USER_SETTING_PATH, SettingHandler +from voicevox_engine.tts_pipeline.tts_engine import CoreAdapter +from voicevox_engine.utility.path_utility import engine_root + + +def generate_api_docs_html(schema: str) -> str: + """OpenAPI schema から API ドキュメント HTML を生成する""" + + return ( + """ + + + voicevox_engine API Document + + + + +
+ + + +""" + % schema + ) + + +if __name__ == "__main__": + import run + + mock_core = MockCoreWrapper() + # FastAPI の機能を用いて OpenAPI schema を生成する + app = run.generate_app( + tts_engines={"mock": MockTTSEngine()}, + cores={"mock": CoreAdapter(mock_core)}, + latest_core_version="mock", + setting_loader=SettingHandler(USER_SETTING_PATH), + preset_manager=PresetManager( # FIXME: impl MockPresetManager + preset_path=engine_root() / "presets.yaml", + ), + ) + api_schema = json.dumps(app.openapi()) + + # API ドキュメント HTML を生成する + api_docs_html = generate_api_docs_html(api_schema) + + # HTML ファイルとして保存する + api_docs_root = Path("docs/api") # 'upload-docs' workflow の対象 + output_path = api_docs_root / "index.html" + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(api_docs_html) diff --git a/build_util/merge_update_infos.py b/build_util/merge_update_infos.py index d3a5bb3a8..7c1ea5784 100644 --- a/build_util/merge_update_infos.py +++ b/build_util/merge_update_infos.py @@ -6,7 +6,6 @@ import json from collections import OrderedDict from pathlib import Path -from typing import Dict, List, Union def merge_json_string(src: str, dst: str) -> str: @@ -23,8 +22,10 @@ def merge_json_string(src: str, dst: str) -> str: >>> merge_json_string(src, dst) '[{"version": "1"}]' """ - src_json: List[Dict[str, Union[str, List[str]]]] = json.loads(src) - dst_json: List[Dict[str, Union[str, List[str]]]] = json.loads(dst) + # FIXME: バリデーションする + # TODO: `str | list[str]`だけど`str`が来るとエラーになるのでならないようにしたい + src_json: list[dict[str, str | list[str]]] = json.loads(src) + dst_json: list[dict[str, str | list[str]]] = json.loads(dst) for src_item in src_json: for dst_item in dst_json: @@ -33,10 +34,13 @@ def merge_json_string(src: str, dst: str) -> str: if key == "version": continue + src_value = src_item[key] + dst_value = dst_item[key] + assert isinstance(src_value, list) + assert isinstance(dst_value, list) + # 異なるものがあった場合だけ後ろに付け足す - src_item[key] = list( - OrderedDict.fromkeys(src_item[key] + dst_item[key]) - ) + src_item[key] = list(OrderedDict.fromkeys(src_value + dst_value)) return json.dumps(src_json) diff --git a/build_util/modify_pyinstaller.bash b/build_util/modify_pyinstaller.bash index a38e1656f..26b3e337e 100755 --- a/build_util/modify_pyinstaller.bash +++ b/build_util/modify_pyinstaller.bash @@ -4,6 +4,8 @@ # 良いGPUが自動的に選択されるようにしている # https://github.com/VOICEVOX/voicevox_engine/issues/502 +# 自前ビルドすることでブートローダーのハッシュ値が変わってウイルス判定を回避する効果もあるかも + set -eux pyinstaller_version=$(pyinstaller -v) diff --git a/build_util/process_voicevox_resource.bash b/build_util/process_voicevox_resource.bash index 7bd1d31f9..c085dfeee 100644 --- a/build_util/process_voicevox_resource.bash +++ b/build_util/process_voicevox_resource.bash @@ -6,22 +6,22 @@ if [ ! -v DOWNLOAD_RESOURCE_PATH ]; then fi rm -r speaker_info -cp -r $DOWNLOAD_RESOURCE_PATH/character_info speaker_info +cp -r "${DOWNLOAD_RESOURCE_PATH}/character_info" speaker_info # キャラクター情報の前処理をする -python $DOWNLOAD_RESOURCE_PATH/scripts/clean_character_info.py \ +python "${DOWNLOAD_RESOURCE_PATH}/scripts/clean_character_info.py" \ --character_info_dir speaker_info/ # マニフェスト -jq -s '.[0] * .[1]' engine_manifest.json $DOWNLOAD_RESOURCE_PATH/engine/engine_manifest.json \ +jq -s '.[0] * .[1]' engine_manifest.json "${DOWNLOAD_RESOURCE_PATH}/engine/engine_manifest.json" \ > engine_manifest.json.tmp mv engine_manifest.json.tmp engine_manifest.json python build_util/merge_update_infos.py \ engine_manifest_assets/update_infos.json \ - $DOWNLOAD_RESOURCE_PATH/engine/engine_manifest_assets/update_infos.json \ + "${DOWNLOAD_RESOURCE_PATH}/engine/engine_manifest_assets/update_infos.json" \ engine_manifest_assets/update_infos.json for f in $(ls $DOWNLOAD_RESOURCE_PATH/engine/engine_manifest_assets/* | grep -v update_infos.json); do - cp $f ./engine_manifest_assets/ + cp "${f}" ./engine_manifest_assets/ done diff --git a/default_setting.yml b/default_setting.yml deleted file mode 100644 index 3421e7a6a..000000000 --- a/default_setting.yml +++ /dev/null @@ -1,2 +0,0 @@ -allow_origin: null -cors_policy_mode: localapps diff --git "a/docs/VOICEVOX\351\237\263\345\243\260\345\220\210\346\210\220\343\202\250\343\203\263\343\202\270\343\203\263\343\201\250\343\201\256\351\200\243\346\220\272.md" "b/docs/VOICEVOX\351\237\263\345\243\260\345\220\210\346\210\220\343\202\250\343\203\263\343\202\270\343\203\263\343\201\250\343\201\256\351\200\243\346\220\272.md" index 21cc6d13e..540173be1 100644 --- "a/docs/VOICEVOX\351\237\263\345\243\260\345\220\210\346\210\220\343\202\250\343\203\263\343\202\270\343\203\263\343\201\250\343\201\256\351\200\243\346\220\272.md" +++ "b/docs/VOICEVOX\351\237\263\345\243\260\345\220\210\346\210\220\343\202\250\343\203\263\343\202\270\343\203\263\343\201\250\343\201\256\351\200\243\346\220\272.md" @@ -3,3 +3,5 @@ - バージョンが上がっても、`/audio_query`で返ってくる値をそのまま`/synthesis`に POST すれば音声合成できるようにする予定です - `AudioQuery`のパラメータは増えますが、なるべくデフォルト値で以前と変わらない音声が生成されるようにします - バージョン 0.7 から音声スタイルが実装されました。スタイルの情報は`/speakers`から取得できます + - スタイルの情報にある`style_id`を`speaker`に指定することで、今まで通り音声合成ができます + - style_id の指定先が speaker なのは互換性のためです diff --git a/engine_manifest.json b/engine_manifest.json index 2ceaaabea..82121f652 100644 --- a/engine_manifest.json +++ b/engine_manifest.json @@ -9,10 +9,10 @@ "port": 50021, "icon": "engine_manifest_assets/icon.png", "default_sampling_rate": 24000, + "frame_rate": 93.75, "terms_of_service": "engine_manifest_assets/terms_of_service.md", "update_infos": "engine_manifest_assets/update_infos.json", "dependency_licenses": "engine_manifest_assets/dependency_licenses.json", - "supported_vvlib_manifest_version": "0.15.0", "supported_features": { "adjust_mora_pitch": { "type": "bool", @@ -52,7 +52,12 @@ "synthesis_morphing" : { "type": "bool", "value": true, - "name": "2人の話者でモーフィングした音声を合成" + "name": "2種類のスタイルでモーフィングした音声を合成" + }, + "sing" : { + "type": "bool", + "value": true, + "name": "歌唱音声合成" }, "manage_library": { "type": "bool", diff --git a/engine_manifest_assets/update_infos.json b/engine_manifest_assets/update_infos.json index 16c97c36d..466f5de04 100644 --- a/engine_manifest_assets/update_infos.json +++ b/engine_manifest_assets/update_infos.json @@ -1,4 +1,58 @@ [ + { + "version": "0.16.0", + "descriptions": [ + "ソングAPIを追加", + "キャラクター「四国めたん」「ずんだもん」「春日部つむぎ」「雨晴はう」「波音リツ」のハミングを追加", + "キャラクター「波音リツ」のソングを追加" + ], + "contributors": ["Hiroshiba", "y-chan"] + }, + { + "version": "0.15.1", + "descriptions": ["ビルド成果物のディレクトリ構造を元に戻した"], + "contributors": [] + }, + { + "version": "0.15.0", + "descriptions": [ + "/validate_kana APIを追加", + "起動時のエンジン設定項目追加", + "ユーザー辞書のインポート・エクスポート機能追加", + "ビルド成果物のディレクトリ構造を変更", + "書き込み系APIを一括で無効化可能に", + "開発環境の向上", + "バグ修正" + ], + "contributors": [ + "aoirint", + "FujisakiEx", + "Hiroshiba", + "K-shir0", + "My-MC", + "nagi-miaow", + "okaits", + "raa0121", + "sabonerune", + "sevenc-nanashi", + "siketyan", + "stmtk1", + "takana-v", + "tarepan", + "tomoish", + "tuna2134", + "weweweok", + "whiteball", + "y-chan" + ] + }, + { + "version": "0.14.7", + "descriptions": [ + "キャラクター「小夜」「ずんだもん」「もち子さん」「青山龍星」のスタイルを追加・更新" + ], + "contributors": [] + }, { "version": "0.14.6", "descriptions": [ diff --git a/make_docs.py b/make_docs.py deleted file mode 100644 index d21ba85b9..000000000 --- a/make_docs.py +++ /dev/null @@ -1,38 +0,0 @@ -import json - -from voicevox_engine.dev.core import mock as core -from voicevox_engine.dev.synthesis_engine.mock import MockSynthesisEngine -from voicevox_engine.preset import PresetManager -from voicevox_engine.setting import USER_SETTING_PATH, SettingLoader -from voicevox_engine.utility import engine_root - -if __name__ == "__main__": - import run - - app = run.generate_app( - synthesis_engines={"mock": MockSynthesisEngine(speakers=core.metas())}, - latest_core_version="mock", - setting_loader=SettingLoader(USER_SETTING_PATH), - preset_manager=PresetManager( # FIXME: impl MockPresetManager - preset_path=engine_root() / "presets.yaml", - ), - ) - with open("docs/api/index.html", "w") as f: - f.write( - """ - - - voicevox_engine API Document - - - - -
- - - -""" - % json.dumps(app.openapi()) - ) diff --git a/poetry.lock b/poetry.lock index 6e7957c9a..daa98c671 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,16 +1,5 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. -[[package]] -name = "aiofiles" -version = "0.7.0" -description = "File support for asyncio." -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, - {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, -] - [[package]] name = "altgraph" version = "0.17.3" @@ -56,16 +45,6 @@ files = [ [package.extras] tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] - [[package]] name = "attrs" version = "23.1.0" @@ -535,52 +514,62 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cython" -version = "0.29.36" -description = "The Cython compiler for writing C extensions for the Python language." -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "Cython-0.29.36-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea33c1c57f331f5653baa1313e445fbe80d1da56dd9a42c8611037887897b9d"}, - {file = "Cython-0.29.36-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2fe34615c13ace29e77bf9d21c26188d23eff7ad8b3e248da70404e5f5436b95"}, - {file = "Cython-0.29.36-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ae75eac4f13cbbcb50b2097470dcea570182446a3ebd0f7e95dd425c2017a2d7"}, - {file = "Cython-0.29.36-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:847d07fc02978c4433d01b4f5ee489b75fd42fd32ccf9cc4b5fd887e8cffe822"}, - {file = "Cython-0.29.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7cb44aeaf6c5c25bd6a7562ece4eadf50d606fc9b5f624fa95bd0281e8bf0a97"}, - {file = "Cython-0.29.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:28fb10aabd56a2e4d399273b48e106abe5a0d271728fd5eed3d36e7171000045"}, - {file = "Cython-0.29.36-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:86b7a13c6b23ab6471d40a320f573fbc8a4e39833947eebed96661145dc34771"}, - {file = "Cython-0.29.36-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:19ccf7fc527cf556e2e6a3dfeffcadfbcabd24a59a988289117795dfed8a25ad"}, - {file = "Cython-0.29.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:74bddfc7dc8958526b2018d3adc1aa6dc9cf2a24095c972e5ad06758c360b261"}, - {file = "Cython-0.29.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6c4d7e36fe0211e394adffd296382b435ac22762d14f2fe45c506c230f91cf2d"}, - {file = "Cython-0.29.36-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:0bca6a7504e8cfc63a4d3c7c9b9a04e5d05501942a6c8cee177363b61a32c2d4"}, - {file = "Cython-0.29.36-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17c74f80b06e2fa8ffc8acd41925f4f9922da8a219cd25c6901beab2f7c56cc5"}, - {file = "Cython-0.29.36-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25ff471a459aad82146973b0b8c177175ab896051080713d3035ad4418739f66"}, - {file = "Cython-0.29.36-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9738f23d00d99481797b155ad58f8fc1c72096926ea2554b8ccc46e1d356c27"}, - {file = "Cython-0.29.36-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:af2f333f08c4c279f3480532341bf70ec8010bcbc7d8a6daa5ca0bf4513af295"}, - {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:cd77cedbcc13cb67aef39b8615fd50a67fc42b0c6defea6fc0a21e19d3a062ec"}, - {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50d506d73a46c4a522ef9fdafcbf7a827ba13907b18ff58f61a8fa0887d0bd8d"}, - {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6a571d7c7b52ee12d73bc65b4855779c069545da3bac26bec06a1389ad17ade5"}, - {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a216b2801c7d9c3babe0a10cc25da3bc92494d7047d1f732d3c47b0cceaf0941"}, - {file = "Cython-0.29.36-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:68abee3be27f21c9642a07a93f8333d491f4c52bc70068e42f51685df9ac1a57"}, - {file = "Cython-0.29.36-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:1ef90023da8a9bf84cf16f06186db0906d2ce52a09f751e2cb9d3da9d54eae46"}, - {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9deef0761e8c798043dbb728a1c6df97b26e5edc65b8d6c7608b3c07af3eb722"}, - {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:69af2365de2343b4e5a61c567e7611ddf2575ae6f6e5c01968f7d4f2747324eb"}, - {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:fdf377b0f6e9325b73ad88933136023184afdc795caeeaaf3dca13494cffd15e"}, - {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ff2cc5518558c598028ae8d9a43401e0e734b74b6e598156b005328c9da3472"}, - {file = "Cython-0.29.36-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7ca921068242cd8b52544870c807fe285c1f248b12df7b6dfae25cc9957b965e"}, - {file = "Cython-0.29.36-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6058a6d04e04d790cda530e1ff675e9352359eb4b777920df3cac2b62a9a030f"}, - {file = "Cython-0.29.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:de2045ceae1857e56a72f08e0acfa48c994277a353b7bdab1f097db9f8803f19"}, - {file = "Cython-0.29.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9f2a4b4587aaef08815410dc20653613ca04a120a2954a92c39e37c6b5fdf6be"}, - {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:2edd9f8edca69178d74cbbbc180bc3e848433c9b7dc80374a11a0bb0076c926d"}, - {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c6c0aea8491a70f98b7496b5057c9523740e02cec21cd678eef609d2aa6c1257"}, - {file = "Cython-0.29.36-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:768f65b16d23c630d8829ce1f95520ef1531a9c0489fa872d87c8c3813f65aee"}, - {file = "Cython-0.29.36-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:568625e8274ee7288ad87b0f615ec36ab446ca9b35e77481ed010027d99c7020"}, - {file = "Cython-0.29.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bdc0a4cb99f55e6878d4b67a4bfee23823484915cb6b7e9c9dd01002dd3592ea"}, - {file = "Cython-0.29.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f0df6552be39853b10dfb5a10dbd08f5c49023d6b390d7ce92d4792a8b6e73ee"}, - {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:8894db6f5b6479a3c164e0454e13083ebffeaa9a0822668bb2319bdf1b783df1"}, - {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53f93a8c342e9445a8f0cb7039775294f2dbbe5241936573daeaf0afe30397e4"}, - {file = "Cython-0.29.36-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ee317f9bcab901a3db39c34ee5a27716f7132e5c0de150125342694d18b30f51"}, - {file = "Cython-0.29.36-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4b8269e5a5d127a2191b02b9df3636c0dac73f14f1ff8a831f39cb5197c4f38"}, - {file = "Cython-0.29.36-py2.py3-none-any.whl", hash = "sha256:95bb13d8be507425d03ebe051f90d4b2a9fdccc64e4f30b35645fdb7542742eb"}, - {file = "Cython-0.29.36.tar.gz", hash = "sha256:41c0cfd2d754e383c9eeb95effc9aa4ab847d0c9747077ddd7c0dcb68c3bc01f"}, +version = "3.0.7" +description = "The Cython compiler for writing C extensions in the Python language." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Cython-3.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3c0e19bb41de6be9d8afc85795159ca16296be81a586cd9588be0400d44a855"}, + {file = "Cython-3.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e8bf00ec1dd1d92e9ae74d2e6891f087a939e1dfb40c9c7fa5d8d6a26c94f5a"}, + {file = "Cython-3.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd6ae43ef2e596c9a88dbf2a8895be2e32cc2f5bc3c8ba2e7753b69068fc0b2d"}, + {file = "Cython-3.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f674be92673e87dd8ee7cfe553d5960ec4effc5ab15063b9a5e265a51585a31a"}, + {file = "Cython-3.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:861cf254bf5836d47c2aee86aa75dd93d3de00ccd1b077c3c7a2bb22cba358e7"}, + {file = "Cython-3.0.7-cp310-cp310-win32.whl", hash = "sha256:f6d8ff62ad55dc0393686438eac4b457a916e4d1118a0b550746bb52b4c756cc"}, + {file = "Cython-3.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:e13abb14843397b76d0472c7d33cd260d5f262ab05cc27ed423317e645e29643"}, + {file = "Cython-3.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c636c9ab92c7838231a1ba769e519d953af8294612f3f772a54d3a5250ff23f"}, + {file = "Cython-3.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d2a684122dfb531853d57c8c85c1d5d44be709e12466dca99fa6aee7d8054f"}, + {file = "Cython-3.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1bdf8a107fdf9e174991aa87a0be7504f60de1ec6bfb1ccfb30e33acac818a0"}, + {file = "Cython-3.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a83e04fde663b84905f3a20213a4333d13a07b79434300704b70dc552761f8b"}, + {file = "Cython-3.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e34b4b08d795ccca920fa26b099558f4f1e4e3f794e4ba8d3433c5bc2454d50a"}, + {file = "Cython-3.0.7-cp311-cp311-win32.whl", hash = "sha256:133057ac45b6fa7fe5d7baada9d3545d09339432f75c0545f556e8c6fecc2932"}, + {file = "Cython-3.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b65abca78aa5ebc8675c8480b9a53006f6efea9910ad099cf32c9fb5617ef251"}, + {file = "Cython-3.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ceac5315fe899c229e874328742154e331fa41337bb03f6f5264636c351c9e"}, + {file = "Cython-3.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea936cf5931297ba07bce121388c4c6266c1b63a9f4d648ae16c92ff090204b"}, + {file = "Cython-3.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fcd9a18ee3ac7f460e0841954feb495102ffbdbec0e6c78562f3495cda000dd"}, + {file = "Cython-3.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7c8d579d13cb81abe704c8b0908d122b81d6e2623265a19c4a6a7377f440debb"}, + {file = "Cython-3.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ef5bb0268bfe5992da3ef9292463a5a895ed8700b134ed2c00008d5471b3ba6e"}, + {file = "Cython-3.0.7-cp312-cp312-win32.whl", hash = "sha256:55f93d3822bc196b37a8bdfa4ec6a35232a399e97f2baa714bd5ed8ea9b0ce68"}, + {file = "Cython-3.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f3845c4506e0d207c5e268fb02813928f3a1e135de954a379f165ef0d581da47"}, + {file = "Cython-3.0.7-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ad7c2303a338b2c0b6c6c68f101a6768725934538756096cf3388a5c07a7525"}, + {file = "Cython-3.0.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed25959e4025870fdde5f895fcb126196d22affd4f4fad85a2823e0dddc85b0"}, + {file = "Cython-3.0.7-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79868ec74e4907a8a6e63effe13547c6157f196a162920b1de066da5849ffb8e"}, + {file = "Cython-3.0.7-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5e3a038332973b12e72236e8884dc99601a840334c2c46cfbbb5851cb94166eb"}, + {file = "Cython-3.0.7-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f2602a5c97a3d618b3b847514204ef3349fb414c59e1126c0c2c708d2c5680f8"}, + {file = "Cython-3.0.7-cp36-cp36m-win32.whl", hash = "sha256:539ad5a21141e6420035cf616bcba48d999bf878839e52692f97fc7e2f16265c"}, + {file = "Cython-3.0.7-cp36-cp36m-win_amd64.whl", hash = "sha256:848a28ea49166454c3bff927e5a47629eecf1aa755d6fb3290569cba0fc93766"}, + {file = "Cython-3.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82f27a0134fc6bb46032ca5f728d8af984f3be94a3cb01cb70ff1224e551b9cf"}, + {file = "Cython-3.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f20c61114c7948cf1214585066406cef4b54a9b935160980e0b6e70ada3a69"}, + {file = "Cython-3.0.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d51709e10ad6213b4bf094af7be7ff82bab43216b3c92a07d05b451deeca79"}, + {file = "Cython-3.0.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3f02c7240abab48d59f0d5fef7064f18f01a2a204616165fa6367a8abf5a8832"}, + {file = "Cython-3.0.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:225f8bba6428b8d711ca2d6c738d2e3a4667f6a2ae40f8a7a5256f69f6a3600e"}, + {file = "Cython-3.0.7-cp37-cp37m-win32.whl", hash = "sha256:30eb2d2938b9195e2c82951713429aff3ad1be9f104437d1536a04eb0cb3dc0e"}, + {file = "Cython-3.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:167b3f3894dcc697cefefac1d198304fae8eb4d5860a7b8bc2459d572e838470"}, + {file = "Cython-3.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c67105f2c6ccf5b3adbcfaecf3c5c9fa8940f9f97955c9ad7d2542151d97d93"}, + {file = "Cython-3.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a1859af761977530df2cd5c36e31d54e8d6708ad2c4656e7125c482364dc216"}, + {file = "Cython-3.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01b94304aab87496e81d1f546e71abf57b430b39be4269df1cd7da9928d70b5b"}, + {file = "Cython-3.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:931aade65f77cf59f2a702ac1f549a4836ce221107c740502cbad18d6d8e9511"}, + {file = "Cython-3.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:812b193c26553f1f375d4f1c50f805c227b24ed2d595bc9cdaf78c992ecc64a4"}, + {file = "Cython-3.0.7-cp38-cp38-win32.whl", hash = "sha256:b227643d8a40b68554dc7d37fcd03fc97b4fb0bd2614aeb5f2e07ab244642d36"}, + {file = "Cython-3.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:0d8a98c7d86ac4d05b251c39faf49423780381aab55fbf2e147f6e006a34a58a"}, + {file = "Cython-3.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816f5285d596062c7ef22790de7d75354b58d4417a9fc64cba914aeeb900db0b"}, + {file = "Cython-3.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d0dae6dccd349b8ccf197c10ef2d05c711ca36a649c7eddbab1de2c90b63a1"}, + {file = "Cython-3.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13211b67b29f6ed8e87c137496c73d93aff0330d97940b4fbed72eae37a4a2a0"}, + {file = "Cython-3.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1853bc34ced5ff6473e881fcf6de29da83262552c8f268a0df53b49c2b89e2c"}, + {file = "Cython-3.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:51e8164b1270625ff101e95c3c1c234421520c07a0a3a20ded9e9431d98afce7"}, + {file = "Cython-3.0.7-cp39-cp39-win32.whl", hash = "sha256:45319d2471f4dbf19893ca53785a421107266e18b8cccd2054fce1e3f72a85f1"}, + {file = "Cython-3.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:612d83fd1eb5aaa5401a755c1f1aafacd9dab404cd350b90d5f404c98b33e4b3"}, + {file = "Cython-3.0.7-py2.py3-none-any.whl", hash = "sha256:936ec37b261b226d7404eff23a9aad284098338150d42a53d6a9af12b18d3892"}, + {file = "Cython-3.0.7.tar.gz", hash = "sha256:fb299acf3a578573c190c858d49e0cf9d75f4bc49c3f24c5a63804997ef09213"}, ] [[package]] @@ -1197,38 +1186,38 @@ files = [ [[package]] name = "mypy" -version = "1.6.0" +version = "1.8.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0"}, - {file = "mypy-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531"}, - {file = "mypy-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41"}, - {file = "mypy-1.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c"}, - {file = "mypy-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182"}, - {file = "mypy-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a"}, - {file = "mypy-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425"}, - {file = "mypy-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8"}, - {file = "mypy-1.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60"}, - {file = "mypy-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead"}, - {file = "mypy-1.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f"}, - {file = "mypy-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566"}, - {file = "mypy-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad"}, - {file = "mypy-1.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13"}, - {file = "mypy-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17"}, - {file = "mypy-1.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a"}, - {file = "mypy-1.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2"}, - {file = "mypy-1.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6"}, - {file = "mypy-1.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed"}, - {file = "mypy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323"}, - {file = "mypy-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67"}, - {file = "mypy-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f"}, - {file = "mypy-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f"}, - {file = "mypy-1.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf"}, - {file = "mypy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849"}, - {file = "mypy-1.6.0-py3-none-any.whl", hash = "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc"}, - {file = "mypy-1.6.0.tar.gz", hash = "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, ] [package.dependencies] @@ -1238,6 +1227,7 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] +mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[package]] @@ -1267,36 +1257,47 @@ setuptools = "*" [[package]] name = "numpy" -version = "1.25.2" +version = "1.26.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, - {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, - {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, - {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, - {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, - {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, - {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, - {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, - {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, - {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, - {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, - {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, - {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, - {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, - {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, - {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, - {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, - {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, ] [[package]] @@ -1514,17 +1515,6 @@ files = [ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] - [[package]] name = "pycodestyle" version = "2.11.0" @@ -1750,27 +1740,23 @@ lint = ["black (>=19.10b0,<=22.10)", "flake8 (>=3.7,<5)", "flake8-bugbear", "iso [[package]] name = "pytest" -version = "6.2.5" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "python-multipart" @@ -1847,6 +1833,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2188,16 +2175,19 @@ anyio = ">=3.4.0,<5" full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" +name = "syrupy" +version = "4.6.0" +description = "Pytest Snapshot Test Utility" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.8.1,<4" files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, + {file = "syrupy-4.6.0-py3-none-any.whl", hash = "sha256:747aae1bcf3cb3249e33b1e6d81097874d23615982d5686ebe637875b0775a1b"}, + {file = "syrupy-4.6.0.tar.gz", hash = "sha256:231b1f5d00f1f85048ba81676c79448076189c4aef4d33f21ae32f3b4c565a54"}, ] +[package.dependencies] +pytest = ">=7.0.0,<8.0.0" + [[package]] name = "tomlkit" version = "0.12.1" @@ -2240,6 +2230,17 @@ files = [ {file = "trove_classifiers-2023.8.7-py3-none-any.whl", hash = "sha256:a676626a31286130d56de2ea1232484df97c567eb429d56cfcb0637e681ecf09"}, ] +[[package]] +name = "types-pyyaml" +version = "6.0.12.12" +description = "Typing stubs for PyYAML" +optional = false +python-versions = "*" +files = [ + {file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"}, + {file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"}, +] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -2431,4 +2432,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "b3ef9f8c5445b3e481d666a4a3b6a73d44fa1159646cf64f480a19aa1999d0ee" +content-hash = "4635ad235914ef05225525233ce0723226417491de9f2551112682c707921365" diff --git a/pyproject.toml b/pyproject.toml index 88926aa0a..9d0db2df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,20 +5,23 @@ version = "0.10.5" enable_black = true enable_flake8 = true enable_isort = true -enable_mypy = false # TODO: eliminate errors and enable at CI -mypy_preset = "entry" # TODO: "strict" +enable_mypy = true +mypy_preset = "entry" # TODO: "strict" +mypy_plugins = [ + { function = "numpy.typing.mypy_plugin" }, + { function = "pydantic.mypy" }, +] line_length = 88 py_version = "py311" isort_known_first_party = ["voicevox_engine"] isort_known_third_party = ["numpy"] [[tool.pysen.lint.mypy_targets]] - paths = [".", "voicevox_engine/"] +paths = ["."] [tool.black] # automatically generated by pysen # pysen ignores and overwrites any modifications line-length = 88 -target-version = ["py310", "py311"] - +target-version = ["py311"] [tool.isort] # automatically generated by pysen # pysen ignores and overwrites any modifications @@ -45,23 +48,18 @@ numpy = "^1.20.0" fastapi = "^0.103.2" python-multipart = "^0.0.5" uvicorn = "^0.15.0" -aiofiles = "^0.7.0" soundfile = "^0.12.1" pyyaml = "^6.0" pyworld = "^0.3.0" -requests = "^2.28.1" jinja2 = "^3.1.2" -pyopenjtalk = {git = "https://github.com/VOICEVOX/pyopenjtalk", rev = "b35fc89fe42948a28e33aed886ea145a51113f88"} +pyopenjtalk = { git = "https://github.com/VOICEVOX/pyopenjtalk", rev = "b35fc89fe42948a28e33aed886ea145a51113f88" } semver = "^3.0.0" platformdirs = "^3.10.0" soxr = "^0.3.6" [tool.poetry.group.dev.dependencies] -cython = "^0.29.34,>=0.29.33" # NOTE: for Python 3.11 pyinstaller = "^5.13" pre-commit = "^2.16.0" -atomicwrites = "^1.4.0" -colorama = "^0.4.4" poetry = "^1.3.1" [tool.poetry.group.test.dependencies] @@ -70,11 +68,13 @@ black = "^22.12.0" flake8-bugbear = "^23.1.0" flake8 = "^6.0.0" isort = "^5.12.0" -mypy = "^1.6.0" -pytest = "^6.2.5" +mypy = "^1.8.0" +pytest = "^7.4.3" coveralls = "^3.2.0" poetry = "^1.3.1" -httpx = "^0.25.0" # NOTE: required by fastapi.testclient.TestClient +httpx = "^0.25.0" # NOTE: required by fastapi.testclient.TestClient +syrupy = "^4.6.0" +types-pyyaml = "^6.0" [tool.poetry.group.license.dependencies] pip-licenses = "^4.2.0" diff --git a/requirements-dev.txt b/requirements-dev.txt index ca5414d46..9b274bec2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,6 @@ -aiofiles==0.7.0 ; python_version >= "3.11" and python_version < "3.12" altgraph==0.17.3 ; python_version >= "3.11" and python_version < "3.12" anyio==3.7.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.7.2 ; python_version >= "3.11" and python_version < "3.12" -atomicwrites==1.4.1 ; python_version >= "3.11" and python_version < "3.12" attrs==23.1.0 ; python_version >= "3.11" and python_version < "3.12" build==0.10.0 ; python_version >= "3.11" and python_version < "3.12" cachecontrol[filecache]==0.13.1 ; python_version >= "3.11" and python_version < "3.12" @@ -12,10 +10,10 @@ cfgv==3.4.0 ; python_version >= "3.11" and python_version < "3.12" charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "3.12" cleo==2.0.1 ; python_version >= "3.11" and python_version < "3.12" click==8.1.7 ; python_version >= "3.11" and python_version < "3.12" -colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" +colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and (platform_system == "Windows" or os_name == "nt") crashtest==0.4.1 ; python_version >= "3.11" and python_version < "3.12" cryptography==41.0.3 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "linux" -cython==0.29.36 ; python_version >= "3.11" and python_version < "3.12" +cython==3.0.7 ; python_version >= "3.11" and python_version < "3.12" distlib==0.3.7 ; python_version >= "3.11" and python_version < "3.12" dulwich==0.21.5 ; python_version >= "3.11" and python_version < "3.12" fastapi==0.103.2 ; python_version >= "3.11" and python_version < "3.12" @@ -36,7 +34,7 @@ markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "3.12" more-itertools==10.1.0 ; python_version >= "3.11" and python_version < "3.12" msgpack==1.0.5 ; python_version >= "3.11" and python_version < "3.12" nodeenv==1.8.0 ; python_version >= "3.11" and python_version < "3.12" -numpy==1.25.2 ; python_version >= "3.11" and python_version < "3.12" +numpy==1.26.2 ; python_version >= "3.11" and python_version < "3.12" packaging==23.1 ; python_version >= "3.11" and python_version < "3.12" pefile==2023.2.7 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" pexpect==4.8.0 ; python_version >= "3.11" and python_version < "3.12" diff --git a/requirements-license.txt b/requirements-license.txt index 9d58db2b5..6da0790fc 100644 --- a/requirements-license.txt +++ b/requirements-license.txt @@ -1,18 +1,15 @@ -aiofiles==0.7.0 ; python_version >= "3.11" and python_version < "3.12" anyio==3.7.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.7.2 ; python_version >= "3.11" and python_version < "3.12" -certifi==2023.7.22 ; python_version >= "3.11" and python_version < "3.12" cffi==1.15.1 ; python_version >= "3.11" and python_version < "3.12" -charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "3.12" click==8.1.7 ; python_version >= "3.11" and python_version < "3.12" colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and platform_system == "Windows" -cython==0.29.36 ; python_version >= "3.11" and python_version < "3.12" +cython==3.0.7 ; python_version >= "3.11" and python_version < "3.12" fastapi==0.103.2 ; python_version >= "3.11" and python_version < "3.12" h11==0.14.0 ; python_version >= "3.11" and python_version < "3.12" idna==3.4 ; python_version >= "3.11" and python_version < "3.12" jinja2==3.1.2 ; python_version >= "3.11" and python_version < "3.12" markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "3.12" -numpy==1.25.2 ; python_version >= "3.11" and python_version < "3.12" +numpy==1.26.2 ; python_version >= "3.11" and python_version < "3.12" pip-licenses==4.3.2 ; python_version >= "3.11" and python_version < "3.12" platformdirs==3.10.0 ; python_version >= "3.11" and python_version < "3.12" prettytable==3.8.0 ; python_version >= "3.11" and python_version < "3.12" @@ -22,7 +19,6 @@ pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@b35fc89fe42948a28e33ae python-multipart==0.0.5 ; python_version >= "3.11" and python_version < "3.12" pyworld==0.3.4 ; python_version >= "3.11" and python_version < "3.12" pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.12" -requests==2.31.0 ; python_version >= "3.11" and python_version < "3.12" semver==3.0.1 ; python_version >= "3.11" and python_version < "3.12" six==1.16.0 ; python_version >= "3.11" and python_version < "3.12" sniffio==1.3.0 ; python_version >= "3.11" and python_version < "3.12" @@ -31,6 +27,5 @@ soxr==0.3.6 ; python_version >= "3.11" and python_version < "3.12" starlette==0.27.0 ; python_version >= "3.11" and python_version < "3.12" tqdm==4.66.1 ; python_version >= "3.11" and python_version < "3.12" typing-extensions==4.7.1 ; python_version >= "3.11" and python_version < "3.12" -urllib3==2.0.4 ; python_version >= "3.11" and python_version < "3.12" uvicorn==0.15.0 ; python_version >= "3.11" and python_version < "3.12" wcwidth==0.2.6 ; python_version >= "3.11" and python_version < "3.12" diff --git a/requirements-test.txt b/requirements-test.txt index c03f1b185..e3802c18d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,5 @@ -aiofiles==0.7.0 ; python_version >= "3.11" and python_version < "3.12" anyio==3.7.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.7.2 ; python_version >= "3.11" and python_version < "3.12" -atomicwrites==1.4.1 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" attrs==23.1.0 ; python_version >= "3.11" and python_version < "3.12" black==22.12.0 ; python_version >= "3.11" and python_version < "3.12" build==0.10.0 ; python_version >= "3.11" and python_version < "3.12" @@ -17,7 +15,7 @@ coverage==6.5.0 ; python_version >= "3.11" and python_version < "3.12" coveralls==3.3.1 ; python_version >= "3.11" and python_version < "3.12" crashtest==0.4.1 ; python_version >= "3.11" and python_version < "3.12" cryptography==41.0.3 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "linux" -cython==0.29.36 ; python_version >= "3.11" and python_version < "3.12" +cython==3.0.7 ; python_version >= "3.11" and python_version < "3.12" dacite==1.8.1 ; python_version >= "3.11" and python_version < "3.12" distlib==0.3.7 ; python_version >= "3.11" and python_version < "3.12" docopt==0.6.2 ; python_version >= "3.11" and python_version < "3.12" @@ -47,8 +45,8 @@ mccabe==0.7.0 ; python_version >= "3.11" and python_version < "3.12" more-itertools==10.1.0 ; python_version >= "3.11" and python_version < "3.12" msgpack==1.0.5 ; python_version >= "3.11" and python_version < "3.12" mypy-extensions==1.0.0 ; python_version >= "3.11" and python_version < "3.12" -mypy==1.6.0 ; python_version >= "3.11" and python_version < "3.12" -numpy==1.25.2 ; python_version >= "3.11" and python_version < "3.12" +mypy==1.8.0 ; python_version >= "3.11" and python_version < "3.12" +numpy==1.26.2 ; python_version >= "3.11" and python_version < "3.12" packaging==23.1 ; python_version >= "3.11" and python_version < "3.12" pathspec==0.11.2 ; python_version >= "3.11" and python_version < "3.12" pexpect==4.8.0 ; python_version >= "3.11" and python_version < "3.12" @@ -59,7 +57,6 @@ poetry-core==1.7.0 ; python_version >= "3.11" and python_version < "3.12" poetry-plugin-export==1.5.0 ; python_version >= "3.11" and python_version < "3.12" poetry==1.6.1 ; python_version >= "3.11" and python_version < "3.12" ptyprocess==0.7.0 ; python_version >= "3.11" and python_version < "3.12" -py==1.11.0 ; python_version >= "3.11" and python_version < "3.12" pycodestyle==2.11.0 ; python_version >= "3.11" and python_version < "3.12" pycparser==2.21 ; python_version >= "3.11" and python_version < "3.12" pydantic==1.10.12 ; python_version >= "3.11" and python_version < "3.12" @@ -68,7 +65,7 @@ pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@b35fc89fe42948a28e33ae pyproject-hooks==1.0.0 ; python_version >= "3.11" and python_version < "3.12" pyrsistent==0.19.3 ; python_version >= "3.11" and python_version < "3.12" pysen==0.10.5 ; python_version >= "3.11" and python_version < "3.12" -pytest==6.2.5 ; python_version >= "3.11" and python_version < "3.12" +pytest==7.4.3 ; python_version >= "3.11" and python_version < "3.12" python-multipart==0.0.5 ; python_version >= "3.11" and python_version < "3.12" pywin32-ctypes==0.2.2 ; python_version >= "3.11" and python_version < "3.12" and sys_platform == "win32" pyworld==0.3.4 ; python_version >= "3.11" and python_version < "3.12" @@ -85,10 +82,11 @@ sniffio==1.3.0 ; python_version >= "3.11" and python_version < "3.12" soundfile==0.12.1 ; python_version >= "3.11" and python_version < "3.12" soxr==0.3.6 ; python_version >= "3.11" and python_version < "3.12" starlette==0.27.0 ; python_version >= "3.11" and python_version < "3.12" -toml==0.10.2 ; python_version >= "3.11" and python_version < "3.12" +syrupy==4.6.0 ; python_version >= "3.11" and python_version < "3.12" tomlkit==0.12.1 ; python_version >= "3.11" and python_version < "3.12" tqdm==4.66.1 ; python_version >= "3.11" and python_version < "3.12" trove-classifiers==2023.8.7 ; python_version >= "3.11" and python_version < "3.12" +types-pyyaml==6.0.12.12 ; python_version >= "3.11" and python_version < "3.12" typing-extensions==4.7.1 ; python_version >= "3.11" and python_version < "3.12" unidiff==0.7.5 ; python_version >= "3.11" and python_version < "3.12" urllib3==2.0.4 ; python_version >= "3.11" and python_version < "3.12" diff --git a/requirements.txt b/requirements.txt index 3eaf3a2a0..ed40a3faa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,16 @@ -aiofiles==0.7.0 ; python_version >= "3.11" and python_version < "3.12" anyio==3.7.1 ; python_version >= "3.11" and python_version < "3.12" asgiref==3.7.2 ; python_version >= "3.11" and python_version < "3.12" -certifi==2023.7.22 ; python_version >= "3.11" and python_version < "3.12" cffi==1.15.1 ; python_version >= "3.11" and python_version < "3.12" -charset-normalizer==3.2.0 ; python_version >= "3.11" and python_version < "3.12" click==8.1.7 ; python_version >= "3.11" and python_version < "3.12" colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.12" and platform_system == "Windows" -cython==0.29.36 ; python_version >= "3.11" and python_version < "3.12" +cython==3.0.7 ; python_version >= "3.11" and python_version < "3.12" fastapi==0.103.2 ; python_version >= "3.11" and python_version < "3.12" h11==0.14.0 ; python_version >= "3.11" and python_version < "3.12" idna==3.4 ; python_version >= "3.11" and python_version < "3.12" jinja2==3.1.2 ; python_version >= "3.11" and python_version < "3.12" ko2kana==1.8 ; python_version >= "3.11" and python_version < "3.12" markupsafe==2.1.3 ; python_version >= "3.11" and python_version < "3.12" -numpy==1.25.2 ; python_version >= "3.11" and python_version < "3.12" +numpy==1.26.2 ; python_version >= "3.11" and python_version < "3.12" platformdirs==3.10.0 ; python_version >= "3.11" and python_version < "3.12" pycparser==2.21 ; python_version >= "3.11" and python_version < "3.12" pydantic==1.10.12 ; python_version >= "3.11" and python_version < "3.12" @@ -21,7 +18,6 @@ pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@b35fc89fe42948a28e33ae python-multipart==0.0.5 ; python_version >= "3.11" and python_version < "3.12" pyworld==0.3.4 ; python_version >= "3.11" and python_version < "3.12" pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "3.12" -requests==2.31.0 ; python_version >= "3.11" and python_version < "3.12" semver==3.0.1 ; python_version >= "3.11" and python_version < "3.12" six==1.16.0 ; python_version >= "3.11" and python_version < "3.12" sniffio==1.3.0 ; python_version >= "3.11" and python_version < "3.12" @@ -30,5 +26,4 @@ soxr==0.3.6 ; python_version >= "3.11" and python_version < "3.12" starlette==0.27.0 ; python_version >= "3.11" and python_version < "3.12" tqdm==4.66.1 ; python_version >= "3.11" and python_version < "3.12" typing-extensions==4.7.1 ; python_version >= "3.11" and python_version < "3.12" -urllib3==2.0.4 ; python_version >= "3.11" and python_version < "3.12" uvicorn==0.15.0 ; python_version >= "3.11" and python_version < "3.12" diff --git a/run.py b/run.py index 1850e6623..ec2917b8c 100644 --- a/run.py +++ b/run.py @@ -7,42 +7,50 @@ import re import sys import traceback -import warnings import zipfile +from collections.abc import Awaitable, Callable from functools import lru_cache from io import BytesIO, TextIOWrapper from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryFile -from typing import Any, Dict, List, Optional +from typing import Annotated, Literal, Optional import soundfile import uvicorn -from fastapi import FastAPI, Form, HTTPException, Query, Request, Response +from fastapi import Depends, FastAPI, Form, HTTPException, Query, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse from fastapi.templating import Jinja2Templates from ko2kana import toKana -from pydantic import ValidationError, conint -from starlette.background import BackgroundTask +from pydantic import ValidationError, parse_obj_as +from starlette.middleware.errors import ServerErrorMiddleware from starlette.responses import FileResponse from voicevox_engine import __version__ from voicevox_engine.cancellable_engine import CancellableEngine -from voicevox_engine.engine_manifest import EngineManifestLoader +from voicevox_engine.core.core_adapter import CoreAdapter +from voicevox_engine.core.core_initializer import initialize_cores from voicevox_engine.engine_manifest.EngineManifest import EngineManifest -from voicevox_engine.kana_parser import create_kana, parse_kana +from voicevox_engine.engine_manifest.EngineManifestLoader import EngineManifestLoader from voicevox_engine.library_manager import LibraryManager -from voicevox_engine.metas.MetasStore import MetasStore, construct_lookup +from voicevox_engine.metas.Metas import StyleId +from voicevox_engine.metas.MetasStore import ( + MetasStore, + construct_lookup, + filter_speakers_and_styles, +) from voicevox_engine.model import ( AccentPhrase, AudioQuery, BaseLibraryInfo, DownloadableLibraryInfo, + FrameAudioQuery, InstalledLibraryInfo, MorphableTargetInfo, ParseKanaBadRequest, ParseKanaError, + Score, Speaker, SpeakerInfo, StyleIdNotFoundError, @@ -59,16 +67,18 @@ from voicevox_engine.morphing import ( synthesis_morphing_parameter as _synthesis_morphing_parameter, ) -from voicevox_engine.part_of_speech_data import MAX_PRIORITY, MIN_PRIORITY -from voicevox_engine.preset import Preset, PresetError, PresetManager -from voicevox_engine.setting import ( - USER_SETTING_PATH, - CorsPolicyMode, - Setting, - SettingLoader, +from voicevox_engine.preset.Preset import Preset +from voicevox_engine.preset.PresetError import PresetError +from voicevox_engine.preset.PresetManager import PresetManager +from voicevox_engine.setting.Setting import CorsPolicyMode, Setting +from voicevox_engine.setting.SettingLoader import USER_SETTING_PATH, SettingHandler +from voicevox_engine.tts_pipeline.kana_converter import create_kana, parse_kana +from voicevox_engine.tts_pipeline.tts_engine import ( + TTSEngine, + make_tts_engines_from_cores, ) -from voicevox_engine.synthesis_engine import SynthesisEngineBase, make_synthesis_engines -from voicevox_engine.user_dict import ( +from voicevox_engine.user_dict.part_of_speech_data import MAX_PRIORITY, MIN_PRIORITY +from voicevox_engine.user_dict.user_dict import ( apply_word, delete_word, import_user_dict, @@ -76,29 +86,13 @@ rewrite_word, update_dict, ) -from voicevox_engine.utility import ( +from voicevox_engine.utility.connect_base64_waves import ( ConnectBase64WavesException, connect_base64_waves, - delete_file, - engine_root, - get_latest_core_version, - get_save_dir, ) - - -def get_style_id_from_deprecated(style_id: int | None, speaker_id: int | None) -> int: - """ - style_idとspeaker_id両方ともNoneかNoneでないかをチェックし、 - どちらか片方しかNoneが存在しなければstyle_idを返す - """ - if speaker_id is not None and style_id is None: - warnings.warn("speakerは非推奨です。style_idを利用してください。", stacklevel=1) - return speaker_id - elif style_id is not None and speaker_id is None: - return style_id - raise HTTPException( - status_code=400, detail="speakerとstyle_idが両方とも存在しないか、両方とも存在しています。" - ) +from voicevox_engine.utility.core_version_utility import get_latest_core_version +from voicevox_engine.utility.path_utility import delete_file, engine_root, get_save_dir +from voicevox_engine.utility.run_utility import decide_boolean_from_env def b64encode_str(s): @@ -111,47 +105,64 @@ def set_output_log_utf8() -> None: """ # コンソールがない環境だとNone https://docs.python.org/ja/3/library/sys.html#sys.__stdin__ if sys.stdout is not None: - # 必ずしもreconfigure()が実装されているとは限らない - try: + if isinstance(sys.stdout, TextIOWrapper): sys.stdout.reconfigure(encoding="utf-8") - except AttributeError: + else: # バッファを全て出力する sys.stdout.flush() - sys.stdout = TextIOWrapper( - sys.stdout.buffer, encoding="utf-8", errors="backslashreplace" - ) + try: + sys.stdout = TextIOWrapper( + sys.stdout.buffer, encoding="utf-8", errors="backslashreplace" + ) + except AttributeError: + # stdout.bufferがない場合は無視 + pass if sys.stderr is not None: - try: + if isinstance(sys.stderr, TextIOWrapper): sys.stderr.reconfigure(encoding="utf-8") - except AttributeError: + else: sys.stderr.flush() - sys.stderr = TextIOWrapper( - sys.stderr.buffer, encoding="utf-8", errors="backslashreplace" - ) + try: + sys.stderr = TextIOWrapper( + sys.stderr.buffer, encoding="utf-8", errors="backslashreplace" + ) + except AttributeError: + # stderr.bufferがない場合は無視 + pass def generate_app( - synthesis_engines: Dict[str, SynthesisEngineBase], + tts_engines: dict[str, TTSEngine], + cores: dict[str, CoreAdapter], latest_core_version: str, - setting_loader: SettingLoader, + setting_loader: SettingHandler, preset_manager: PresetManager, experimental_katakana_transcription: bool, cancellable_engine: CancellableEngine | None = None, root_dir: Optional[Path] = None, cors_policy_mode: CorsPolicyMode = CorsPolicyMode.localapps, - allow_origin: Optional[List[str]] = None, + allow_origin: Optional[list[str]] = None, + disable_mutable_api: bool = False, ) -> FastAPI: if root_dir is None: root_dir = engine_root() - default_sampling_rate = synthesis_engines[latest_core_version].default_sampling_rate - app = FastAPI( title="VOICEVOX Engine", description="VOICEVOXの音声合成エンジンです。", version=__version__, ) + # 未処理の例外が発生するとCORSMiddlewareが適用されない問題に対するワークアラウンド + # ref: https://github.com/VOICEVOX/voicevox_engine/issues/91 + async def global_execution_handler(request: Request, exc: Exception) -> Response: + return JSONResponse( + status_code=500, + content="Internal Server Error", + ) + + app.add_middleware(ServerErrorMiddleware, handler=global_execution_handler) + # CORS用のヘッダを生成するミドルウェア localhost_regex = "^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?$" compiled_localhost_regex = re.compile(localhost_regex) @@ -178,7 +189,9 @@ def generate_app( # 許可されていないOriginを遮断するミドルウェア @app.middleware("http") - async def block_origin_middleware(request: Request, call_next): + async def block_origin_middleware( + request: Request, call_next: Callable[[Request], Awaitable[Response]] + ) -> Response | JSONResponse: isValidOrigin: bool = False if "Origin" not in request.headers: # Originのない純粋なリクエストの場合 isValidOrigin = True @@ -198,6 +211,11 @@ async def block_origin_middleware(request: Request, call_next): status_code=403, content={"detail": "Origin not allowed"} ) + # 許可されていないAPIを無効化する + def check_disabled_mutable_api(): + if disable_mutable_api: + raise HTTPException(status_code=403, detail="エンジンの静的なデータを変更するAPIは無効化されています") + engine_manifest_data = EngineManifestLoader( engine_root() / "engine_manifest.json", engine_root() ).load_manifest() @@ -211,7 +229,11 @@ async def block_origin_middleware(request: Request, call_next): metas_store = MetasStore(root_dir / "speaker_info") - setting_ui_template = Jinja2Templates(directory=engine_root() / "ui_template") + setting_ui_template = Jinja2Templates( + directory=engine_root() / "ui_template", + variable_start_string="", + variable_end_string="", + ) # キャッシュを有効化 # モジュール側でlru_cacheを指定するとキャッシュを制御しにくいため、HTTPサーバ側で指定する @@ -228,11 +250,19 @@ async def block_origin_middleware(request: Request, call_next): def apply_user_dict(): update_dict() - def get_engine(core_version: Optional[str]) -> SynthesisEngineBase: + def get_engine(core_version: Optional[str]) -> TTSEngine: + if core_version is None: + return tts_engines[latest_core_version] + if core_version in tts_engines: + return tts_engines[core_version] + raise HTTPException(status_code=422, detail="不明なバージョンです") + + def get_core(core_version: Optional[str]) -> CoreAdapter: + """指定したバージョンのコアを取得する""" if core_version is None: - return synthesis_engines[latest_core_version] - if core_version in synthesis_engines: - return synthesis_engines[core_version] + return cores[latest_core_version] + if core_version in cores: + return cores[core_version] raise HTTPException(status_code=422, detail="不明なバージョンです") @app.post( @@ -243,18 +273,17 @@ def get_engine(core_version: Optional[str]) -> SynthesisEngineBase: ) def audio_query( text: str, - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> AudioQuery: """ - クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 + 音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 """ - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) + core = get_core(core_version) if experimental_katakana_transcription: text = toKana(text).replace(" ", "") - accent_phrases = engine.create_accent_phrases(text, style_id=style_id) + accent_phrases = engine.create_accent_phrases(text, style_id) return AudioQuery( accent_phrases=accent_phrases, speedScale=1, @@ -263,7 +292,7 @@ def audio_query( volumeScale=1, prePhonemeLength=0.1, postPhonemeLength=0.1, - outputSamplingRate=default_sampling_rate, + outputSamplingRate=core.default_sampling_rate, outputStereo=False, kana=create_kana(accent_phrases), ) @@ -280,9 +309,10 @@ def audio_query_from_preset( core_version: str | None = None, ) -> AudioQuery: """ - クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 + 音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 """ engine = get_engine(core_version) + core = get_core(core_version) try: presets = preset_manager.load_presets() except PresetError as err: @@ -296,9 +326,7 @@ def audio_query_from_preset( if experimental_katakana_transcription: text = toKana(text).replace(" ", "") - accent_phrases = engine.create_accent_phrases( - text, style_id=selected_preset.style_id - ) + accent_phrases = engine.create_accent_phrases(text, selected_preset.style_id) return AudioQuery( accent_phrases=accent_phrases, speedScale=selected_preset.speedScale, @@ -307,7 +335,7 @@ def audio_query_from_preset( volumeScale=selected_preset.volumeScale, prePhonemeLength=selected_preset.prePhonemeLength, postPhonemeLength=selected_preset.postPhonemeLength, - outputSamplingRate=default_sampling_rate, + outputSamplingRate=core.default_sampling_rate, outputStereo=False, kana=create_kana(accent_phrases), ) @@ -326,39 +354,31 @@ def audio_query_from_preset( ) def accent_phrases( text: str, - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 is_kana: bool = False, core_version: str | None = None, ) -> list[AccentPhrase]: """ テキストからアクセント句を得ます。 - is_kanaが`true`のとき、テキストは次のAquesTalk風記法で解釈されます。デフォルトは`false`です。 + is_kanaが`true`のとき、テキストは次のAquesTalk 風記法で解釈されます。デフォルトは`false`です。 * 全てのカナはカタカナで記述される * アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。 * カナの手前に`_`を入れるとそのカナは無声化される * アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。 * アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。 """ - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) if is_kana: try: - accent_phrases = parse_kana(text) + return engine.create_accent_phrases_from_kana(text, style_id) except ParseKanaError as err: raise HTTPException( - status_code=400, - detail=ParseKanaBadRequest(err).dict(), + status_code=400, detail=ParseKanaBadRequest(err).dict() ) - accent_phrases = engine.replace_mora_data( - accent_phrases=accent_phrases, style_id=style_id - ) - - return accent_phrases else: if experimental_katakana_transcription: text = toKana(text).replace(" ", "") - return engine.create_accent_phrases(text, style_id=style_id) + return engine.create_accent_phrases(text, style_id) @app.post( "/mora_data", @@ -368,13 +388,11 @@ def accent_phrases( ) def mora_data( accent_phrases: list[AccentPhrase], - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> list[AccentPhrase]: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) - return engine.replace_mora_data(accent_phrases, style_id=style_id) + return engine.update_length_and_pitch(accent_phrases, style_id) @app.post( "/mora_length", @@ -384,15 +402,11 @@ def mora_data( ) def mora_length( accent_phrases: list[AccentPhrase], - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> list[AccentPhrase]: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) - return engine.replace_phoneme_length( - accent_phrases=accent_phrases, style_id=style_id - ) + return engine.update_length(accent_phrases, style_id) @app.post( "/mora_pitch", @@ -402,15 +416,11 @@ def mora_length( ) def mora_pitch( accent_phrases: list[AccentPhrase], - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> list[AccentPhrase]: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) - return engine.replace_mora_pitch( - accent_phrases=accent_phrases, style_id=style_id - ) + return engine.update_pitch(accent_phrases, style_id) @app.post( "/synthesis", @@ -427,20 +437,16 @@ def mora_pitch( ) def synthesis( query: AudioQuery, - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 enable_interrogative_upspeak: bool = Query( # noqa: B008 default=True, description="疑問系のテキストが与えられたら語尾を自動調整する", ), core_version: str | None = None, ) -> FileResponse: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) - wave = engine.synthesis( - query=query, - style_id=style_id, - enable_interrogative_upspeak=enable_interrogative_upspeak, + wave = engine.synthesize_wave( + query, style_id, enable_interrogative_upspeak=enable_interrogative_upspeak ) with NamedTemporaryFile(delete=False) as f: @@ -470,21 +476,16 @@ def synthesis( def cancellable_synthesis( query: AudioQuery, request: Request, - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> FileResponse: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) if cancellable_engine is None: raise HTTPException( status_code=404, detail="実験的機能はデフォルトで無効になっています。使用するには引数を指定してください。", ) f_name = cancellable_engine._synthesis_impl( - query=query, - style_id=style_id, - request=request, - core_version=core_version, + query, style_id, request, core_version=core_version ) if f_name == "": raise HTTPException(status_code=422, detail="不明なバージョンです") @@ -511,12 +512,10 @@ def cancellable_synthesis( summary="複数まとめて音声合成する", ) def multi_synthesis( - queries: list[AccentPhrase], - style_id: int | None = Query(default=None), # noqa: B008 - speaker: int | None = Query(default=None, deprecated=True), # noqa: B008 + queries: list[AudioQuery], + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> FileResponse: - style_id = get_style_id_from_deprecated(style_id=style_id, speaker_id=speaker) engine = get_engine(core_version) sampling_rate = queries[0].outputSamplingRate @@ -529,7 +528,7 @@ def multi_synthesis( ) with TemporaryFile() as wav_file: - wave = engine.synthesis(query=queries[i], style_id=style_id) + wave = engine.synthesize_wave(queries[i], style_id) soundfile.write( file=wav_file, data=wave, @@ -549,24 +548,23 @@ def multi_synthesis( "/morphable_targets", response_model=list[dict[str, MorphableTargetInfo]], tags=["音声合成"], - summary="指定した話者に対してエンジン内の話者がモーフィングが可能か判定する", + summary="指定したスタイルに対してエンジン内の話者がモーフィングが可能か判定する", ) def morphable_targets( - base_speakers: list[int], - core_version: str | None = None, + base_style_ids: list[StyleId], core_version: str | None = None ) -> list[dict[str, MorphableTargetInfo]]: """ - 指定されたベース話者に対してエンジン内の各話者がモーフィング機能を利用可能か返します。 + 指定されたベーススタイルに対してエンジン内の各話者がモーフィング機能を利用可能か返します。 モーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。 プロパティが存在しない場合は、モーフィングが許可されているとみなします。 - 返り値の話者はstring型なので注意。 + 返り値のスタイルIDはstring型なので注意。 """ - engine = get_engine(core_version) + core = get_core(core_version) try: - speakers = metas_store.load_combined_metas(engine=engine) + speakers = metas_store.load_combined_metas(core=core) morphable_targets = get_morphable_targets( - speakers=speakers, base_speakers=base_speakers + speakers=speakers, base_style_ids=base_style_ids ) # jsonはint型のキーを持てないので、string型に変換する return [ @@ -589,31 +587,32 @@ def morphable_targets( } }, tags=["音声合成"], - summary="2人の話者でモーフィングした音声を合成する", + summary="2種類のスタイルでモーフィングした音声を合成する", ) def _synthesis_morphing( query: AudioQuery, - base_speaker: int, - target_speaker: int, + base_style_id: StyleId = Query(alias="base_speaker"), # noqa: B008 + target_style_id: StyleId = Query(alias="target_speaker"), # noqa: B008 morph_rate: float = Query(..., ge=0.0, le=1.0), # noqa: B008 core_version: str | None = None, ) -> FileResponse: """ - 指定された2人の話者で音声を合成、指定した割合でモーフィングした音声を得ます。 - モーフィングの割合は`morph_rate`で指定でき、0.0でベースの話者、1.0でターゲットの話者に近づきます。 + 指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。 + モーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます。 """ engine = get_engine(core_version) + core = get_core(core_version) try: - speakers = metas_store.load_combined_metas(engine=engine) + speakers = metas_store.load_combined_metas(core=core) speaker_lookup = construct_lookup(speakers=speakers) is_permitted = is_synthesis_morphing_permitted( - speaker_lookup, base_speaker, target_speaker + speaker_lookup, base_style_id, target_style_id ) if not is_permitted: raise HTTPException( status_code=400, - detail="指定された話者ペアでのモーフィングはできません", + detail="指定されたスタイルペアでのモーフィングはできません", ) except StyleIdNotFoundError as e: raise HTTPException( @@ -623,9 +622,10 @@ def _synthesis_morphing( # 生成したパラメータはキャッシュされる morph_param = synthesis_morphing_parameter( engine=engine, + core=core, query=query, - base_speaker=base_speaker, - target_speaker=target_speaker, + base_style_id=base_style_id, + target_style_id=target_style_id, ) morph_wave = synthesis_morphing( @@ -649,6 +649,69 @@ def _synthesis_morphing( background=BackgroundTask(delete_file, f.name), ) + @app.post( + "/sing_frame_audio_query", + response_model=FrameAudioQuery, + tags=["クエリ作成"], + summary="歌唱音声合成用のクエリを作成する", + ) + def sing_frame_audio_query( + score: Score, + style_id: StyleId = Query(alias="speaker"), # noqa: B008 + core_version: str | None = None, + ) -> FrameAudioQuery: + """ + 歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。 + """ + engine = get_engine(core_version) + core = get_core(core_version) + phonemes, f0, volume = engine.create_sing_phoneme_and_f0_and_volume( + score, style_id + ) + + return FrameAudioQuery( + f0=f0, + volume=volume, + phonemes=phonemes, + volumeScale=1, + outputSamplingRate=core.default_sampling_rate, + outputStereo=False, + ) + + @app.post( + "/frame_synthesis", + response_class=FileResponse, + responses={ + 200: { + "content": { + "audio/wav": {"schema": {"type": "string", "format": "binary"}} + }, + } + }, + tags=["音声合成"], + ) + def frame_synthesis( + query: FrameAudioQuery, + style_id: StyleId = Query(alias="speaker"), # noqa: B008 + core_version: str | None = None, + ) -> FileResponse: + """ + 歌唱音声合成を行います。 + """ + engine = get_engine(core_version) + wave = engine.frame_synthsize_wave(query, style_id) + + with NamedTemporaryFile(delete=False) as f: + soundfile.write( + file=f, data=wave, samplerate=query.outputSamplingRate, format="WAV" + ) + + return FileResponse( + f.name, + media_type="audio/wav", + background=BackgroundTask(delete_file, f.name), + ) + @app.post( "/connect_waves", response_class=FileResponse, @@ -701,7 +764,12 @@ def get_presets() -> list[Preset]: raise HTTPException(status_code=422, detail=str(err)) return presets - @app.post("/add_preset", response_model=int, tags=["その他"]) + @app.post( + "/add_preset", + response_model=int, + tags=["その他"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def add_preset(preset: Preset) -> int: """ 新しいプリセットを追加します @@ -723,7 +791,12 @@ def add_preset(preset: Preset) -> int: raise HTTPException(status_code=422, detail=str(err)) return id - @app.post("/update_preset", response_model=int, tags=["その他"]) + @app.post( + "/update_preset", + response_model=int, + tags=["その他"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def update_preset(preset: Preset) -> int: """ 既存のプリセットを更新します @@ -745,7 +818,12 @@ def update_preset(preset: Preset) -> int: raise HTTPException(status_code=422, detail=str(err)) return id - @app.post("/delete_preset", status_code=204, tags=["その他"]) + @app.post( + "/delete_preset", + status_code=204, + tags=["その他"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def delete_preset(id: int) -> Response: """ 既存のプリセットを削除します @@ -769,7 +847,7 @@ def version() -> str: @app.get("/core_versions", response_model=list[str], tags=["その他"]) def core_versions() -> Response: return Response( - content=json.dumps(list(synthesis_engines.keys())), + content=json.dumps(list(cores.keys())), media_type="application/json", ) @@ -777,60 +855,92 @@ def core_versions() -> Response: def speakers( core_version: str | None = None, ) -> list[Speaker]: - engine = get_engine(core_version) - return metas_store.load_combined_metas(engine=engine) + speakers = metas_store.load_combined_metas(get_core(core_version)) + return filter_speakers_and_styles(speakers, "speaker") @app.get("/speaker_info", response_model=SpeakerInfo, tags=["その他"]) def speaker_info( speaker_uuid: str, core_version: str | None = None, - ) -> dict[str, Any]: + ) -> SpeakerInfo: """ 指定されたspeaker_uuidに関する情報をjson形式で返します。 画像や音声はbase64エンコードされたものが返されます。 - - Returns - ------- - ret_data: SpeakerInfo """ - speakers = json.loads(get_engine(core_version).speakers) + return _speaker_info( + speaker_uuid=speaker_uuid, + speaker_or_singer="speaker", + core_version=core_version, + ) + + # FIXME: この関数をどこかに切り出す + def _speaker_info( + speaker_uuid: str, + speaker_or_singer: Literal["speaker", "singer"], + core_version: str | None, + ) -> SpeakerInfo: + # エンジンに含まれる話者メタ情報は、次のディレクトリ構造に従わなければならない: + # {root_dir}/ + # speaker_info/ + # {speaker_uuid_0}/ + # policy.md + # portrait.png + # icons/ + # {id_0}.png + # {id_1}.png + # ... + # portraits/ + # {id_0}.png + # {id_1}.png + # ... + # voice_samples/ + # {id_0}_001.wav + # {id_0}_002.wav + # {id_0}_003.wav + # {id_1}_001.wav + # ... + # {speaker_uuid_1}/ + # ... + + # 該当話者の検索 + speakers = parse_obj_as( + list[Speaker], json.loads(get_core(core_version).speakers) + ) + speakers = filter_speakers_and_styles(speakers, speaker_or_singer) for i in range(len(speakers)): - if speakers[i]["speaker_uuid"] == speaker_uuid: + if speakers[i].speaker_uuid == speaker_uuid: speaker = speakers[i] break else: raise HTTPException(status_code=404, detail="該当する話者が見つかりません") try: - policy = (root_dir / f"speaker_info/{speaker_uuid}/policy.md").read_text( - "utf-8" - ) - portrait = b64encode_str( - (root_dir / f"speaker_info/{speaker_uuid}/portrait.png").read_bytes() - ) + speaker_path = root_dir / "speaker_info" / speaker_uuid + # 話者情報の取得 + # speaker policy + policy_path = speaker_path / "policy.md" + policy = policy_path.read_text("utf-8") + # speaker portrait + portrait_path = speaker_path / "portrait.png" + portrait = b64encode_str(portrait_path.read_bytes()) + # スタイル情報の取得 style_infos = [] - for style in speaker["styles"]: - id = style["id"] - icon = b64encode_str( - ( - root_dir / f"speaker_info/{speaker_uuid}/icons/{id}.png" - ).read_bytes() - ) - style_portrait_path = ( - root_dir / f"speaker_info/{speaker_uuid}/portraits/{id}.png" - ) - style_portrait = ( - b64encode_str(style_portrait_path.read_bytes()) - if style_portrait_path.exists() - else None - ) + for style in speaker.styles: + id = style.id + # style icon + style_icon_path = speaker_path / "icons" / f"{id}.png" + icon = b64encode_str(style_icon_path.read_bytes()) + # style portrait + style_portrait_path = speaker_path / "portraits" / f"{id}.png" + style_portrait = None + if style_portrait_path.exists(): + style_portrait = b64encode_str(style_portrait_path.read_bytes()) + # voice samples voice_samples = [ b64encode_str( ( - root_dir - / "speaker_info/{}/voice_samples/{}_{}.wav".format( - speaker_uuid, id, str(j + 1).zfill(3) - ) + speaker_path + / "voice_samples/{}_{}.wav".format(id, str(j + 1).zfill(3)) ).read_bytes() ) for j in range(3) @@ -849,153 +959,145 @@ def speaker_info( traceback.print_exc() raise HTTPException(status_code=500, detail="追加情報が見つかりませんでした") - ret_data = {"policy": policy, "portrait": portrait, "style_infos": style_infos} + ret_data = SpeakerInfo( + policy=policy, + portrait=portrait, + style_infos=style_infos, + ) return ret_data - @app.get( - "/downloadable_libraries", - response_model=list[DownloadableLibraryInfo], - tags=["音声ライブラリ管理"], - ) - def downloadable_libraries() -> list[DownloadableLibraryInfo]: - """ - ダウンロード可能な音声ライブラリの情報を返します。 - - Returns - ------- - ret_data: list[DownloadableLibrary] - """ - if not engine_manifest_data.supported_features.manage_library: - raise HTTPException(status_code=404, detail="この機能は実装されていません") - return library_manager.downloadable_libraries() - - @app.get( - "/installed_libraries", - response_model=dict[str, InstalledLibraryInfo], - tags=["音声ライブラリ管理"], - ) - def installed_libraries() -> dict[str, InstalledLibraryInfo]: - """ - インストールした音声ライブラリの情報を返します。 - - Returns - ------- - ret_data: dict[str, InstalledLibrary] - """ - if not engine_manifest_data.supported_features.manage_library: - raise HTTPException(status_code=404, detail="この機能は実装されていません") - return library_manager.installed_libraries() + @app.get("/singers", response_model=list[Speaker], tags=["その他"]) + def singers( + core_version: str | None = None, + ) -> list[Speaker]: + singers = metas_store.load_combined_metas(get_core(core_version)) + return filter_speakers_and_styles(singers, "singer") - @app.post( - "/install_library/{library_uuid}", - status_code=204, - tags=["音声ライブラリ管理"], - ) - async def install_library( - library_uuid: str, - request: Request, - ) -> Response: + @app.get("/singer_info", response_model=SpeakerInfo, tags=["その他"]) + def singer_info( + speaker_uuid: str, + core_version: str | None = None, + ) -> SpeakerInfo: """ - 音声ライブラリをインストールします。 - 音声ライブラリのZIPファイルをリクエストボディとして送信してください。 - - Parameters - ---------- - library_uuid: str - 音声ライブラリのID + 指定されたspeaker_uuidに関する情報をjson形式で返します。 + 画像や音声はbase64エンコードされたものが返されます。 """ - if not engine_manifest_data.supported_features.manage_library: - raise HTTPException(status_code=404, detail="この機能は実装されていません") - archive = BytesIO(await request.body()) - loop = asyncio.get_event_loop() - await loop.run_in_executor( - None, library_manager.install_library, library_uuid, archive + return _speaker_info( + speaker_uuid=speaker_uuid, + speaker_or_singer="singer", + core_version=core_version, ) - return Response(status_code=204) - @app.post( - "/uninstall_library/{library_uuid}", - status_code=204, - tags=["音声ライブラリ管理"], - ) - def uninstall_library(library_uuid: str) -> Response: - """ - 音声ライブラリをアンインストールします。 + if engine_manifest_data.supported_features.manage_library: - Parameters - ---------- - library_uuid: str - 音声ライブラリのID - """ - if not engine_manifest_data.supported_features.manage_library: - raise HTTPException(status_code=404, detail="この機能は実装されていません") - library_manager.uninstall_library(library_uuid) - return Response(status_code=204) - - @app.post("/initialize_style_id", status_code=204, tags=["その他"]) - def initialize_style_id( - style_id: int, - skip_reinit: bool = Query( # noqa: B008 - False, description="既に初期化済みのスタイルの再初期化をスキップするかどうか" - ), - core_version: str | None = None, - ) -> Response: - """ - 指定されたstyle_idのスタイルを初期化します。 - 実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。 - """ - engine = get_engine(core_version) - engine.initialize_style_id_synthesis(style_id=style_id, skip_reinit=skip_reinit) - return Response(status_code=204) + @app.get( + "/downloadable_libraries", + response_model=list[DownloadableLibraryInfo], + tags=["音声ライブラリ管理"], + ) + def downloadable_libraries() -> list[DownloadableLibraryInfo]: + """ + ダウンロード可能な音声ライブラリの情報を返します。 + + Returns + ------- + ret_data: list[DownloadableLibrary] + """ + if not engine_manifest_data.supported_features.manage_library: + raise HTTPException(status_code=404, detail="この機能は実装されていません") + return library_manager.downloadable_libraries() + + @app.get( + "/installed_libraries", + response_model=dict[str, InstalledLibraryInfo], + tags=["音声ライブラリ管理"], + ) + def installed_libraries() -> dict[str, InstalledLibraryInfo]: + """ + インストールした音声ライブラリの情報を返します。 + + Returns + ------- + ret_data: dict[str, InstalledLibrary] + """ + if not engine_manifest_data.supported_features.manage_library: + raise HTTPException(status_code=404, detail="この機能は実装されていません") + return library_manager.installed_libraries() + + @app.post( + "/install_library/{library_uuid}", + status_code=204, + tags=["音声ライブラリ管理"], + dependencies=[Depends(check_disabled_mutable_api)], + ) + async def install_library( + library_uuid: str, + request: Request, + ) -> Response: + """ + 音声ライブラリをインストールします。 + 音声ライブラリのZIPファイルをリクエストボディとして送信してください。 + + Parameters + ---------- + library_uuid: str + 音声ライブラリのID + """ + if not engine_manifest_data.supported_features.manage_library: + raise HTTPException(status_code=404, detail="この機能は実装されていません") + archive = BytesIO(await request.body()) + loop = asyncio.get_event_loop() + await loop.run_in_executor( + None, library_manager.install_library, library_uuid, archive + ) + return Response(status_code=204) - @app.get("/is_initialized_style_id", response_model=bool, tags=["その他"]) - def is_initialized_style_id( - style_id: int, - core_version: str | None = None, - ) -> bool: - """ - 指定されたstyle_idのスタイルが初期化されているかどうかを返します。 - """ - engine = get_engine(core_version) - return engine.is_initialized_style_id_synthesis(style_id) + @app.post( + "/uninstall_library/{library_uuid}", + status_code=204, + tags=["音声ライブラリ管理"], + dependencies=[Depends(check_disabled_mutable_api)], + ) + def uninstall_library(library_uuid: str) -> Response: + """ + 音声ライブラリをアンインストールします。 + + Parameters + ---------- + library_uuid: str + 音声ライブラリのID + """ + if not engine_manifest_data.supported_features.manage_library: + raise HTTPException(status_code=404, detail="この機能は実装されていません") + library_manager.uninstall_library(library_uuid) + return Response(status_code=204) - @app.post("/initialize_speaker", status_code=204, tags=["その他"], deprecated=True) + @app.post("/initialize_speaker", status_code=204, tags=["その他"]) def initialize_speaker( - speaker: int, + style_id: StyleId = Query(alias="speaker"), # noqa: B008 skip_reinit: bool = Query( # noqa: B008 - False, description="既に初期化済みの話者の再初期化をスキップするかどうか" + default=False, description="既に初期化済みのスタイルの再初期化をスキップするかどうか" ), core_version: str | None = None, ) -> Response: """ - こちらのAPIは非推奨です。`initialize_style_id`を利用してください。\n - 指定されたspeaker_idの話者を初期化します。 + 指定されたスタイルを初期化します。 実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。 """ - warnings.warn( - "使用しているAPI(/initialize_speaker)は非推奨です。/initialized_style_idを利用してください。", - stacklevel=1, - ) - return initialize_style_id( - style_id=speaker, skip_reinit=skip_reinit, core_version=core_version - ) + core = get_core(core_version) + core.initialize_style_id_synthesis(style_id, skip_reinit=skip_reinit) + return Response(status_code=204) - @app.get( - "/is_initialized_speaker", response_model=bool, tags=["その他"], deprecated=True - ) + @app.get("/is_initialized_speaker", response_model=bool, tags=["その他"]) def is_initialized_speaker( - speaker: int, + style_id: StyleId = Query(alias="speaker"), # noqa: B008 core_version: str | None = None, ) -> bool: """ - こちらのAPIは非推奨です。`is_initialize_style_id`を利用してください。\n - 指定されたspeaker_idの話者が初期化されているかどうかを返します。 + 指定されたスタイルが初期化されているかどうかを返します。 """ - warnings.warn( - "使用しているAPI(/is_initialize_speaker)は非推奨です。/is_initialized_style_idを利用してください。", - stacklevel=1, - ) - return is_initialized_style_id(style_id=speaker, core_version=core_version) + core = get_core(core_version) + return core.is_initialized_style_id_synthesis(style_id) @app.get("/user_dict", response_model=dict[str, UserDictWord], tags=["ユーザー辞書"]) def get_user_dict_words() -> dict[str, UserDictWord]: @@ -1014,13 +1116,18 @@ def get_user_dict_words() -> dict[str, UserDictWord]: traceback.print_exc() raise HTTPException(status_code=422, detail="辞書の読み込みに失敗しました。") - @app.post("/user_dict_word", response_model=str, tags=["ユーザー辞書"]) + @app.post( + "/user_dict_word", + response_model=str, + tags=["ユーザー辞書"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def add_user_dict_word( surface: str, pronunciation: str, accent_type: int, word_type: WordTypes | None = None, - priority: conint(ge=MIN_PRIORITY, le=MAX_PRIORITY) | None = None, + priority: Annotated[int | None, Query(ge=MIN_PRIORITY, le=MAX_PRIORITY)] = None, ) -> Response: """ ユーザー辞書に言葉を追加します。 @@ -1055,14 +1162,19 @@ def add_user_dict_word( traceback.print_exc() raise HTTPException(status_code=422, detail="ユーザー辞書への追加に失敗しました。") - @app.put("/user_dict_word/{word_uuid}", status_code=204, tags=["ユーザー辞書"]) + @app.put( + "/user_dict_word/{word_uuid}", + status_code=204, + tags=["ユーザー辞書"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def rewrite_user_dict_word( surface: str, pronunciation: str, accent_type: int, word_uuid: str, word_type: WordTypes | None = None, - priority: conint(ge=MIN_PRIORITY, le=MAX_PRIORITY) | None = None, + priority: Annotated[int | None, Query(ge=MIN_PRIORITY, le=MAX_PRIORITY)] = None, ) -> Response: """ ユーザー辞書に登録されている言葉を更新します。 @@ -1102,7 +1214,12 @@ def rewrite_user_dict_word( traceback.print_exc() raise HTTPException(status_code=422, detail="ユーザー辞書の更新に失敗しました。") - @app.delete("/user_dict_word/{word_uuid}", status_code=204, tags=["ユーザー辞書"]) + @app.delete( + "/user_dict_word/{word_uuid}", + status_code=204, + tags=["ユーザー辞書"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def delete_user_dict_word(word_uuid: str) -> Response: """ ユーザー辞書に登録されている言葉を削除します。 @@ -1121,7 +1238,12 @@ def delete_user_dict_word(word_uuid: str) -> Response: traceback.print_exc() raise HTTPException(status_code=422, detail="ユーザー辞書の更新に失敗しました。") - @app.post("/import_user_dict", status_code=204, tags=["ユーザー辞書"]) + @app.post( + "/import_user_dict", + status_code=204, + tags=["ユーザー辞書"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def import_user_dict_words( import_dict_data: dict[str, UserDictWord], override: bool, @@ -1147,7 +1269,7 @@ def import_user_dict_words( def supported_devices( core_version: str | None = None, ) -> Response: - supported_devices = get_engine(core_version).supported_devices + supported_devices = get_core(core_version).supported_devices if supported_devices is None: raise HTTPException(status_code=422, detail="非対応の機能です。") return Response( @@ -1163,7 +1285,7 @@ def engine_manifest() -> EngineManifest: "/validate_kana", response_model=bool, tags=["その他"], - summary="テキストがAquesTalk風記法に従っているか判定する", + summary="テキストがAquesTalk 風記法に従っているか判定する", responses={ 400: { "description": "テキストが不正です", @@ -1173,7 +1295,7 @@ def engine_manifest() -> EngineManifest: ) def validate_kana(text: str) -> bool: """ - テキストがAquesTalk風記法に従っているかどうかを判定します。 + テキストがAquesTalk 風記法に従っているかどうかを判定します。 従っていない場合はエラーが返ります。 Parameters @@ -1192,8 +1314,12 @@ def validate_kana(text: str) -> bool: @app.get("/setting", response_class=Response, tags=["設定"]) def setting_get(request: Request) -> Response: - settings = setting_loader.load_setting_file() + """ + 設定ページを返します。 + """ + settings = setting_loader.load() + brand_name = engine_manifest_data.brand_name cors_policy_mode = settings.cors_policy_mode allow_origin = settings.allow_origin @@ -1204,39 +1330,34 @@ def setting_get(request: Request) -> Response: "ui.html", { "request": request, + "brand_name": brand_name, "cors_policy_mode": cors_policy_mode, "allow_origin": allow_origin, }, ) - @app.post("/setting", response_class=Response, tags=["設定"]) + @app.post( + "/setting", + response_class=Response, + tags=["設定"], + dependencies=[Depends(check_disabled_mutable_api)], + ) def setting_post( - request: Request, - cors_policy_mode: str | None = Form(None), # noqa: B008 - allow_origin: str | None = Form(None), # noqa: B008 + cors_policy_mode: CorsPolicyMode = Form(), # noqa + allow_origin: str | None = Form(default=None), # noqa ) -> Response: + """ + 設定を更新します。 + """ settings = Setting( cors_policy_mode=cors_policy_mode, allow_origin=allow_origin, ) # 更新した設定へ上書き - setting_loader.dump_setting_file(settings) - - message = "設定を保存しました。" + setting_loader.save(settings) - if allow_origin is None: - allow_origin = "" - - return setting_ui_template.TemplateResponse( - "ui.html", - { - "request": request, - "cors_policy_mode": cors_policy_mode, - "allow_origin": allow_origin, - "message": message, - }, - ) + return Response(status_code=204) # BaseLibraryInfo/VvlibManifestモデルはAPIとして表には出ないが、エディタ側で利用したいので、手動で追加する # ref: https://fastapi.tiangolo.com/advanced/extending-openapi/#modify-the-openapi-schema @@ -1267,7 +1388,7 @@ def custom_openapi(): app.openapi_schema = openapi_schema return openapi_schema - app.openapi = custom_openapi + app.openapi = custom_openapi # type: ignore[method-assign] return app @@ -1275,23 +1396,16 @@ def custom_openapi(): def main() -> None: multiprocessing.freeze_support() - output_log_utf8 = os.getenv("VV_OUTPUT_LOG_UTF8", default="") - if output_log_utf8 == "1": + output_log_utf8 = decide_boolean_from_env("VV_OUTPUT_LOG_UTF8") + if output_log_utf8: set_output_log_utf8() - elif not (output_log_utf8 == "" or output_log_utf8 == "0"): - print( - "WARNING: invalid VV_OUTPUT_LOG_UTF8 environment variable value", - file=sys.stderr, - ) parser = argparse.ArgumentParser(description="VOICEVOX のエンジンです。") parser.add_argument( "--host", type=str, default="127.0.0.1", help="接続を受け付けるホストアドレスです。" ) parser.add_argument("--port", type=int, default=50021, help="接続を受け付けるポート番号です。") - parser.add_argument( - "--use_gpu", action="store_true", help="指定するとGPUを使って音声合成するようになります。" - ) + parser.add_argument("--use_gpu", action="store_true", help="GPUを使って音声合成するようになります。") parser.add_argument( "--voicevox_dir", type=Path, default=None, help="VOICEVOXのディレクトリパスです。" ) @@ -1312,12 +1426,12 @@ def main() -> None: parser.add_argument( "--enable_mock", action="store_true", - help="指定するとVOICEVOX COREを使わずモックで音声合成を行います。", + help="VOICEVOX COREを使わずモックで音声合成を行います。", ) parser.add_argument( "--enable_cancellable_synthesis", action="store_true", - help="指定すると音声合成を途中でキャンセルできるようになります。", + help="音声合成を途中でキャンセルできるようになります。", ) parser.add_argument( "--init_processes", @@ -1326,7 +1440,7 @@ def main() -> None: help="cancellable_synthesis機能の初期化時に生成するプロセス数です。", ) parser.add_argument( - "--load_all_models", action="store_true", help="指定すると起動時に全ての音声合成モデルを読み込みます。" + "--load_all_models", action="store_true", help="起動時に全ての音声合成モデルを読み込みます。" ) # 引数へcpu_num_threadsの指定がなければ、環境変数をロールします。 @@ -1337,8 +1451,8 @@ def main() -> None: type=int, default=os.getenv("VV_CPU_NUM_THREADS") or None, help=( - "音声合成を行うスレッド数です。指定しないと、代わりに環境変数VV_CPU_NUM_THREADSの値が使われます。" - "VV_CPU_NUM_THREADSが空文字列でなく数値でもない場合はエラー終了します。" + "音声合成を行うスレッド数です。指定しない場合、代わりに環境変数 VV_CPU_NUM_THREADS の値が使われます。" + "VV_CPU_NUM_THREADS が空文字列でなく数値でもない場合はエラー終了します。" ), ) @@ -1346,7 +1460,7 @@ def main() -> None: "--output_log_utf8", action="store_true", help=( - "指定するとログ出力をUTF-8でおこないます。指定しないと、代わりに環境変数 VV_OUTPUT_LOG_UTF8 の値が使われます。" + "ログ出力をUTF-8でおこないます。指定しない場合、代わりに環境変数 VV_OUTPUT_LOG_UTF8 の値が使われます。" "VV_OUTPUT_LOG_UTF8 の値が1の場合はUTF-8で、0または空文字、値がない場合は環境によって自動的に決定されます。" ), ) @@ -1360,11 +1474,17 @@ def main() -> None: "CORSの許可モード。allまたはlocalappsが指定できます。allはすべてを許可します。" "localappsはオリジン間リソース共有ポリシーを、app://.とlocalhost関連に限定します。" "その他のオリジンはallow_originオプションで追加できます。デフォルトはlocalapps。" + "このオプションは--setting_fileで指定される設定ファイルよりも優先されます。" ), ) parser.add_argument( - "--allow_origin", nargs="*", help="許可するオリジンを指定します。スペースで区切ることで複数指定できます。" + "--allow_origin", + nargs="*", + help=( + "許可するオリジンを指定します。スペースで区切ることで複数指定できます。" + "このオプションは--setting_fileで指定される設定ファイルよりも優先されます。" + ), ) parser.add_argument( @@ -1388,6 +1508,16 @@ def main() -> None: help="韓国語と英語の発音をカタカナに置き換えます。数字は変換しません。", ) + parser.add_argument( + "--disable_mutable_api", + action="store_true", + help=( + "辞書登録や設定変更など、エンジンの静的なデータを変更するAPIを無効化します。" + "指定しない場合、代わりに環境変数 VV_DISABLE_MUTABLE_API の値が使われます。" + "VV_DISABLE_MUTABLE_API の値が1の場合は無効化で、0または空文字、値がない場合は無視されます。" + ), + ) + args = parser.parse_args() if args.output_log_utf8: @@ -1402,7 +1532,7 @@ def main() -> None: cpu_num_threads: int | None = args.cpu_num_threads load_all_models: bool = args.load_all_models - synthesis_engines = make_synthesis_engines( + cores = initialize_cores( use_gpu=use_gpu, voicelib_dirs=voicelib_dirs, voicevox_dir=voicevox_dir, @@ -1411,8 +1541,9 @@ def main() -> None: enable_mock=enable_mock, load_all_models=load_all_models, ) - assert len(synthesis_engines) != 0, "音声合成エンジンがありません。" - latest_core_version = get_latest_core_version(versions=synthesis_engines.keys()) + tts_engines = make_tts_engines_from_cores(cores) + assert len(tts_engines) != 0, "音声合成エンジンがありません。" + latest_core_version = get_latest_core_version(versions=list(tts_engines.keys())) # Cancellable Engine enable_cancellable_synthesis: bool = args.enable_cancellable_synthesis @@ -1434,9 +1565,9 @@ def main() -> None: if root_dir is None: root_dir = engine_root() - setting_loader = SettingLoader(args.setting_file) + setting_loader = SettingHandler(args.setting_file) - settings = setting_loader.load_setting_file() + settings = setting_loader.load() cors_policy_mode: CorsPolicyMode | None = args.cors_policy_mode if cors_policy_mode is None: @@ -1466,9 +1597,14 @@ def main() -> None: preset_path=preset_path, ) + disable_mutable_api: bool = args.disable_mutable_api | decide_boolean_from_env( + "VV_DISABLE_MUTABLE_API" + ) + uvicorn.run( generate_app( - synthesis_engines, + tts_engines, + cores, latest_core_version, setting_loader, preset_manager=preset_manager, @@ -1477,6 +1613,7 @@ def main() -> None: root_dir=root_dir, cors_policy_mode=cors_policy_mode, allow_origin=allow_origin, + disable_mutable_api=disable_mutable_api, ), host=args.host, port=args.port, diff --git a/run.spec b/run.spec index b84c82408..970f2adfa 100644 --- a/run.spec +++ b/run.spec @@ -10,7 +10,6 @@ datas = [ ('default.csv', '.'), ('licenses.json', '.'), ('presets.yaml', '.'), - ('default_setting.yml', '.'), ('ui_template', 'ui_template'), ] datas += collect_data_files('pyopenjtalk') diff --git a/setup.cfg b/setup.cfg index 2a1a913e0..a66e1fd12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,8 @@ disallow_untyped_defs = False ignore_errors = False ignore_missing_imports = True no_implicit_optional = True -python_version = 3.10 +plugins = numpy.typing.mypy_plugin,pydantic.mypy +python_version = 3.11 show_error_codes = True strict_equality = True strict_optional = True diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 000000000..e354d3809 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,16 @@ +import pytest +from syrupy.assertion import SnapshotAssertion +from syrupy.extensions.json import JSONSnapshotExtension + + +@pytest.fixture +def snapshot_json(snapshot: SnapshotAssertion) -> SnapshotAssertion: + """ + syrupyでJSONをsnapshotするためのfixture。 + + Examples + -------- + >>> def test_foo(snapshot_json: SnapshotAssertion): + >>> assert snapshot_json == {"key": "value"} + """ + return snapshot.use_extension(JSONSnapshotExtension) diff --git "a/test/e2e/__snapshots__/test_audio_query/test_speaker\343\202\222\346\214\207\345\256\232\343\201\227\343\201\246\351\237\263\345\243\260\345\220\210\346\210\220\343\202\257\343\202\250\343\203\252\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" "b/test/e2e/__snapshots__/test_audio_query/test_speaker\343\202\222\346\214\207\345\256\232\343\201\227\343\201\246\351\237\263\345\243\260\345\220\210\346\210\220\343\202\257\343\202\250\343\203\252\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" new file mode 100644 index 000000000..e47234dce --- /dev/null +++ "b/test/e2e/__snapshots__/test_audio_query/test_speaker\343\202\222\346\214\207\345\256\232\343\201\227\343\201\246\351\237\263\345\243\260\345\220\210\346\210\220\343\202\257\343\202\250\343\203\252\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" @@ -0,0 +1,60 @@ +{ + "accent_phrases": [ + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "t", + "consonant_length": 2.31, + "pitch": 3.38, + "text": "テ", + "vowel": "e", + "vowel_length": 0.88 + }, + { + "consonant": "s", + "consonant_length": 2.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 0.38 + }, + { + "consonant": "t", + "consonant_length": 2.31, + "pitch": 4.19, + "text": "ト", + "vowel": "o", + "vowel_length": 1.88 + }, + { + "consonant": "d", + "consonant_length": 0.75, + "pitch": 1.62, + "text": "デ", + "vowel": "e", + "vowel_length": 0.88 + }, + { + "consonant": "s", + "consonant_length": 2.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 0.38 + } + ], + "pause_mora": null + } + ], + "intonationScale": 1.0, + "kana": "テ'_ストデ_ス", + "outputSamplingRate": 24000, + "outputStereo": false, + "pitchScale": 0.0, + "postPhonemeLength": 0.1, + "prePhonemeLength": 0.1, + "speedScale": 1.0, + "volumeScale": 1.0 +} diff --git "a/test/e2e/__snapshots__/test_engine_manifest/test_\343\202\250\343\203\263\343\202\270\343\203\263\343\203\236\343\203\213\343\203\225\343\202\247\343\202\271\343\203\210\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" "b/test/e2e/__snapshots__/test_engine_manifest/test_\343\202\250\343\203\263\343\202\270\343\203\263\343\203\236\343\203\213\343\203\225\343\202\247\343\202\271\343\203\210\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" new file mode 100644 index 000000000..d69263afd --- /dev/null +++ "b/test/e2e/__snapshots__/test_engine_manifest/test_\343\202\250\343\203\263\343\202\270\343\203\263\343\203\236\343\203\213\343\203\225\343\202\247\343\202\271\343\203\210\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" @@ -0,0 +1,181 @@ +{ + "brand_name": "DUMMY", + "default_sampling_rate": 24000, + "dependency_licenses": [ + { + "license": "dummy license", + "name": "dummy library", + "text": "dummy license text", + "version": "0.0.1" + } + ], + "frame_rate": 93.75, + "icon": "MD5:f957eb4f5daedccb4eb6a5170f384bf4", + "manifest_version": "0.13.1", + "name": "DUMMY Engine", + "supported_features": { + "adjust_intonation_scale": true, + "adjust_mora_pitch": true, + "adjust_phoneme_length": true, + "adjust_pitch_scale": true, + "adjust_speed_scale": true, + "adjust_volume_scale": true, + "interrogative_upspeak": true, + "manage_library": true, + "sing": true, + "synthesis_morphing": true + }, + "supported_vvlib_manifest_version": null, + "terms_of_service": "dummy teams of service", + "update_infos": [ + { + "contributors": [ + "Hiroshiba", + "y-chan" + ], + "descriptions": [ + "ソングAPIを追加", + "キャラクター「四国めたん」「ずんだもん」「春日部つむぎ」「雨晴はう」「波音リツ」のハミングを追加", + "キャラクター「波音リツ」のソングを追加" + ], + "version": "0.16.0" + }, + { + "contributors": [], + "descriptions": [ + "ビルド成果物のディレクトリ構造を元に戻した" + ], + "version": "0.15.1" + }, + { + "contributors": [ + "aoirint", + "FujisakiEx", + "Hiroshiba", + "K-shir0", + "My-MC", + "nagi-miaow", + "okaits", + "raa0121", + "sabonerune", + "sevenc-nanashi", + "siketyan", + "stmtk1", + "takana-v", + "tarepan", + "tomoish", + "tuna2134", + "weweweok", + "whiteball", + "y-chan" + ], + "descriptions": [ + "/validate_kana APIを追加", + "起動時のエンジン設定項目追加", + "ユーザー辞書のインポート・エクスポート機能追加", + "ビルド成果物のディレクトリ構造を変更", + "書き込み系APIを一括で無効化可能に", + "開発環境の向上", + "バグ修正" + ], + "version": "0.15.0" + }, + { + "contributors": [], + "descriptions": [ + "キャラクター「小夜」「ずんだもん」「もち子さん」「青山龍星」のスタイルを追加・更新" + ], + "version": "0.14.7" + }, + { + "contributors": [], + "descriptions": [ + "キャラクター「栗田まろん」「あいえるたん」「満別花丸」「琴詠ニア」を追加" + ], + "version": "0.14.6" + }, + { + "contributors": [], + "descriptions": [ + "キャラクター「中国うさぎ」を追加", + "キャラクター「波音リツ」「もち子さん」のスタイルを追加" + ], + "version": "0.14.5" + }, + { + "contributors": [ + "Hiroshiba" + ], + "descriptions": [ + "キャラクター「春歌ナナ」「猫使アル」「猫使ビィ」を追加", + "バグ修正" + ], + "version": "0.14.4" + }, + { + "contributors": [ + "Hiroshiba" + ], + "descriptions": [ + "キャラクター「†聖騎士 紅桜†」「雀松朱司」「麒ヶ島宗麟」を追加", + "同時書き込みで辞書が破損する問題を修正" + ], + "version": "0.14.3" + }, + { + "contributors": [], + "descriptions": [ + "DirectML版の生成が遅い問題を修正" + ], + "version": "0.14.2" + }, + { + "contributors": [], + "descriptions": [ + "AquesTalkライクな記法で生成した音声のバグを修正" + ], + "version": "0.14.1" + }, + { + "contributors": [ + "aoirint", + "Appletigerv", + "haru3me", + "Hiroshiba", + "ksk001100", + "masinc", + "misogihagi", + "My-MC", + "nebocco", + "PickledChair", + "qryxip", + "qwerty2501", + "sabonerune", + "sarisia", + "Segu-g", + "sevenc-nanashi", + "shigobu", + "smly", + "takana-v", + "ts-klassen", + "whiteball", + "y-chan" + ], + "descriptions": [ + "コアをRust言語に移行", + "セキュリティアップデート", + "スタイルごとに異なる立ち絵の提供を可能に", + "VVPPファイルの提供", + "設定GUIの提供", + "プリセットの保存", + "モーフィングAPIの仕様変更", + "DirectML利用時に適したGPUを自動選択", + "開発環境の向上", + "バグ修正" + ], + "version": "0.14.0" + } + ], + "url": "https://github.com/VOICEVOX/voicevox_engine", + "uuid": "c7b58856-bd56-4aa1-afb7-b8415f824b06" +} diff --git "a/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" new file mode 100644 index 000000000..f766359ce --- /dev/null +++ "b/test/e2e/__snapshots__/test_openapi/test_OpenAPI\343\201\256\345\275\242\343\201\214\345\244\211\343\202\217\343\201\243\343\201\246\343\201\204\343\201\252\343\201\204\343\201\223\343\201\250\343\202\222\347\242\272\350\252\215.json" @@ -0,0 +1,3095 @@ +{ + "components": { + "schemas": { + "AccentPhrase": { + "description": "アクセント句ごとの情報", + "properties": { + "accent": { + "title": "アクセント箇所", + "type": "integer" + }, + "is_interrogative": { + "default": false, + "title": "疑問系かどうか", + "type": "boolean" + }, + "moras": { + "items": { + "$ref": "#/components/schemas/Mora" + }, + "title": "モーラのリスト", + "type": "array" + }, + "pause_mora": { + "allOf": [ + { + "$ref": "#/components/schemas/Mora" + } + ], + "title": "後ろに無音を付けるかどうか" + } + }, + "required": [ + "moras", + "accent" + ], + "title": "AccentPhrase", + "type": "object" + }, + "AudioQuery": { + "description": "音声合成用のクエリ", + "properties": { + "accent_phrases": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "アクセント句のリスト", + "type": "array" + }, + "intonationScale": { + "title": "全体の抑揚", + "type": "number" + }, + "kana": { + "title": "[読み取り専用]AquesTalk 風記法によるテキスト。音声合成用のクエリとしては無視される", + "type": "string" + }, + "outputSamplingRate": { + "title": "音声データの出力サンプリングレート", + "type": "integer" + }, + "outputStereo": { + "title": "音声データをステレオ出力するか否か", + "type": "boolean" + }, + "pitchScale": { + "title": "全体の音高", + "type": "number" + }, + "postPhonemeLength": { + "title": "音声の後の無音時間", + "type": "number" + }, + "prePhonemeLength": { + "title": "音声の前の無音時間", + "type": "number" + }, + "speedScale": { + "title": "全体の話速", + "type": "number" + }, + "volumeScale": { + "title": "全体の音量", + "type": "number" + } + }, + "required": [ + "accent_phrases", + "speedScale", + "pitchScale", + "intonationScale", + "volumeScale", + "prePhonemeLength", + "postPhonemeLength", + "outputSamplingRate", + "outputStereo" + ], + "title": "AudioQuery", + "type": "object" + }, + "BaseLibraryInfo": { + "description": "音声ライブラリの情報", + "properties": { + "bytes": { + "title": "音声ライブラリのバイト数", + "type": "integer" + }, + "download_url": { + "title": "音声ライブラリのダウンロードURL", + "type": "string" + }, + "name": { + "title": "音声ライブラリの名前", + "type": "string" + }, + "speakers": { + "items": { + "$ref": "#/components/schemas/LibrarySpeaker" + }, + "title": "音声ライブラリに含まれる話者のリスト", + "type": "array" + }, + "uuid": { + "title": "音声ライブラリのUUID", + "type": "string" + }, + "version": { + "title": "音声ライブラリのバージョン", + "type": "string" + } + }, + "required": [ + "name", + "uuid", + "version", + "download_url", + "bytes", + "speakers" + ], + "title": "BaseLibraryInfo", + "type": "object" + }, + "Body_setting_post_setting_post": { + "properties": { + "allow_origin": { + "title": "Allow Origin", + "type": "string" + }, + "cors_policy_mode": { + "$ref": "#/components/schemas/CorsPolicyMode" + } + }, + "required": [ + "cors_policy_mode" + ], + "title": "Body_setting_post_setting_post", + "type": "object" + }, + "CorsPolicyMode": { + "description": "CORSの許可モード", + "enum": [ + "all", + "localapps" + ], + "title": "CorsPolicyMode", + "type": "string" + }, + "DownloadableLibraryInfo": { + "description": "ダウンロード可能な音声ライブラリの情報", + "properties": { + "bytes": { + "title": "音声ライブラリのバイト数", + "type": "integer" + }, + "download_url": { + "title": "音声ライブラリのダウンロードURL", + "type": "string" + }, + "name": { + "title": "音声ライブラリの名前", + "type": "string" + }, + "speakers": { + "items": { + "$ref": "#/components/schemas/LibrarySpeaker" + }, + "title": "音声ライブラリに含まれる話者のリスト", + "type": "array" + }, + "uuid": { + "title": "音声ライブラリのUUID", + "type": "string" + }, + "version": { + "title": "音声ライブラリのバージョン", + "type": "string" + } + }, + "required": [ + "name", + "uuid", + "version", + "download_url", + "bytes", + "speakers" + ], + "title": "DownloadableLibraryInfo", + "type": "object" + }, + "EngineManifest": { + "description": "エンジン自体に関する情報", + "properties": { + "brand_name": { + "title": "ブランド名", + "type": "string" + }, + "default_sampling_rate": { + "title": "デフォルトのサンプリング周波数", + "type": "integer" + }, + "dependency_licenses": { + "items": { + "$ref": "#/components/schemas/LicenseInfo" + }, + "title": "依存関係のライセンス情報", + "type": "array" + }, + "frame_rate": { + "title": "エンジンのフレームレート", + "type": "number" + }, + "icon": { + "title": "エンジンのアイコンをBASE64エンコードしたもの", + "type": "string" + }, + "manifest_version": { + "title": "マニフェストのバージョン", + "type": "string" + }, + "name": { + "title": "エンジン名", + "type": "string" + }, + "supported_features": { + "allOf": [ + { + "$ref": "#/components/schemas/SupportedFeatures" + } + ], + "title": "エンジンが持つ機能" + }, + "supported_vvlib_manifest_version": { + "title": "エンジンが対応するvvlibのバージョン", + "type": "string" + }, + "terms_of_service": { + "title": "エンジンの利用規約", + "type": "string" + }, + "update_infos": { + "items": { + "$ref": "#/components/schemas/UpdateInfo" + }, + "title": "エンジンのアップデート情報", + "type": "array" + }, + "url": { + "title": "エンジンのURL", + "type": "string" + }, + "uuid": { + "title": "エンジンのUUID", + "type": "string" + } + }, + "required": [ + "manifest_version", + "name", + "brand_name", + "uuid", + "url", + "icon", + "default_sampling_rate", + "frame_rate", + "terms_of_service", + "update_infos", + "dependency_licenses", + "supported_features" + ], + "title": "EngineManifest", + "type": "object" + }, + "FrameAudioQuery": { + "description": "フレームごとの音声合成用のクエリ", + "properties": { + "f0": { + "items": { + "type": "number" + }, + "title": "フレームごとの基本周波数", + "type": "array" + }, + "outputSamplingRate": { + "title": "音声データの出力サンプリングレート", + "type": "integer" + }, + "outputStereo": { + "title": "音声データをステレオ出力するか否か", + "type": "boolean" + }, + "phonemes": { + "items": { + "$ref": "#/components/schemas/FramePhoneme" + }, + "title": "音素のリスト", + "type": "array" + }, + "volume": { + "items": { + "type": "number" + }, + "title": "フレームごとの音量", + "type": "array" + }, + "volumeScale": { + "title": "全体の音量", + "type": "number" + } + }, + "required": [ + "f0", + "volume", + "phonemes", + "volumeScale", + "outputSamplingRate", + "outputStereo" + ], + "title": "FrameAudioQuery", + "type": "object" + }, + "FramePhoneme": { + "description": "音素の情報", + "properties": { + "frame_length": { + "title": "音素のフレーム長", + "type": "integer" + }, + "phoneme": { + "title": "音素", + "type": "string" + } + }, + "required": [ + "phoneme", + "frame_length" + ], + "title": "FramePhoneme", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "InstalledLibraryInfo": { + "description": "インストール済み音声ライブラリの情報", + "properties": { + "bytes": { + "title": "音声ライブラリのバイト数", + "type": "integer" + }, + "download_url": { + "title": "音声ライブラリのダウンロードURL", + "type": "string" + }, + "name": { + "title": "音声ライブラリの名前", + "type": "string" + }, + "speakers": { + "items": { + "$ref": "#/components/schemas/LibrarySpeaker" + }, + "title": "音声ライブラリに含まれる話者のリスト", + "type": "array" + }, + "uninstallable": { + "title": "アンインストール可能かどうか", + "type": "boolean" + }, + "uuid": { + "title": "音声ライブラリのUUID", + "type": "string" + }, + "version": { + "title": "音声ライブラリのバージョン", + "type": "string" + } + }, + "required": [ + "name", + "uuid", + "version", + "download_url", + "bytes", + "speakers", + "uninstallable" + ], + "title": "InstalledLibraryInfo", + "type": "object" + }, + "LibrarySpeaker": { + "description": "音声ライブラリに含まれる話者の情報", + "properties": { + "speaker": { + "allOf": [ + { + "$ref": "#/components/schemas/Speaker" + } + ], + "title": "話者情報" + }, + "speaker_info": { + "allOf": [ + { + "$ref": "#/components/schemas/SpeakerInfo" + } + ], + "title": "話者の追加情報" + } + }, + "required": [ + "speaker", + "speaker_info" + ], + "title": "LibrarySpeaker", + "type": "object" + }, + "LicenseInfo": { + "description": "依存ライブラリのライセンス情報", + "properties": { + "license": { + "title": "依存ライブラリのライセンス名", + "type": "string" + }, + "name": { + "title": "依存ライブラリ名", + "type": "string" + }, + "text": { + "title": "依存ライブラリのライセンス本文", + "type": "string" + }, + "version": { + "title": "依存ライブラリのバージョン", + "type": "string" + } + }, + "required": [ + "name", + "text" + ], + "title": "LicenseInfo", + "type": "object" + }, + "Mora": { + "description": "モーラ(子音+母音)ごとの情報", + "properties": { + "consonant": { + "title": "子音の音素", + "type": "string" + }, + "consonant_length": { + "title": "子音の音長", + "type": "number" + }, + "pitch": { + "title": "音高", + "type": "number" + }, + "text": { + "title": "文字", + "type": "string" + }, + "vowel": { + "title": "母音の音素", + "type": "string" + }, + "vowel_length": { + "title": "母音の音長", + "type": "number" + } + }, + "required": [ + "text", + "vowel", + "vowel_length", + "pitch" + ], + "title": "Mora", + "type": "object" + }, + "MorphableTargetInfo": { + "properties": { + "is_morphable": { + "title": "指定した話者に対してモーフィングの可否", + "type": "boolean" + } + }, + "required": [ + "is_morphable" + ], + "title": "MorphableTargetInfo", + "type": "object" + }, + "Note": { + "description": "音符ごとの情報", + "properties": { + "frame_length": { + "title": "音符のフレーム長", + "type": "integer" + }, + "key": { + "title": "音階", + "type": "integer" + }, + "lyric": { + "title": "音符の歌詞", + "type": "string" + } + }, + "required": [ + "frame_length", + "lyric" + ], + "title": "Note", + "type": "object" + }, + "ParseKanaBadRequest": { + "properties": { + "error_args": { + "additionalProperties": { + "type": "string" + }, + "title": "エラーを起こした箇所", + "type": "object" + }, + "error_name": { + "description": "|name|description|\n|---|---|\n| UNKNOWN_TEXT | 判別できない読み仮名があります: {text} |\n| ACCENT_TOP | 句頭にアクセントは置けません: {text} |\n| ACCENT_TWICE | 1つのアクセント句に二つ以上のアクセントは置けません: {text} |\n| ACCENT_NOTFOUND | アクセントを指定していないアクセント句があります: {text} |\n| EMPTY_PHRASE | {position}番目のアクセント句が空白です |\n| INTERROGATION_MARK_NOT_AT_END | アクセント句末以外に「?」は置けません: {text} |\n| INFINITE_LOOP | 処理時に無限ループになってしまいました...バグ報告をお願いします。 |", + "title": "エラー名", + "type": "string" + }, + "text": { + "title": "エラーメッセージ", + "type": "string" + } + }, + "required": [ + "text", + "error_name", + "error_args" + ], + "title": "ParseKanaBadRequest", + "type": "object" + }, + "Preset": { + "description": "プリセット情報", + "properties": { + "id": { + "title": "プリセットID", + "type": "integer" + }, + "intonationScale": { + "title": "全体の抑揚", + "type": "number" + }, + "name": { + "title": "プリセット名", + "type": "string" + }, + "pitchScale": { + "title": "全体の音高", + "type": "number" + }, + "postPhonemeLength": { + "title": "音声の後の無音時間", + "type": "number" + }, + "prePhonemeLength": { + "title": "音声の前の無音時間", + "type": "number" + }, + "speaker_uuid": { + "title": "話者のUUID", + "type": "string" + }, + "speedScale": { + "title": "全体の話速", + "type": "number" + }, + "style_id": { + "title": "スタイルID", + "type": "integer" + }, + "volumeScale": { + "title": "全体の音量", + "type": "number" + } + }, + "required": [ + "id", + "name", + "speaker_uuid", + "style_id", + "speedScale", + "pitchScale", + "intonationScale", + "volumeScale", + "prePhonemeLength", + "postPhonemeLength" + ], + "title": "Preset", + "type": "object" + }, + "Score": { + "description": "楽譜情報", + "properties": { + "notes": { + "items": { + "$ref": "#/components/schemas/Note" + }, + "title": "音符のリスト", + "type": "array" + } + }, + "required": [ + "notes" + ], + "title": "Score", + "type": "object" + }, + "Speaker": { + "description": "話者情報", + "properties": { + "name": { + "title": "名前", + "type": "string" + }, + "speaker_uuid": { + "title": "話者のUUID", + "type": "string" + }, + "styles": { + "items": { + "$ref": "#/components/schemas/SpeakerStyle" + }, + "title": "スタイルの一覧", + "type": "array" + }, + "supported_features": { + "allOf": [ + { + "$ref": "#/components/schemas/SpeakerSupportedFeatures" + } + ], + "title": "話者の対応機能" + }, + "version": { + "default": "話者のバージョン", + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "speaker_uuid", + "styles" + ], + "title": "Speaker", + "type": "object" + }, + "SpeakerInfo": { + "description": "話者の追加情報", + "properties": { + "policy": { + "title": "policy.md", + "type": "string" + }, + "portrait": { + "title": "portrait.pngをbase64エンコードしたもの", + "type": "string" + }, + "style_infos": { + "items": { + "$ref": "#/components/schemas/StyleInfo" + }, + "title": "スタイルの追加情報", + "type": "array" + } + }, + "required": [ + "policy", + "portrait", + "style_infos" + ], + "title": "SpeakerInfo", + "type": "object" + }, + "SpeakerStyle": { + "description": "話者のスタイル情報", + "properties": { + "id": { + "title": "スタイルID", + "type": "integer" + }, + "name": { + "title": "スタイル名", + "type": "string" + }, + "type": { + "default": "talk", + "enum": [ + "talk", + "singing_teacher", + "frame_decode", + "sing" + ], + "title": "モデルの種類。talk:音声合成クエリの作成と音声合成が可能。singing_teacher:歌唱音声合成用のクエリの作成が可能。frame_decode:歌唱音声合成が可能。sing:歌唱音声合成用のクエリの作成と歌唱音声合成が可能。", + "type": "string" + } + }, + "required": [ + "name", + "id" + ], + "title": "SpeakerStyle", + "type": "object" + }, + "SpeakerSupportPermittedSynthesisMorphing": { + "description": "An enumeration.", + "enum": [ + "ALL", + "SELF_ONLY", + "NOTHING" + ], + "title": "SpeakerSupportPermittedSynthesisMorphing", + "type": "string" + }, + "SpeakerSupportedFeatures": { + "description": "話者の対応機能の情報", + "properties": { + "permitted_synthesis_morphing": { + "allOf": [ + { + "$ref": "#/components/schemas/SpeakerSupportPermittedSynthesisMorphing" + } + ], + "default": "ALL", + "title": "モーフィング機能への対応" + } + }, + "title": "SpeakerSupportedFeatures", + "type": "object" + }, + "StyleInfo": { + "description": "スタイルの追加情報", + "properties": { + "icon": { + "title": "当該スタイルのアイコンをbase64エンコードしたもの", + "type": "string" + }, + "id": { + "title": "スタイルID", + "type": "integer" + }, + "portrait": { + "title": "当該スタイルのportrait.pngをbase64エンコードしたもの", + "type": "string" + }, + "voice_samples": { + "items": { + "type": "string" + }, + "title": "voice_sampleのwavファイルをbase64エンコードしたもの", + "type": "array" + } + }, + "required": [ + "id", + "icon", + "voice_samples" + ], + "title": "StyleInfo", + "type": "object" + }, + "SupportedDevicesInfo": { + "description": "対応しているデバイスの情報", + "properties": { + "cpu": { + "title": "CPUに対応しているか", + "type": "boolean" + }, + "cuda": { + "title": "CUDA(Nvidia GPU)に対応しているか", + "type": "boolean" + }, + "dml": { + "title": "DirectML(Nvidia GPU/Radeon GPU等)に対応しているか", + "type": "boolean" + } + }, + "required": [ + "cpu", + "cuda", + "dml" + ], + "title": "SupportedDevicesInfo", + "type": "object" + }, + "SupportedFeatures": { + "description": "エンジンが持つ機能の一覧", + "properties": { + "adjust_intonation_scale": { + "title": "全体の抑揚の調整", + "type": "boolean" + }, + "adjust_mora_pitch": { + "title": "モーラごとの音高の調整", + "type": "boolean" + }, + "adjust_phoneme_length": { + "title": "音素ごとの長さの調整", + "type": "boolean" + }, + "adjust_pitch_scale": { + "title": "全体の音高の調整", + "type": "boolean" + }, + "adjust_speed_scale": { + "title": "全体の話速の調整", + "type": "boolean" + }, + "adjust_volume_scale": { + "title": "全体の音量の調整", + "type": "boolean" + }, + "interrogative_upspeak": { + "title": "疑問文の自動調整", + "type": "boolean" + }, + "manage_library": { + "title": "音声ライブラリのインストール・アンインストール", + "type": "boolean" + }, + "sing": { + "title": "歌唱音声合成", + "type": "boolean" + }, + "synthesis_morphing": { + "title": "2種類のスタイルでモーフィングした音声を合成", + "type": "boolean" + } + }, + "required": [ + "adjust_mora_pitch", + "adjust_phoneme_length", + "adjust_speed_scale", + "adjust_pitch_scale", + "adjust_intonation_scale", + "adjust_volume_scale", + "interrogative_upspeak", + "synthesis_morphing" + ], + "title": "SupportedFeatures", + "type": "object" + }, + "UpdateInfo": { + "description": "エンジンのアップデート情報", + "properties": { + "contributors": { + "items": { + "type": "string" + }, + "title": "貢献者名", + "type": "array" + }, + "descriptions": { + "items": { + "type": "string" + }, + "title": "アップデートの詳細についての説明", + "type": "array" + }, + "version": { + "title": "エンジンのバージョン名", + "type": "string" + } + }, + "required": [ + "version", + "descriptions" + ], + "title": "UpdateInfo", + "type": "object" + }, + "UserDictWord": { + "description": "辞書のコンパイルに使われる情報", + "properties": { + "accent_associative_rule": { + "title": "アクセント結合規則", + "type": "string" + }, + "accent_type": { + "title": "アクセント型", + "type": "integer" + }, + "context_id": { + "default": 1348, + "title": "文脈ID", + "type": "integer" + }, + "inflectional_form": { + "title": "活用形", + "type": "string" + }, + "inflectional_type": { + "title": "活用型", + "type": "string" + }, + "mora_count": { + "title": "モーラ数", + "type": "integer" + }, + "part_of_speech": { + "title": "品詞", + "type": "string" + }, + "part_of_speech_detail_1": { + "title": "品詞細分類1", + "type": "string" + }, + "part_of_speech_detail_2": { + "title": "品詞細分類2", + "type": "string" + }, + "part_of_speech_detail_3": { + "title": "品詞細分類3", + "type": "string" + }, + "priority": { + "maximum": 10.0, + "minimum": 0.0, + "title": "優先度", + "type": "integer" + }, + "pronunciation": { + "title": "発音", + "type": "string" + }, + "stem": { + "title": "原形", + "type": "string" + }, + "surface": { + "title": "表層形", + "type": "string" + }, + "yomi": { + "title": "読み", + "type": "string" + } + }, + "required": [ + "surface", + "priority", + "part_of_speech", + "part_of_speech_detail_1", + "part_of_speech_detail_2", + "part_of_speech_detail_3", + "inflectional_type", + "inflectional_form", + "stem", + "yomi", + "pronunciation", + "accent_type", + "accent_associative_rule" + ], + "title": "UserDictWord", + "type": "object" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + }, + "VvlibManifest": { + "description": "vvlib(VOICEVOX Library)に関する情報", + "properties": { + "brand_name": { + "title": "エンジンのブランド名", + "type": "string" + }, + "engine_name": { + "title": "エンジン名", + "type": "string" + }, + "engine_uuid": { + "title": "エンジンのUUID", + "type": "string" + }, + "manifest_version": { + "title": "マニフェストバージョン", + "type": "string" + }, + "name": { + "title": "音声ライブラリ名", + "type": "string" + }, + "uuid": { + "title": "音声ライブラリのUUID", + "type": "string" + }, + "version": { + "title": "音声ライブラリバージョン", + "type": "string" + } + }, + "required": [ + "manifest_version", + "name", + "version", + "uuid", + "brand_name", + "engine_name", + "engine_uuid" + ], + "title": "VvlibManifest", + "type": "object" + }, + "WordTypes": { + "description": "fastapiでword_type引数を検証する時に使用するクラス", + "enum": [ + "PROPER_NOUN", + "COMMON_NOUN", + "VERB", + "ADJECTIVE", + "SUFFIX" + ], + "title": "WordTypes", + "type": "string" + } + } + }, + "info": { + "description": "VOICEVOXの音声合成エンジンです。", + "title": "VOICEVOX Engine", + "version": "latest" + }, + "openapi": "3.1.0", + "paths": { + "/accent_phrases": { + "post": { + "description": "テキストからアクセント句を得ます。\nis_kanaが`true`のとき、テキストは次のAquesTalk 風記法で解釈されます。デフォルトは`false`です。\n* 全てのカナはカタカナで記述される\n* アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。\n* カナの手前に`_`を入れるとそのカナは無声化される\n* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。\n* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。", + "operationId": "accent_phrases_accent_phrases_post", + "parameters": [ + { + "in": "query", + "name": "text", + "required": true, + "schema": { + "title": "Text", + "type": "string" + } + }, + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "is_kana", + "required": false, + "schema": { + "default": false, + "title": "Is Kana", + "type": "boolean" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Response Accent Phrases Accent Phrases Post", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParseKanaBadRequest" + } + } + }, + "description": "読み仮名のパースに失敗" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "テキストからアクセント句を得る", + "tags": [ + "クエリ編集" + ] + } + }, + "/add_preset": { + "post": { + "description": "新しいプリセットを追加します\n\nParameters\n-------\npreset: Preset\n 新しいプリセット。\n プリセットIDが既存のものと重複している場合は、新規のプリセットIDが採番されます。\n\nReturns\n-------\nid: int\n 追加したプリセットのプリセットID", + "operationId": "add_preset_add_preset_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Preset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Add Preset Add Preset Post", + "type": "integer" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Add Preset", + "tags": [ + "その他" + ] + } + }, + "/audio_query": { + "post": { + "description": "音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。", + "operationId": "audio_query_audio_query_post", + "parameters": [ + { + "in": "query", + "name": "text", + "required": true, + "schema": { + "title": "Text", + "type": "string" + } + }, + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioQuery" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "音声合成用のクエリを作成する", + "tags": [ + "クエリ作成" + ] + } + }, + "/audio_query_from_preset": { + "post": { + "description": "音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。", + "operationId": "audio_query_from_preset_audio_query_from_preset_post", + "parameters": [ + { + "in": "query", + "name": "text", + "required": true, + "schema": { + "title": "Text", + "type": "string" + } + }, + { + "in": "query", + "name": "preset_id", + "required": true, + "schema": { + "title": "Preset Id", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioQuery" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "音声合成用のクエリをプリセットを用いて作成する", + "tags": [ + "クエリ作成" + ] + } + }, + "/cancellable_synthesis": { + "post": { + "operationId": "cancellable_synthesis_cancellable_synthesis_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "audio/wav": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "音声合成する(キャンセル可能)", + "tags": [ + "音声合成" + ] + } + }, + "/connect_waves": { + "post": { + "description": "base64エンコードされたwavデータを一纏めにし、wavファイルで返します。", + "operationId": "connect_waves_connect_waves_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "title": "Waves", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "audio/wav": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "base64エンコードされた複数のwavデータを一つに結合する", + "tags": [ + "その他" + ] + } + }, + "/core_versions": { + "get": { + "operationId": "core_versions_core_versions_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "title": "Response Core Versions Core Versions Get", + "type": "array" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Core Versions", + "tags": [ + "その他" + ] + } + }, + "/delete_preset": { + "post": { + "description": "既存のプリセットを削除します\n\nParameters\n-------\nid: int\n 削除するプリセットのプリセットID", + "operationId": "delete_preset_delete_preset_post", + "parameters": [ + { + "in": "query", + "name": "id", + "required": true, + "schema": { + "title": "Id", + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Delete Preset", + "tags": [ + "その他" + ] + } + }, + "/downloadable_libraries": { + "get": { + "description": "ダウンロード可能な音声ライブラリの情報を返します。\n\nReturns\n-------\nret_data: list[DownloadableLibrary]", + "operationId": "downloadable_libraries_downloadable_libraries_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DownloadableLibraryInfo" + }, + "title": "Response Downloadable Libraries Downloadable Libraries Get", + "type": "array" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Downloadable Libraries", + "tags": [ + "音声ライブラリ管理" + ] + } + }, + "/engine_manifest": { + "get": { + "operationId": "engine_manifest_engine_manifest_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EngineManifest" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Engine Manifest", + "tags": [ + "その他" + ] + } + }, + "/frame_synthesis": { + "post": { + "description": "歌唱音声合成を行います。", + "operationId": "frame_synthesis_frame_synthesis_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameAudioQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "audio/wav": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Frame Synthesis", + "tags": [ + "音声合成" + ] + } + }, + "/import_user_dict": { + "post": { + "description": "他のユーザー辞書をインポートします。\n\nParameters\n----------\nimport_dict_data: dict[str, UserDictWord]\n インポートするユーザー辞書のデータ\noverride: bool\n 重複したエントリがあった場合、上書きするかどうか", + "operationId": "import_user_dict_words_import_user_dict_post", + "parameters": [ + { + "in": "query", + "name": "override", + "required": true, + "schema": { + "title": "Override", + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "$ref": "#/components/schemas/UserDictWord" + }, + "title": "Import Dict Data", + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Import User Dict Words", + "tags": [ + "ユーザー辞書" + ] + } + }, + "/initialize_speaker": { + "post": { + "description": "指定されたスタイルを初期化します。\n実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。", + "operationId": "initialize_speaker_initialize_speaker_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "description": "既に初期化済みのスタイルの再初期化をスキップするかどうか", + "in": "query", + "name": "skip_reinit", + "required": false, + "schema": { + "default": false, + "description": "既に初期化済みのスタイルの再初期化をスキップするかどうか", + "title": "Skip Reinit", + "type": "boolean" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Initialize Speaker", + "tags": [ + "その他" + ] + } + }, + "/install_library/{library_uuid}": { + "post": { + "description": "音声ライブラリをインストールします。\n音声ライブラリのZIPファイルをリクエストボディとして送信してください。\n\nParameters\n----------\nlibrary_uuid: str\n 音声ライブラリのID", + "operationId": "install_library_install_library__library_uuid__post", + "parameters": [ + { + "in": "path", + "name": "library_uuid", + "required": true, + "schema": { + "title": "Library Uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Install Library", + "tags": [ + "音声ライブラリ管理" + ] + } + }, + "/installed_libraries": { + "get": { + "description": "インストールした音声ライブラリの情報を返します。\n\nReturns\n-------\nret_data: dict[str, InstalledLibrary]", + "operationId": "installed_libraries_installed_libraries_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "$ref": "#/components/schemas/InstalledLibraryInfo" + }, + "title": "Response Installed Libraries Installed Libraries Get", + "type": "object" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Installed Libraries", + "tags": [ + "音声ライブラリ管理" + ] + } + }, + "/is_initialized_speaker": { + "get": { + "description": "指定されたスタイルが初期化されているかどうかを返します。", + "operationId": "is_initialized_speaker_is_initialized_speaker_get", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Is Initialized Speaker Is Initialized Speaker Get", + "type": "boolean" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Is Initialized Speaker", + "tags": [ + "その他" + ] + } + }, + "/mora_data": { + "post": { + "operationId": "mora_data_mora_data_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Accent Phrases", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Response Mora Data Mora Data Post", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "アクセント句から音高・音素長を得る", + "tags": [ + "クエリ編集" + ] + } + }, + "/mora_length": { + "post": { + "operationId": "mora_length_mora_length_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Accent Phrases", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Response Mora Length Mora Length Post", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "アクセント句から音素長を得る", + "tags": [ + "クエリ編集" + ] + } + }, + "/mora_pitch": { + "post": { + "operationId": "mora_pitch_mora_pitch_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Accent Phrases", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AccentPhrase" + }, + "title": "Response Mora Pitch Mora Pitch Post", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "アクセント句から音高を得る", + "tags": [ + "クエリ編集" + ] + } + }, + "/morphable_targets": { + "post": { + "description": "指定されたベーススタイルに対してエンジン内の各話者がモーフィング機能を利用可能か返します。\nモーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。\nプロパティが存在しない場合は、モーフィングが許可されているとみなします。\n返り値のスタイルIDはstring型なので注意。", + "operationId": "morphable_targets_morphable_targets_post", + "parameters": [ + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "integer" + }, + "title": "Base Style Ids", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": { + "$ref": "#/components/schemas/MorphableTargetInfo" + }, + "type": "object" + }, + "title": "Response Morphable Targets Morphable Targets Post", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "指定したスタイルに対してエンジン内の話者がモーフィングが可能か判定する", + "tags": [ + "音声合成" + ] + } + }, + "/multi_synthesis": { + "post": { + "operationId": "multi_synthesis_multi_synthesis_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AudioQuery" + }, + "title": "Queries", + "type": "array" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/zip": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "複数まとめて音声合成する", + "tags": [ + "音声合成" + ] + } + }, + "/presets": { + "get": { + "description": "エンジンが保持しているプリセットの設定を返します\n\nReturns\n-------\npresets: list[Preset]\n プリセットのリスト", + "operationId": "get_presets_presets_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Preset" + }, + "title": "Response Get Presets Presets Get", + "type": "array" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Get Presets", + "tags": [ + "その他" + ] + } + }, + "/setting": { + "get": { + "description": "設定ページを返します。", + "operationId": "setting_get_setting_get", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "summary": "Setting Get", + "tags": [ + "設定" + ] + }, + "post": { + "description": "設定を更新します。", + "operationId": "setting_post_setting_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_setting_post_setting_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Setting Post", + "tags": [ + "設定" + ] + } + }, + "/sing_frame_audio_query": { + "post": { + "description": "歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。", + "operationId": "sing_frame_audio_query_sing_frame_audio_query_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Score" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameAudioQuery" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "歌唱音声合成用のクエリを作成する", + "tags": [ + "クエリ作成" + ] + } + }, + "/singer_info": { + "get": { + "description": "指定されたspeaker_uuidに関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。", + "operationId": "singer_info_singer_info_get", + "parameters": [ + { + "in": "query", + "name": "speaker_uuid", + "required": true, + "schema": { + "title": "Speaker Uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpeakerInfo" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Singer Info", + "tags": [ + "その他" + ] + } + }, + "/singers": { + "get": { + "operationId": "singers_singers_get", + "parameters": [ + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Speaker" + }, + "title": "Response Singers Singers Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Singers", + "tags": [ + "その他" + ] + } + }, + "/speaker_info": { + "get": { + "description": "指定されたspeaker_uuidに関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。", + "operationId": "speaker_info_speaker_info_get", + "parameters": [ + { + "in": "query", + "name": "speaker_uuid", + "required": true, + "schema": { + "title": "Speaker Uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpeakerInfo" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Speaker Info", + "tags": [ + "その他" + ] + } + }, + "/speakers": { + "get": { + "operationId": "speakers_speakers_get", + "parameters": [ + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Speaker" + }, + "title": "Response Speakers Speakers Get", + "type": "array" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Speakers", + "tags": [ + "その他" + ] + } + }, + "/supported_devices": { + "get": { + "operationId": "supported_devices_supported_devices_get", + "parameters": [ + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportedDevicesInfo" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Supported Devices", + "tags": [ + "その他" + ] + } + }, + "/synthesis": { + "post": { + "operationId": "synthesis_synthesis_post", + "parameters": [ + { + "in": "query", + "name": "speaker", + "required": true, + "schema": { + "title": "Speaker", + "type": "integer" + } + }, + { + "description": "疑問系のテキストが与えられたら語尾を自動調整する", + "in": "query", + "name": "enable_interrogative_upspeak", + "required": false, + "schema": { + "default": true, + "description": "疑問系のテキストが与えられたら語尾を自動調整する", + "title": "Enable Interrogative Upspeak", + "type": "boolean" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "audio/wav": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "音声合成する", + "tags": [ + "音声合成" + ] + } + }, + "/synthesis_morphing": { + "post": { + "description": "指定された2種類のスタイルで音声を合成、指定した割合でモーフィングした音声を得ます。\nモーフィングの割合は`morph_rate`で指定でき、0.0でベースのスタイル、1.0でターゲットのスタイルに近づきます。", + "operationId": "_synthesis_morphing_synthesis_morphing_post", + "parameters": [ + { + "in": "query", + "name": "base_speaker", + "required": true, + "schema": { + "title": "Base Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "target_speaker", + "required": true, + "schema": { + "title": "Target Speaker", + "type": "integer" + } + }, + { + "in": "query", + "name": "morph_rate", + "required": true, + "schema": { + "maximum": 1.0, + "minimum": 0.0, + "title": "Morph Rate", + "type": "number" + } + }, + { + "in": "query", + "name": "core_version", + "required": false, + "schema": { + "title": "Core Version", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AudioQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "audio/wav": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "2種類のスタイルでモーフィングした音声を合成する", + "tags": [ + "音声合成" + ] + } + }, + "/uninstall_library/{library_uuid}": { + "post": { + "description": "音声ライブラリをアンインストールします。\n\nParameters\n----------\nlibrary_uuid: str\n 音声ライブラリのID", + "operationId": "uninstall_library_uninstall_library__library_uuid__post", + "parameters": [ + { + "in": "path", + "name": "library_uuid", + "required": true, + "schema": { + "title": "Library Uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Uninstall Library", + "tags": [ + "音声ライブラリ管理" + ] + } + }, + "/update_preset": { + "post": { + "description": "既存のプリセットを更新します\n\nParameters\n-------\npreset: Preset\n 更新するプリセット。\n プリセットIDが更新対象と一致している必要があります。\n\nReturns\n-------\nid: int\n 更新したプリセットのプリセットID", + "operationId": "update_preset_update_preset_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Preset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Update Preset Update Preset Post", + "type": "integer" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Update Preset", + "tags": [ + "その他" + ] + } + }, + "/user_dict": { + "get": { + "description": "ユーザー辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。\n\nReturns\n-------\ndict[str, UserDictWord]\n 単語のUUIDとその詳細", + "operationId": "get_user_dict_words_user_dict_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "$ref": "#/components/schemas/UserDictWord" + }, + "title": "Response Get User Dict Words User Dict Get", + "type": "object" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Get User Dict Words", + "tags": [ + "ユーザー辞書" + ] + } + }, + "/user_dict_word": { + "post": { + "description": "ユーザー辞書に言葉を追加します。\n\nParameters\n----------\nsurface : str\n 言葉の表層形\npronunciation: str\n 言葉の発音(カタカナ)\naccent_type: int\n アクセント型(音が下がる場所を指す)\nword_type: WordTypes, optional\n PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか\npriority: int, optional\n 単語の優先度(0から10までの整数)\n 数字が大きいほど優先度が高くなる\n 1から9までの値を指定することを推奨", + "operationId": "add_user_dict_word_user_dict_word_post", + "parameters": [ + { + "in": "query", + "name": "surface", + "required": true, + "schema": { + "title": "Surface", + "type": "string" + } + }, + { + "in": "query", + "name": "pronunciation", + "required": true, + "schema": { + "title": "Pronunciation", + "type": "string" + } + }, + { + "in": "query", + "name": "accent_type", + "required": true, + "schema": { + "title": "Accent Type", + "type": "integer" + } + }, + { + "in": "query", + "name": "word_type", + "required": false, + "schema": { + "$ref": "#/components/schemas/WordTypes" + } + }, + { + "in": "query", + "name": "priority", + "required": false, + "schema": { + "maximum": 10.0, + "minimum": 0.0, + "title": "Priority", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Add User Dict Word User Dict Word Post", + "type": "string" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Add User Dict Word", + "tags": [ + "ユーザー辞書" + ] + } + }, + "/user_dict_word/{word_uuid}": { + "delete": { + "description": "ユーザー辞書に登録されている言葉を削除します。\n\nParameters\n----------\nword_uuid: str\n 削除する言葉のUUID", + "operationId": "delete_user_dict_word_user_dict_word__word_uuid__delete", + "parameters": [ + { + "in": "path", + "name": "word_uuid", + "required": true, + "schema": { + "title": "Word Uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Delete User Dict Word", + "tags": [ + "ユーザー辞書" + ] + }, + "put": { + "description": "ユーザー辞書に登録されている言葉を更新します。\n\nParameters\n----------\nsurface : str\n 言葉の表層形\npronunciation: str\n 言葉の発音(カタカナ)\naccent_type: int\n アクセント型(音が下がる場所を指す)\nword_uuid: str\n 更新する言葉のUUID\nword_type: WordTypes, optional\n PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか\npriority: int, optional\n 単語の優先度(0から10までの整数)\n 数字が大きいほど優先度が高くなる\n 1から9までの値を指定することを推奨", + "operationId": "rewrite_user_dict_word_user_dict_word__word_uuid__put", + "parameters": [ + { + "in": "path", + "name": "word_uuid", + "required": true, + "schema": { + "title": "Word Uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "surface", + "required": true, + "schema": { + "title": "Surface", + "type": "string" + } + }, + { + "in": "query", + "name": "pronunciation", + "required": true, + "schema": { + "title": "Pronunciation", + "type": "string" + } + }, + { + "in": "query", + "name": "accent_type", + "required": true, + "schema": { + "title": "Accent Type", + "type": "integer" + } + }, + { + "in": "query", + "name": "word_type", + "required": false, + "schema": { + "$ref": "#/components/schemas/WordTypes" + } + }, + { + "in": "query", + "name": "priority", + "required": false, + "schema": { + "maximum": 10.0, + "minimum": 0.0, + "title": "Priority", + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Rewrite User Dict Word", + "tags": [ + "ユーザー辞書" + ] + } + }, + "/validate_kana": { + "post": { + "description": "テキストがAquesTalk 風記法に従っているかどうかを判定します。\n従っていない場合はエラーが返ります。\n\nParameters\n----------\ntext: str\n 判定する対象の文字列", + "operationId": "validate_kana_validate_kana_post", + "parameters": [ + { + "in": "query", + "name": "text", + "required": true, + "schema": { + "title": "Text", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Validate Kana Validate Kana Post", + "type": "boolean" + } + } + }, + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParseKanaBadRequest" + } + } + }, + "description": "テキストが不正です" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "テキストがAquesTalk 風記法に従っているか判定する", + "tags": [ + "その他" + ] + } + }, + "/version": { + "get": { + "operationId": "version_version_get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "title": "Response Version Version Get", + "type": "string" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Version", + "tags": [ + "その他" + ] + } + } + } +} diff --git "a/test/e2e/__snapshots__/test_preset/test_\343\203\227\343\203\252\343\202\273\343\203\203\343\203\210\344\270\200\350\246\247\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" "b/test/e2e/__snapshots__/test_preset/test_\343\203\227\343\203\252\343\202\273\343\203\203\343\203\210\344\270\200\350\246\247\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" new file mode 100644 index 000000000..07e2707b8 --- /dev/null +++ "b/test/e2e/__snapshots__/test_preset/test_\343\203\227\343\203\252\343\202\273\343\203\203\343\203\210\344\270\200\350\246\247\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" @@ -0,0 +1,14 @@ +[ + { + "id": 1, + "intonationScale": 1.0, + "name": "サンプルプリセット", + "pitchScale": 0.0, + "postPhonemeLength": 0.1, + "prePhonemeLength": 0.1, + "speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", + "speedScale": 1.0, + "style_id": 0, + "volumeScale": 1.0 + } +] diff --git a/test/e2e/__snapshots__/test_setting.ambr b/test/e2e/__snapshots__/test_setting.ambr new file mode 100644 index 000000000..97c020182 --- /dev/null +++ b/test/e2e/__snapshots__/test_setting.ambr @@ -0,0 +1,373 @@ +# serializer version: 1 +# name: test_setting画面が取得できる + ''' + + + + + + + + VOICEVOX Engine 設定 + + + + + + + +
+

読み込み中です。表示には数秒かかることがあります。

+
+ + + + + + + + ''' +# --- diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" new file mode 100644 index 000000000..f2c119142 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy2 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:72ceb00f20b2a1e449f0b45973cc8b24", + "style_infos": [ + { + "icon": "MD5:5f2211c3144b8dee613056bef5893d60", + "id": 5, + "portrait": null, + "voice_samples": [ + "MD5:2b7f17f6751b9f0c76950ad3bcc1a619", + "MD5:4bc9f14cda818955cba931b1532e18fd", + "MD5:9ebfc3cf3fba47513a60c464fc57c705" + ] + }, + { + "icon": "MD5:375ecba26764b7c71ce61731b52f71f8", + "id": 7, + "portrait": null, + "voice_samples": [ + "MD5:fc93b361293ce128afd8f48d4cd89bc5", + "MD5:b74ee50cb135ccf29c0a1be2711f8cca", + "MD5:08c325d1cfe72209949a77c327b60302" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" new file mode 100644 index 000000000..1923de700 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy1 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:cab33c9fdf563682108666a012dc9853", + "style_infos": [ + { + "icon": "MD5:9dfc8b32d4afc3c933388ba85a8d8d12", + "id": 4, + "portrait": "MD5:2aba7f7037d00903dada4401582bf31a", + "voice_samples": [ + "MD5:49c763de77c5c6be4967900b08b561a9", + "MD5:d9736740e3735bbf45efd792f8af7383", + "MD5:58bda86215149663b605d0ba0db59bde" + ] + }, + { + "icon": "MD5:53b0f8ce874e450fc8cc5758d6ed2b03", + "id": 6, + "portrait": "MD5:6a79f7e6d8ca9087be9a0e39eac67e7b", + "voice_samples": [ + "MD5:e9fbdc80f22d91a1ad96612ea60391b4", + "MD5:8c403062ffc5fca5605aca46778ed512", + "MD5:95a626ec8a36a3a550d7ba4188937cb3" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" new file mode 100644 index 000000000..5c1b401fc --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[b1a81618-b27b-40d2-b0ea-27a9ad408c4b].json" @@ -0,0 +1,16 @@ +{ + "policy": "dummy4 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:70cb4a361935084f00f6956a4e8e4f32", + "style_infos": [ + { + "icon": "MD5:e1e2fab676912fc0796a5b23320a0b67", + "id": 9, + "portrait": null, + "voice_samples": [ + "MD5:fa1230e97dec17b814ec05da1709be19", + "MD5:714f4c4f2d3c51a1d9597a6960b8367c", + "MD5:bc47fd0d1ea9083c2f4621461ae072b8" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" new file mode 100644 index 000000000..e421371b7 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\346\255\214\346\211\213\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" @@ -0,0 +1,57 @@ +[ + { + "name": "dummy1", + "speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", + "styles": [ + { + "id": 4, + "name": "style2", + "type": "frame_decode" + }, + { + "id": 6, + "name": "style3", + "type": "frame_decode" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "ALL" + }, + "version": "mock" + }, + { + "name": "dummy2", + "speaker_uuid": "388f246b-8c41-4ac1-8e2d-5d79f3ff56d9", + "styles": [ + { + "id": 5, + "name": "style2", + "type": "frame_decode" + }, + { + "id": 7, + "name": "style3", + "type": "sing" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "SELF_ONLY" + }, + "version": "mock" + }, + { + "name": "dummy4", + "speaker_uuid": "b1a81618-b27b-40d2-b0ea-27a9ad408c4b", + "styles": [ + { + "id": 9, + "name": "style0", + "type": "sing" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "ALL" + }, + "version": "mock" + } +] diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" new file mode 100644 index 000000000..da10a2bf0 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[35b2c544-660e-401e-b503-0e14c635303a].json" @@ -0,0 +1,16 @@ +{ + "policy": "dummy3 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:90b250e0976f792e2fda2b1ad2643c7b", + "style_infos": [ + { + "icon": "MD5:541aeccd87319c0af159cfa13baf26cb", + "id": 8, + "portrait": "MD5:1bb8b584e8499d601a3f3bf0c3216391", + "voice_samples": [ + "MD5:148c72905d47a308cbdf9858c99ef9d7", + "MD5:46fda5f38dec0df94445066eee9ed128", + "MD5:61e7d2d3180c2c891cf096f50b98e317" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" new file mode 100644 index 000000000..b7dea7d76 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[388f246b-8c41-4ac1-8e2d-5d79f3ff56d9].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy2 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:72ceb00f20b2a1e449f0b45973cc8b24", + "style_infos": [ + { + "icon": "MD5:3248458ae11d28ec1eb482db7f1927d9", + "id": 1, + "portrait": null, + "voice_samples": [ + "MD5:2cdd82264a8b0ad508ff3f5a84d5c920", + "MD5:14b4a96141c6b9e86ce4f38adaac1fcb", + "MD5:4494752eec42b718ff3b9a3fb934596a" + ] + }, + { + "icon": "MD5:3e32a4a66bd2505cb75f91c8028d061c", + "id": 3, + "portrait": "MD5:1dd8a513f11c204c1449172b7a812be8", + "voice_samples": [ + "MD5:2bd7d3be714fdfdda2e96aa98888a9bd", + "MD5:10a9d6d4bcd02a6fa37d13c3f7335df1", + "MD5:6a21d1007f8957fca45843fde1e2d1c2" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" new file mode 100644 index 000000000..9d5d0588a --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\343\201\256\346\203\205\345\240\261\343\202\222\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213[7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff].json" @@ -0,0 +1,26 @@ +{ + "policy": "dummy1 policy\n\nhttps://voicevox.hiroshiba.jp/\n", + "portrait": "MD5:cab33c9fdf563682108666a012dc9853", + "style_infos": [ + { + "icon": "MD5:529b1750562ca339ba05c1b00f4b2854", + "id": 0, + "portrait": "MD5:6c1c461e54ba4f57d0c17171d17e1d80", + "voice_samples": [ + "MD5:85acb767ac22b1d17915c666cc5cee90", + "MD5:ad9e64177d28f960fb9ce40162cd82c2", + "MD5:16bf0d95c463fff08353a3452bdb8d7c" + ] + }, + { + "icon": "MD5:04cce28c375949935497ec3d5d015be9", + "id": 2, + "portrait": "MD5:0a4e78369adc266672d571f4c0663697", + "voice_samples": [ + "MD5:f508aca632a8f1cfd9eee7ed29cff96c", + "MD5:09984e27d23eee8af13809a0f621f7fd", + "MD5:e3f9a4df3f537bfb9d63d1791eda73e6" + ] + } + ] +} diff --git "a/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" new file mode 100644 index 000000000..bf6d37e51 --- /dev/null +++ "b/test/e2e/__snapshots__/test_speakers/test_\350\251\261\350\200\205\344\270\200\350\246\247\343\201\214\345\217\226\345\276\227\343\201\247\343\201\215\343\202\213.json" @@ -0,0 +1,57 @@ +[ + { + "name": "dummy1", + "speaker_uuid": "7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff", + "styles": [ + { + "id": 0, + "name": "style0", + "type": "talk" + }, + { + "id": 2, + "name": "style1", + "type": "talk" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "ALL" + }, + "version": "mock" + }, + { + "name": "dummy2", + "speaker_uuid": "388f246b-8c41-4ac1-8e2d-5d79f3ff56d9", + "styles": [ + { + "id": 1, + "name": "style0", + "type": "talk" + }, + { + "id": 3, + "name": "style1", + "type": "talk" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "SELF_ONLY" + }, + "version": "mock" + }, + { + "name": "dummy3", + "speaker_uuid": "35b2c544-660e-401e-b503-0e14c635303a", + "styles": [ + { + "id": 8, + "name": "style0", + "type": "talk" + } + ], + "supported_features": { + "permitted_synthesis_morphing": "NOTHING" + }, + "version": "mock" + } +] diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py index af21590c1..df5bb1744 100644 --- a/test/e2e/conftest.py +++ b/test/e2e/conftest.py @@ -1,29 +1,40 @@ from pathlib import Path import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient from run import generate_app -from voicevox_engine.preset import PresetManager -from voicevox_engine.setting import SettingLoader -from voicevox_engine.synthesis_engine import make_synthesis_engines +from voicevox_engine.core.core_initializer import initialize_cores +from voicevox_engine.preset.PresetManager import PresetManager +from voicevox_engine.setting.SettingLoader import SettingHandler +from voicevox_engine.tts_pipeline.tts_engine import make_tts_engines_from_cores from voicevox_engine.utility.core_version_utility import get_latest_core_version @pytest.fixture(scope="session") -def client(): - synthesis_engines = make_synthesis_engines(use_gpu=False) - latest_core_version = get_latest_core_version(versions=synthesis_engines.keys()) - setting_loader = SettingLoader(Path("./default_setting.yml")) +def app_params(): + cores = initialize_cores(use_gpu=False, enable_mock=True) + tts_engines = make_tts_engines_from_cores(cores) + latest_core_version = get_latest_core_version(versions=list(tts_engines.keys())) + setting_loader = SettingHandler(Path("./not_exist.yaml")) preset_manager = PresetManager( # FIXME: impl MockPresetManager preset_path=Path("./presets.yaml"), ) + return { + "tts_engines": tts_engines, + "cores": cores, + "latest_core_version": latest_core_version, + "setting_loader": setting_loader, + "preset_manager": preset_manager, + } - return TestClient( - generate_app( - synthesis_engines=synthesis_engines, - latest_core_version=latest_core_version, - setting_loader=setting_loader, - preset_manager=preset_manager, - ) - ) + +@pytest.fixture(scope="session") +def app(app_params: dict) -> FastAPI: + return generate_app(**app_params) + + +@pytest.fixture(scope="session") +def client(app: FastAPI) -> TestClient: + return TestClient(app) diff --git a/test/e2e/test_audio_query.py b/test/e2e/test_audio_query.py new file mode 100644 index 000000000..a77db614e --- /dev/null +++ b/test/e2e/test_audio_query.py @@ -0,0 +1,16 @@ +""" +AudioQuery APIのテスト +""" + +from test.utility import round_floats + +from fastapi.testclient import TestClient +from syrupy.assertion import SnapshotAssertion + + +def test_speakerを指定して音声合成クエリが取得できる( + client: TestClient, snapshot_json: SnapshotAssertion +) -> None: + response = client.post("/audio_query", params={"text": "テストです", "speaker": 0}) + assert response.status_code == 200 + assert snapshot_json == round_floats(response.json(), round_value=2) diff --git a/test/e2e/test_disable_api.py b/test/e2e/test_disable_api.py new file mode 100644 index 000000000..bdd16cbf8 --- /dev/null +++ b/test/e2e/test_disable_api.py @@ -0,0 +1,51 @@ +""" +APIを無効化するテスト +""" + +from typing import Literal + +from fastapi.testclient import TestClient +from run import generate_app + + +# clientとschemaとパスを受け取ってリクエストを送信し、レスポンスが403であることを確認する +def _assert_request_and_response_403( + client: TestClient, + method: Literal["post", "get", "put", "delete"], + path: str, +) -> None: + if method == "post": + response = client.post(path) + elif method == "get": + response = client.get(path) + elif method == "put": + response = client.put(path) + elif method == "delete": + response = client.delete(path) + else: + raise ValueError("methodはpost, get, put, deleteのいずれかである必要があります") + + assert response.status_code == 403, f"{method} {path} が403を返しませんでした" + + +def test_disable_mutable_api(app_params: dict) -> None: + """エンジンの静的なデータを変更するAPIを無効化するテスト""" + client = TestClient(generate_app(**app_params, disable_mutable_api=True)) + + # APIが無効化されているか確認 + _assert_request_and_response_403(client, "post", "/add_preset") + _assert_request_and_response_403(client, "post", "/update_preset") + _assert_request_and_response_403(client, "post", "/delete_preset") + _assert_request_and_response_403(client, "post", "/user_dict_word") + _assert_request_and_response_403(client, "put", "/user_dict_word/dummy") + _assert_request_and_response_403(client, "delete", "/user_dict_word/dummy") + _assert_request_and_response_403(client, "post", "/import_user_dict") + _assert_request_and_response_403(client, "post", "/setting") + + # FIXME: EngineManifestをDI可能にし、EngineManifestに従ってこれらのAPIを加える + # _assert_request_and_response_403(client, "post", "/install_library/dummy") + # _assert_request_and_response_403(client, "post", "/uninstall_library/dummy") + + # 他のAPIは有効 + response = client.get("/version") + assert response.status_code == 200 diff --git a/test/e2e/test_engine_manifest.py b/test/e2e/test_engine_manifest.py new file mode 100644 index 000000000..664fa93dc --- /dev/null +++ b/test/e2e/test_engine_manifest.py @@ -0,0 +1,14 @@ +""" +/engine_manifest APIのテスト +""" + +from test.utility import hash_long_string + +from fastapi.testclient import TestClient +from syrupy.assertion import SnapshotAssertion + + +def test_エンジンマニフェストを取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + response = client.get("/engine_manifest") + assert response.status_code == 200 + assert snapshot_json == hash_long_string(response.json()) diff --git a/test/e2e/test_openapi.py b/test/e2e/test_openapi.py new file mode 100644 index 000000000..d26a2b7c5 --- /dev/null +++ b/test/e2e/test_openapi.py @@ -0,0 +1,10 @@ +from typing import Any + +from fastapi import FastAPI +from syrupy.assertion import SnapshotAssertion + + +def test_OpenAPIの形が変わっていないことを確認(app: FastAPI, snapshot_json: SnapshotAssertion) -> None: + # 変更があった場合はREADMEの「スナップショットの更新」の手順で更新可能 + openapi: Any = app.openapi() # snapshot_jsonがmypyに対応していないのでワークアラウンド + assert snapshot_json == openapi diff --git a/test/e2e/test_preset.py b/test/e2e/test_preset.py new file mode 100644 index 000000000..d1020d07b --- /dev/null +++ b/test/e2e/test_preset.py @@ -0,0 +1,12 @@ +""" +プリセットAPIのテスト +""" + +from fastapi.testclient import TestClient +from syrupy.assertion import SnapshotAssertion + + +def test_プリセット一覧を取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + response = client.get("/presets") + assert response.status_code == 200 + assert snapshot_json == response.json() diff --git a/test/e2e/test_setting.py b/test/e2e/test_setting.py new file mode 100644 index 000000000..4400df904 --- /dev/null +++ b/test/e2e/test_setting.py @@ -0,0 +1,12 @@ +""" +setting APIのテスト +""" + +from fastapi.testclient import TestClient +from syrupy.assertion import SnapshotAssertion + + +def test_setting画面が取得できる(client: TestClient, snapshot: SnapshotAssertion) -> None: + response = client.get("/setting") + assert response.status_code == 200 + assert snapshot == response.content.decode("utf-8") diff --git a/test/e2e/test_speakers.py b/test/e2e/test_speakers.py new file mode 100644 index 000000000..598d41bbf --- /dev/null +++ b/test/e2e/test_speakers.py @@ -0,0 +1,46 @@ +""" +話者・歌手のテスト。 +TODO: 話者と歌手の両ドメイン共通のドメイン用語を定め、このテストファイル名を変更する。 +""" + +from test.utility import hash_long_string + +from fastapi.testclient import TestClient +from pydantic import parse_obj_as +from syrupy.assertion import SnapshotAssertion + +from voicevox_engine.metas.Metas import Speaker + + +def test_話者一覧が取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + response = client.get("/speakers") + assert response.status_code == 200 + assert snapshot_json == response.json() + + +def test_話者の情報を取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + speakers = parse_obj_as(list[Speaker], client.get("/speakers").json()) + for speaker in speakers: + response = client.get( + "/speaker_info", params={"speaker_uuid": speaker.speaker_uuid} + ) + assert snapshot_json( + name=speaker.speaker_uuid, + ) == hash_long_string(response.json()) + + +def test_歌手一覧が取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + response = client.get("/singers") + assert response.status_code == 200 + assert snapshot_json == response.json() + + +def test_歌手の情報を取得できる(client: TestClient, snapshot_json: SnapshotAssertion) -> None: + singers = parse_obj_as(list[Speaker], client.get("/singers").json()) + for singer in singers: + response = client.get( + "/singer_info", params={"speaker_uuid": singer.speaker_uuid} + ) + assert snapshot_json( + name=singer.speaker_uuid, + ) == hash_long_string(response.json()) diff --git a/test/e2e/test_user_dict_word.py b/test/e2e/test_user_dict_word.py new file mode 100644 index 000000000..87c4cd1f1 --- /dev/null +++ b/test/e2e/test_user_dict_word.py @@ -0,0 +1,24 @@ +""" +ユーザー辞書の言葉のAPIのテスト +""" + + +from fastapi.testclient import TestClient + + +def test_post_user_dict_word(client: TestClient) -> None: + true_params: dict[str, str | int] = { + "surface": "test", + "pronunciation": "テスト", + "accent_type": 1, + "word_type": "PROPER_NOUN", + "priority": 5, + } + + # 正常系 + response = client.post("/user_dict_word", params=true_params) + assert response.status_code == 200 + + # 範囲外の優先度はエラー + response = client.post("/user_dict_word", params={**true_params, "priority": 100}) + assert response.status_code == 422 diff --git a/test/e2e/test_validate_version.py b/test/e2e/test_validate_version.py index b431a4a44..be1ca649d 100644 --- a/test/e2e/test_validate_version.py +++ b/test/e2e/test_validate_version.py @@ -3,7 +3,7 @@ from voicevox_engine import __version__ -def test_fetch_version_success(client: TestClient): +def test_fetch_version_success(client: TestClient) -> None: response = client.get("/version") assert response.status_code == 200 assert response.json() == __version__ diff --git a/test/presets-test-1.yaml b/test/preset/presets-test-1.yaml similarity index 100% rename from test/presets-test-1.yaml rename to test/preset/presets-test-1.yaml diff --git a/test/presets-test-2.yaml b/test/preset/presets-test-2.yaml similarity index 100% rename from test/presets-test-2.yaml rename to test/preset/presets-test-2.yaml diff --git a/test/presets-test-3.yaml b/test/preset/presets-test-3.yaml similarity index 100% rename from test/presets-test-3.yaml rename to test/preset/presets-test-3.yaml diff --git a/test/presets-test-4.yaml b/test/preset/presets-test-4.yaml similarity index 100% rename from test/presets-test-4.yaml rename to test/preset/presets-test-4.yaml diff --git a/test/test_preset.py b/test/preset/test_preset.py similarity index 84% rename from test/test_preset.py rename to test/preset/test_preset.py index 3a162829c..ffa0d698c 100644 --- a/test/test_preset.py +++ b/test/preset/test_preset.py @@ -4,7 +4,14 @@ from tempfile import TemporaryDirectory from unittest import TestCase -from voicevox_engine.preset import Preset, PresetError, PresetManager +from voicevox_engine.preset.Preset import Preset +from voicevox_engine.preset.PresetError import PresetError +from voicevox_engine.preset.PresetManager import PresetManager + +presets_test_1_yaml_path = Path("test/preset/presets-test-1.yaml") +presets_test_2_yaml_path = Path("test/preset/presets-test-2.yaml") +presets_test_3_yaml_path = Path("test/preset/presets-test-3.yaml") +presets_test_4_yaml_path = Path("test/preset/presets-test-4.yaml") class TestPresetManager(TestCase): @@ -16,29 +23,29 @@ def tearDown(self): self.tmp_dir.cleanup() def test_validation(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-1.yaml")) + preset_manager = PresetManager(preset_path=presets_test_1_yaml_path) presets = preset_manager.load_presets() self.assertFalse(presets is None) def test_validation_same(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-1.yaml")) + preset_manager = PresetManager(preset_path=presets_test_1_yaml_path) presets = preset_manager.load_presets() presets2 = preset_manager.load_presets() self.assertFalse(presets is None) self.assertEqual(presets, presets2) def test_validation_2(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-2.yaml")) + preset_manager = PresetManager(preset_path=presets_test_2_yaml_path) with self.assertRaises(PresetError, msg="プリセットの設定ファイルにミスがあります"): preset_manager.load_presets() def test_preset_id(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-3.yaml")) + preset_manager = PresetManager(preset_path=presets_test_3_yaml_path) with self.assertRaises(PresetError, msg="プリセットのidに重複があります"): preset_manager.load_presets() def test_empty_file(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-4.yaml")) + preset_manager = PresetManager(preset_path=presets_test_4_yaml_path) with self.assertRaises(PresetError, msg="プリセットの設定ファイルが空の内容です"): preset_manager.load_presets() @@ -49,7 +56,7 @@ def test_not_exist_file(self): def test_add_preset(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -74,7 +81,7 @@ def test_add_preset(self): remove(temp_path) def test_add_preset_load_failure(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-2.yaml")) + preset_manager = PresetManager(preset_path=presets_test_2_yaml_path) with self.assertRaises(PresetError, msg="プリセットの設定ファイルにミスがあります"): preset_manager.add_preset( Preset( @@ -95,7 +102,7 @@ def test_add_preset_load_failure(self): def test_add_preset_conflict_id(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -121,7 +128,7 @@ def test_add_preset_conflict_id(self): def test_add_preset_conflict_id2(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -147,7 +154,7 @@ def test_add_preset_conflict_id2(self): def test_add_preset_write_failure(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -164,8 +171,8 @@ def test_add_preset_write_failure(self): } ) preset_manager.load_presets() - preset_manager.load_presets = lambda: [] - preset_manager.preset_path = "" + preset_manager.load_presets = lambda: [] # type:ignore[method-assign] + preset_manager.preset_path = "" # type: ignore[assignment] with self.assertRaises(PresetError, msg="プリセットの設定ファイルに書き込み失敗しました"): preset_manager.add_preset(preset) self.assertEqual(len(preset_manager.presets), 2) @@ -173,7 +180,7 @@ def test_add_preset_write_failure(self): def test_update_preset(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -198,7 +205,7 @@ def test_update_preset(self): remove(temp_path) def test_update_preset_load_failure(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-2.yaml")) + preset_manager = PresetManager(preset_path=presets_test_2_yaml_path) with self.assertRaises(PresetError, msg="プリセットの設定ファイルにミスがあります"): preset_manager.update_preset( Preset( @@ -219,7 +226,7 @@ def test_update_preset_load_failure(self): def test_update_preset_not_found(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -242,7 +249,7 @@ def test_update_preset_not_found(self): def test_update_preset_write_failure(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset = Preset( **{ @@ -259,8 +266,8 @@ def test_update_preset_write_failure(self): } ) preset_manager.load_presets() - preset_manager.load_presets = lambda: [] - preset_manager.preset_path = "" + preset_manager.load_presets = lambda: [] # type:ignore[method-assign] + preset_manager.preset_path = "" # type: ignore[assignment] with self.assertRaises(PresetError, msg="プリセットの設定ファイルに書き込み失敗しました"): preset_manager.update_preset(preset) self.assertEqual(len(preset_manager.presets), 2) @@ -269,7 +276,7 @@ def test_update_preset_write_failure(self): def test_delete_preset(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) id = preset_manager.delete_preset(1) self.assertEqual(id, 1) @@ -277,13 +284,13 @@ def test_delete_preset(self): remove(temp_path) def test_delete_preset_load_failure(self): - preset_manager = PresetManager(preset_path=Path("test/presets-test-2.yaml")) + preset_manager = PresetManager(preset_path=presets_test_2_yaml_path) with self.assertRaises(PresetError, msg="プリセットの設定ファイルにミスがあります"): preset_manager.delete_preset(10) def test_delete_preset_not_found(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) with self.assertRaises(PresetError, msg="削除対象のプリセットが存在しません"): preset_manager.delete_preset(10) @@ -292,11 +299,11 @@ def test_delete_preset_not_found(self): def test_delete_preset_write_failure(self): temp_path = self.tmp_dir_path / "presets-test-temp.yaml" - copyfile(Path("test/presets-test-1.yaml"), temp_path) + copyfile(presets_test_1_yaml_path, temp_path) preset_manager = PresetManager(preset_path=temp_path) preset_manager.load_presets() - preset_manager.load_presets = lambda: [] - preset_manager.preset_path = "" + preset_manager.load_presets = lambda: [] # type:ignore[method-assign] + preset_manager.preset_path = "" # type: ignore[assignment] with self.assertRaises(PresetError, msg="プリセットの設定ファイルに書き込み失敗しました"): preset_manager.delete_preset(1) self.assertEqual(len(preset_manager.presets), 2) diff --git a/test/setting-test-load-1.yaml b/test/setting/setting-test-load-1.yaml similarity index 100% rename from test/setting-test-load-1.yaml rename to test/setting/setting-test-load-1.yaml diff --git a/test/setting-test-load-2.yaml b/test/setting/setting-test-load-2.yaml similarity index 100% rename from test/setting-test-load-2.yaml rename to test/setting/setting-test-load-2.yaml diff --git a/test/setting-test-load-3.yaml b/test/setting/setting-test-load-3.yaml similarity index 100% rename from test/setting-test-load-3.yaml rename to test/setting/setting-test-load-3.yaml diff --git a/test/test_setting.py b/test/setting/test_setting.py similarity index 64% rename from test/test_setting.py rename to test/setting/test_setting.py index 494e3095e..468e76b11 100644 --- a/test/test_setting.py +++ b/test/setting/test_setting.py @@ -2,7 +2,8 @@ from tempfile import TemporaryDirectory from unittest import TestCase -from voicevox_engine.setting import CorsPolicyMode, Setting, SettingLoader +from voicevox_engine.setting.Setting import CorsPolicyMode, Setting +from voicevox_engine.setting.SettingLoader import SettingHandler class TestSettingLoader(TestCase): @@ -11,8 +12,8 @@ def setUp(self): self.tmp_dir_path = Path(self.tmp_dir.name) def test_loading_1(self): - setting_loader = SettingLoader(Path("not_exist.yaml")) - settings = setting_loader.load_setting_file() + setting_loader = SettingHandler(Path("not_exist.yaml")) + settings = setting_loader.load() self.assertEqual( settings.dict(), @@ -20,10 +21,10 @@ def test_loading_1(self): ) def test_loading_2(self): - setting_loader = SettingLoader( - setting_file_path=Path("test/setting-test-load-1.yaml") + setting_loader = SettingHandler( + setting_file_path=Path("test/setting/setting-test-load-1.yaml") ) - settings = setting_loader.load_setting_file() + settings = setting_loader.load() self.assertEqual( settings.dict(), @@ -31,10 +32,10 @@ def test_loading_2(self): ) def test_loading_3(self): - setting_loader = SettingLoader( - setting_file_path=Path("test/setting-test-load-2.yaml") + setting_loader = SettingHandler( + setting_file_path=Path("test/setting/setting-test-load-2.yaml") ) - settings = setting_loader.load_setting_file() + settings = setting_loader.load() self.assertEqual( settings.dict(), @@ -42,10 +43,10 @@ def test_loading_3(self): ) def test_loading_4(self): - setting_loader = SettingLoader( - setting_file_path=Path("test/setting-test-load-3.yaml") + setting_loader = SettingHandler( + setting_file_path=Path("test/setting/setting-test-load-3.yaml") ) - settings = setting_loader.load_setting_file() + settings = setting_loader.load() self.assertEqual( settings.dict(), @@ -56,15 +57,15 @@ def test_loading_4(self): ) def test_dump(self): - setting_loader = SettingLoader( + setting_loader = SettingHandler( setting_file_path=Path(self.tmp_dir_path / "setting-test-dump.yaml") ) settings = Setting(cors_policy_mode=CorsPolicyMode.localapps) - setting_loader.dump_setting_file(settings) + setting_loader.save(settings) self.assertTrue(setting_loader.setting_file_path.is_file()) self.assertEqual( - setting_loader.load_setting_file().dict(), + setting_loader.load().dict(), {"allow_origin": None, "cors_policy_mode": CorsPolicyMode.localapps}, ) diff --git a/test/test_connect_base64_waves.py b/test/test_connect_base64_waves.py index ac9dfb841..96cf92d1b 100644 --- a/test/test_connect_base64_waves.py +++ b/test/test_connect_base64_waves.py @@ -5,21 +5,25 @@ import numpy as np import numpy.testing import soundfile +from numpy.typing import NDArray from soxr import resample -from voicevox_engine.utility import ConnectBase64WavesException, connect_base64_waves +from voicevox_engine.utility.connect_base64_waves import ( + ConnectBase64WavesException, + connect_base64_waves, +) def generate_sine_wave_ndarray( seconds: float, samplerate: int, frequency: float -) -> np.ndarray: +) -> NDArray[np.float32]: x = np.linspace(0, seconds, int(seconds * samplerate), endpoint=False) wave = np.sin(2 * np.pi * frequency * x).astype(np.float32) return wave -def encode_bytes(wave_ndarray: np.ndarray, samplerate: int) -> bytes: +def encode_bytes(wave_ndarray: NDArray[np.float32], samplerate: int) -> bytes: wave_bio = io.BytesIO() soundfile.write( file=wave_bio, diff --git a/test/test_core_version_utility.py b/test/test_core_version_utility.py index e96ba8009..7ac191011 100644 --- a/test/test_core_version_utility.py +++ b/test/test_core_version_utility.py @@ -1,6 +1,9 @@ from unittest import TestCase -from voicevox_engine.utility import get_latest_core_version, parse_core_version +from voicevox_engine.utility.core_version_utility import ( + get_latest_core_version, + parse_core_version, +) class TestCoreVersion(TestCase): diff --git a/test/test_full_context_label.py b/test/test_full_context_label.py deleted file mode 100644 index 7cdde34f4..000000000 --- a/test/test_full_context_label.py +++ /dev/null @@ -1,404 +0,0 @@ -from copy import deepcopy -from itertools import chain -from unittest import TestCase - -from voicevox_engine.full_context_label import ( - AccentPhrase, - BreathGroup, - Mora, - Phoneme, - Utterance, -) - - -class TestBasePhonemes(TestCase): - def setUp(self): - super().setUp() - # pyopenjtalk.extract_fullcontext("こんにちは、ヒホです。")の結果 - # 出来る限りテスト内で他のライブラリに依存しないため、 - # またテスト内容を透明化するために、テストケースを生成している - self.test_case_hello_hiho = [ - # sil (無音) - "xx^xx-sil+k=o/A:xx+xx+xx/B:xx-xx_xx/C:xx_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:5_5%0_xx_xx/H:xx_xx/I:xx-xx" - + "@xx+xx&xx-xx|xx+xx/J:1_5/K:2+2-9", - # k - "xx^sil-k+o=N/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # o - "sil^k-o+N=n/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # N (ん) - "k^o-N+n=i/A:-3+2+4/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # n - "o^N-n+i=ch/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # i - "N^n-i+ch=i/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # ch - "n^i-ch+i=w/A:-1+4+2/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # i - "i^ch-i+w=a/A:-1+4+2/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # w - "ch^i-w+a=pau/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # a - "i^w-a+pau=h/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" - + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" - + "@1+2&1-2|1+9/J:1_4/K:2+2-9", - # pau (読点) - "w^a-pau+h=i/A:xx+xx+xx/B:09-xx_xx/C:xx_xx+xx/D:09+xx_xx/E:5_5!0_xx-xx" - + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:4_1%0_xx_xx/H:1_5/I:xx-xx" - + "@xx+xx&xx-xx|xx+xx/J:1_4/K:2+2-9", - # h - "a^pau-h+i=h/A:0+1+4/B:09-xx_xx/C:09_xx+xx/D:22+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # i - "pau^h-i+h=o/A:0+1+4/B:09-xx_xx/C:09_xx+xx/D:22+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # h - "h^i-h+o=d/A:1+2+3/B:09-xx_xx/C:22_xx+xx/D:10+7_2/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # o - "i^h-o+d=e/A:1+2+3/B:09-xx_xx/C:22_xx+xx/D:10+7_2/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # d - "h^o-d+e=s/A:2+3+2/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # e - "o^d-e+s=U/A:2+3+2/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # s - "d^e-s+U=sil/A:3+4+1/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # U (無声母音) - "e^s-U+sil=xx/A:3+4+1/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" - + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" - + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", - # sil (無音) - "s^U-sil+xx=xx/A:xx+xx+xx/B:10-7_2/C:xx_xx+xx/D:xx+xx_xx/E:4_1!0_xx-xx" - + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:xx_xx%xx_xx_xx/H:1_4/I:xx-xx" - + "@xx+xx&xx-xx|xx+xx/J:xx_xx/K:2+2-9", - ] - self.phonemes_hello_hiho = [ - Phoneme.from_label(label) for label in self.test_case_hello_hiho - ] - - -class TestPhoneme(TestBasePhonemes): - def test_phoneme(self): - self.assertEqual( - " ".join([phoneme.phoneme for phoneme in self.phonemes_hello_hiho]), - "sil k o N n i ch i w a pau h i h o d e s U sil", - ) - - def test_is_pause(self): - self.assertEqual( - [phoneme.is_pause() for phoneme in self.phonemes_hello_hiho], - [ - True, # sil - False, # k - False, # o - False, # N - False, # n - False, # i - False, # ch - False, # i - False, # w - False, # a - True, # pau - False, # h - False, # i - False, # h - False, # o - False, # d - False, # e - False, # s - False, # u - True, # sil - ], - ) - - def test_label(self) -> None: - self.assertEqual( - [phoneme.label for phoneme in self.phonemes_hello_hiho], - self.test_case_hello_hiho, - ) - - -class TestMora(TestBasePhonemes): - def setUp(self) -> None: - super().setUp() - # contexts["a2"] == "1" ko - self.mora_hello_1 = Mora( - consonant=self.phonemes_hello_hiho[1], vowel=self.phonemes_hello_hiho[2] - ) - # contexts["a2"] == "2" N - self.mora_hello_2 = Mora(consonant=None, vowel=self.phonemes_hello_hiho[3]) - # contexts["a2"] == "3" ni - self.mora_hello_3 = Mora( - consonant=self.phonemes_hello_hiho[4], vowel=self.phonemes_hello_hiho[5] - ) - # contexts["a2"] == "4" chi - self.mora_hello_4 = Mora( - consonant=self.phonemes_hello_hiho[6], vowel=self.phonemes_hello_hiho[7] - ) - # contexts["a2"] == "5" wa - self.mora_hello_5 = Mora( - consonant=self.phonemes_hello_hiho[8], vowel=self.phonemes_hello_hiho[9] - ) - # contexts["a2"] == "1" hi - self.mora_hiho_1 = Mora( - consonant=self.phonemes_hello_hiho[11], vowel=self.phonemes_hello_hiho[12] - ) - # contexts["a2"] == "2" ho - self.mora_hiho_2 = Mora( - consonant=self.phonemes_hello_hiho[13], vowel=self.phonemes_hello_hiho[14] - ) - # contexts["a2"] == "3" de - self.mora_hiho_3 = Mora( - consonant=self.phonemes_hello_hiho[15], vowel=self.phonemes_hello_hiho[16] - ) - # contexts["a2"] == "1" sU - self.mora_hiho_4 = Mora( - consonant=self.phonemes_hello_hiho[17], vowel=self.phonemes_hello_hiho[18] - ) - - def assert_phonemes(self, mora: Mora, mora_str: str) -> None: - self.assertEqual( - "".join([phoneme.phoneme for phoneme in mora.phonemes]), mora_str - ) - - def assert_labels(self, mora: Mora, label_start: int, label_end: int) -> None: - self.assertEqual(mora.labels, self.test_case_hello_hiho[label_start:label_end]) - - def test_phonemes(self) -> None: - self.assert_phonemes(self.mora_hello_1, "ko") - self.assert_phonemes(self.mora_hello_2, "N") - self.assert_phonemes(self.mora_hello_3, "ni") - self.assert_phonemes(self.mora_hello_4, "chi") - self.assert_phonemes(self.mora_hello_5, "wa") - self.assert_phonemes(self.mora_hiho_1, "hi") - self.assert_phonemes(self.mora_hiho_2, "ho") - self.assert_phonemes(self.mora_hiho_3, "de") - self.assert_phonemes(self.mora_hiho_4, "sU") - - def test_labels(self) -> None: - self.assert_labels(self.mora_hello_1, 1, 3) - self.assert_labels(self.mora_hello_2, 3, 4) - self.assert_labels(self.mora_hello_3, 4, 6) - self.assert_labels(self.mora_hello_4, 6, 8) - self.assert_labels(self.mora_hello_5, 8, 10) - self.assert_labels(self.mora_hiho_1, 11, 13) - self.assert_labels(self.mora_hiho_2, 13, 15) - self.assert_labels(self.mora_hiho_3, 15, 17) - self.assert_labels(self.mora_hiho_4, 17, 19) - - def test_set_context(self): - # 値を書き換えるので、他のテストに影響を出さないためにdeepcopyする - mora_hello_1 = deepcopy(self.mora_hello_1) - # phonemeにあたる"p3"を書き換える - mora_hello_1.set_context("p3", "a") - self.assert_phonemes(mora_hello_1, "aa") - - -class TestAccentPhrase(TestBasePhonemes): - def setUp(self) -> None: - super().setUp() - # TODO: ValueErrorを吐く作為的ではない自然な例の模索 - # 存在しないなら放置でよい - self.accent_phrase_hello = AccentPhrase.from_phonemes( - self.phonemes_hello_hiho[1:10] - ) - self.accent_phrase_hiho = AccentPhrase.from_phonemes( - self.phonemes_hello_hiho[11:19] - ) - - def test_accent(self): - self.assertEqual(self.accent_phrase_hello.accent, 5) - self.assertEqual(self.accent_phrase_hiho.accent, 1) - - def test_set_context(self): - accent_phrase_hello = deepcopy(self.accent_phrase_hello) - # phonemeにあたる"p3"を書き換える - accent_phrase_hello.set_context("p3", "a") - self.assertEqual( - "".join([phoneme.phoneme for phoneme in accent_phrase_hello.phonemes]), - "aaaaaaaaa", - ) - - def test_phonemes(self): - self.assertEqual( - " ".join( - [phoneme.phoneme for phoneme in self.accent_phrase_hello.phonemes] - ), - "k o N n i ch i w a", - ) - self.assertEqual( - " ".join([phoneme.phoneme for phoneme in self.accent_phrase_hiho.phonemes]), - "h i h o d e s U", - ) - - def test_labels(self): - self.assertEqual( - self.accent_phrase_hello.labels, self.test_case_hello_hiho[1:10] - ) - self.assertEqual( - self.accent_phrase_hiho.labels, self.test_case_hello_hiho[11:19] - ) - - def test_merge(self): - # 「こんにちはヒホです」 - # 読点を無くしたものと同等 - merged_accent_phrase = self.accent_phrase_hello.merge(self.accent_phrase_hiho) - self.assertEqual(merged_accent_phrase.accent, 5) - self.assertEqual( - " ".join([phoneme.phoneme for phoneme in merged_accent_phrase.phonemes]), - "k o N n i ch i w a h i h o d e s U", - ) - self.assertEqual( - merged_accent_phrase.labels, - self.test_case_hello_hiho[1:10] + self.test_case_hello_hiho[11:19], - ) - - -class TestBreathGroup(TestBasePhonemes): - def setUp(self) -> None: - super().setUp() - self.breath_group_hello = BreathGroup.from_phonemes( - self.phonemes_hello_hiho[1:10] - ) - self.breath_group_hiho = BreathGroup.from_phonemes( - self.phonemes_hello_hiho[11:19] - ) - - def test_set_context(self): - # 値を書き換えるので、他のテストに影響を出さないためにdeepcopyする - breath_group_hello = deepcopy(self.breath_group_hello) - # phonemeにあたる"p3"を書き換える - breath_group_hello.set_context("p3", "a") - self.assertEqual( - "".join([phoneme.phoneme for phoneme in breath_group_hello.phonemes]), - "aaaaaaaaa", - ) - - def test_phonemes(self): - self.assertEqual( - " ".join([phoneme.phoneme for phoneme in self.breath_group_hello.phonemes]), - "k o N n i ch i w a", - ) - self.assertEqual( - " ".join([phoneme.phoneme for phoneme in self.breath_group_hiho.phonemes]), - "h i h o d e s U", - ) - - def test_labels(self): - self.assertEqual( - self.breath_group_hello.labels, self.test_case_hello_hiho[1:10] - ) - self.assertEqual( - self.breath_group_hiho.labels, self.test_case_hello_hiho[11:19] - ) - - -class TestUtterance(TestBasePhonemes): - def setUp(self) -> None: - super().setUp() - self.utterance_hello_hiho = Utterance.from_phonemes(self.phonemes_hello_hiho) - - def test_phonemes(self): - self.assertEqual( - " ".join( - [phoneme.phoneme for phoneme in self.utterance_hello_hiho.phonemes] - ), - "sil k o N n i ch i w a pau h i h o d e s U sil", - ) - changed_utterance = Utterance.from_phonemes(self.utterance_hello_hiho.phonemes) - self.assertEqual(len(changed_utterance.breath_groups), 2) - accent_phrases = list( - chain.from_iterable( - breath_group.accent_phrases - for breath_group in changed_utterance.breath_groups - ) - ) - for prev, cent, post in zip( - [None] + accent_phrases[:-1], - accent_phrases, - accent_phrases[1:] + [None], - ): - mora_num = len(cent.moras) - accent = cent.accent - - if prev is not None: - for phoneme in prev.phonemes: - self.assertEqual(phoneme.contexts["g1"], str(mora_num)) - self.assertEqual(phoneme.contexts["g2"], str(accent)) - - if post is not None: - for phoneme in post.phonemes: - self.assertEqual(phoneme.contexts["e1"], str(mora_num)) - self.assertEqual(phoneme.contexts["e2"], str(accent)) - - for phoneme in cent.phonemes: - self.assertEqual( - phoneme.contexts["k2"], - str( - sum( - [ - len(breath_group.accent_phrases) - for breath_group in changed_utterance.breath_groups - ] - ) - ), - ) - - for prev, cent, post in zip( - [None] + changed_utterance.breath_groups[:-1], - changed_utterance.breath_groups, - changed_utterance.breath_groups[1:] + [None], - ): - accent_phrase_num = len(cent.accent_phrases) - - if prev is not None: - for phoneme in prev.phonemes: - self.assertEqual(phoneme.contexts["j1"], str(accent_phrase_num)) - - if post is not None: - for phoneme in post.phonemes: - self.assertEqual(phoneme.contexts["h1"], str(accent_phrase_num)) - - for phoneme in cent.phonemes: - self.assertEqual(phoneme.contexts["i1"], str(accent_phrase_num)) - self.assertEqual( - phoneme.contexts["i5"], - str(accent_phrases.index(cent.accent_phrases[0]) + 1), - ) - self.assertEqual( - phoneme.contexts["i6"], - str( - len(accent_phrases) - - accent_phrases.index(cent.accent_phrases[0]) - ), - ) - - def test_labels(self): - self.assertEqual(self.utterance_hello_hiho.labels, self.test_case_hello_hiho) diff --git a/test/test_library_manager.py b/test/test_library_manager.py index 9924a0703..51fe8bc50 100644 --- a/test/test_library_manager.py +++ b/test/test_library_manager.py @@ -44,7 +44,7 @@ def tearDown(self): self.library_file.close() self.library_filename.unlink() - def create_vvlib_without_manifest(self, filename: str): + def create_vvlib_without_manifest(self, filename: str) -> None: with ZipFile(filename, "w") as zf_out, ZipFile( self.library_filename, "r" ) as zf_in: diff --git a/test/test_metas_store.py b/test/test_metas_store.py new file mode 100644 index 000000000..cf354928a --- /dev/null +++ b/test/test_metas_store.py @@ -0,0 +1,107 @@ +import uuid +from unittest import TestCase + +from voicevox_engine.metas.Metas import Speaker, SpeakerStyle, StyleType +from voicevox_engine.metas.MetasStore import filter_speakers_and_styles + + +def _gen_speaker(style_types: list[StyleType]) -> Speaker: + return Speaker( + speaker_uuid=str(uuid.uuid4()), + name="", + styles=[ + SpeakerStyle( + name="", + id=0, + type=style_type, + ) + for style_type in style_types + ], + ) + + +def _equal_speakers(a: list[Speaker], b: list[Speaker]) -> bool: + if len(a) != len(b): + return False + for i in range(len(a)): + if a[i].speaker_uuid != b[i].speaker_uuid: + return False + return True + + +class TestMetasStore(TestCase): + def test_filter_speakers_and_styles_with_speaker(self): + # Inputs + speaker_talk_only = _gen_speaker(["talk"]) + speaker_singing_teacher_only = _gen_speaker(["singing_teacher"]) + speaker_frame_decode_only = _gen_speaker(["frame_decode"]) + speaker_sing_only = _gen_speaker(["sing"]) + speaker_allstyle = _gen_speaker( + ["talk", "singing_teacher", "frame_decode", "sing"] + ) + + # Outputs + result = filter_speakers_and_styles( + [ + speaker_talk_only, + speaker_singing_teacher_only, + speaker_frame_decode_only, + speaker_sing_only, + speaker_allstyle, + ], + "speaker", + ) + + # Tests + self.assertEqual(len(result), 2) + + # 話者だけになっている + self.assertTrue(_equal_speakers(result, [speaker_talk_only, speaker_allstyle])) + + # スタイルがフィルタリングされている + for speaker in result: + for style in speaker.styles: + self.assertEqual(style.type, "talk") + + def test_filter_speakers_and_styles_with_singer(self): + # Inputs + speaker_talk_only = _gen_speaker(["talk"]) + speaker_singing_teacher_only = _gen_speaker(["singing_teacher"]) + speaker_frame_decode_only = _gen_speaker(["frame_decode"]) + speaker_sing_only = _gen_speaker(["sing"]) + speaker_allstyle = _gen_speaker( + ["talk", "singing_teacher", "frame_decode", "sing"] + ) + + # Outputs + result = filter_speakers_and_styles( + [ + speaker_talk_only, + speaker_singing_teacher_only, + speaker_frame_decode_only, + speaker_sing_only, + speaker_allstyle, + ], + "singer", + ) + + # Tests + self.assertEqual(len(result), 4) + + # 歌手だけになっている + self.assertTrue( + _equal_speakers( + result, + [ + speaker_singing_teacher_only, + speaker_frame_decode_only, + speaker_sing_only, + speaker_allstyle, + ], + ) + ) + + # スタイルがフィルタリングされている + for speaker in result: + for style in speaker.styles: + self.assertIn(style.type, ["singing_teacher", "frame_decode", "sing"]) diff --git a/test/test_mock_synthesis_engine.py b/test/test_mock_synthesis_engine.py deleted file mode 100644 index ce6c59825..000000000 --- a/test/test_mock_synthesis_engine.py +++ /dev/null @@ -1,140 +0,0 @@ -from unittest import TestCase - -from voicevox_engine.dev.synthesis_engine import MockSynthesisEngine -from voicevox_engine.kana_parser import create_kana -from voicevox_engine.model import AccentPhrase, AudioQuery, Mora - - -class TestMockSynthesisEngine(TestCase): - def setUp(self): - super().setUp() - - self.accent_phrases_hello_hiho = [ - AccentPhrase( - moras=[ - Mora( - text="コ", - consonant="k", - consonant_length=0.0, - vowel="o", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ン", - consonant=None, - consonant_length=None, - vowel="N", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ニ", - consonant="n", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="チ", - consonant="ch", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ワ", - consonant="w", - consonant_length=0.0, - vowel="a", - vowel_length=0.0, - pitch=0.0, - ), - ], - accent=5, - pause_mora=Mora( - text="、", - consonant=None, - consonant_length=None, - vowel="pau", - vowel_length=0.0, - pitch=0.0, - ), - ), - AccentPhrase( - moras=[ - Mora( - text="ヒ", - consonant="h", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ホ", - consonant="h", - consonant_length=0.0, - vowel="o", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="デ", - consonant="d", - consonant_length=0.0, - vowel="e", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ス", - consonant="s", - consonant_length=0.0, - vowel="U", - vowel_length=0.0, - pitch=0.0, - ), - ], - accent=1, - pause_mora=None, - ), - ] - self.engine = MockSynthesisEngine(speakers="", supported_devices="") - - def test_replace_phoneme_length(self): - self.assertEqual( - self.engine.replace_phoneme_length( - accent_phrases=self.accent_phrases_hello_hiho, - style_id=0, - ), - self.accent_phrases_hello_hiho, - ) - - def test_replace_mora_pitch(self): - self.assertEqual( - self.engine.replace_mora_pitch( - accent_phrases=self.accent_phrases_hello_hiho, - style_id=0, - ), - self.accent_phrases_hello_hiho, - ) - - def test_synthesis(self): - self.engine.synthesis( - AudioQuery( - accent_phrases=self.accent_phrases_hello_hiho, - speedScale=1, - pitchScale=0, - intonationScale=1, - volumeScale=1, - prePhonemeLength=0.1, - postPhonemeLength=0.1, - outputSamplingRate=24000, - outputStereo=False, - kana=create_kana(self.accent_phrases_hello_hiho), - ), - style_id=0, - ) diff --git a/test/test_mock_tts_engine.py b/test/test_mock_tts_engine.py new file mode 100644 index 000000000..ef6bea1d5 --- /dev/null +++ b/test/test_mock_tts_engine.py @@ -0,0 +1,74 @@ +from unittest import TestCase + +from voicevox_engine.dev.tts_engine.mock import MockTTSEngine +from voicevox_engine.metas.Metas import StyleId +from voicevox_engine.model import AccentPhrase, AudioQuery, Mora +from voicevox_engine.tts_pipeline.kana_converter import create_kana + + +def _gen_mora(text: str, consonant: str | None, vowel: str) -> Mora: + """モーラ (length=0, pitch=0) を生成する""" + return Mora( + text=text, + consonant=consonant, + consonant_length=0.0 if consonant else None, + vowel=vowel, + vowel_length=0.0, + pitch=0.0, + ) + + +class TestMockTTSEngine(TestCase): + def setUp(self): + super().setUp() + + self.accent_phrases_hello_hiho = [ + AccentPhrase( + moras=[ + _gen_mora("コ", "k", "o"), + _gen_mora("ン", None, "N"), + _gen_mora("ニ", "n", "i"), + _gen_mora("チ", "ch", "i"), + _gen_mora("ワ", "w", "a"), + ], + accent=5, + pause_mora=_gen_mora("、", None, "pau"), + ), + AccentPhrase( + moras=[ + _gen_mora("ヒ", "h", "i"), + _gen_mora("ホ", "h", "o"), + _gen_mora("デ", "d", "e"), + _gen_mora("ス", "s", "U"), + ], + accent=1, + pause_mora=None, + ), + ] + self.engine = MockTTSEngine() + + def test_update_length(self): + """`.update_length()` がエラー無く生成をおこなう""" + self.engine.update_length(self.accent_phrases_hello_hiho, StyleId(0)) + + def test_update_pitch(self): + """`.update_pitch()` がエラー無く生成をおこなう""" + self.engine.update_pitch(self.accent_phrases_hello_hiho, StyleId(0)) + + def test_synthesize_wave(self): + """`.synthesize_wave()` がエラー無く生成をおこなう""" + self.engine.synthesize_wave( + AudioQuery( + accent_phrases=self.accent_phrases_hello_hiho, + speedScale=1, + pitchScale=0, + intonationScale=1, + volumeScale=1, + prePhonemeLength=0.1, + postPhonemeLength=0.1, + outputSamplingRate=24000, + outputStereo=False, + kana=create_kana(self.accent_phrases_hello_hiho), + ), + StyleId(0), + ) diff --git a/test/test_mora_list.py b/test/test_mora_list.py deleted file mode 100644 index 25b287fa0..000000000 --- a/test/test_mora_list.py +++ /dev/null @@ -1,20 +0,0 @@ -from unittest import TestCase - -from voicevox_engine.mora_list import openjtalk_mora2text - - -class TestOpenJTalkMoraList(TestCase): - def test_mora2text(self): - self.assertEqual("ッ", openjtalk_mora2text["cl"]) - self.assertEqual("ティ", openjtalk_mora2text["ti"]) - self.assertEqual("トゥ", openjtalk_mora2text["tu"]) - self.assertEqual("ディ", openjtalk_mora2text["di"]) - # GitHub issue #60 - self.assertEqual("ギェ", openjtalk_mora2text["gye"]) - self.assertEqual("イェ", openjtalk_mora2text["ye"]) - - def test_mora2text_injective(self): - """異なるモーラが同じ読みがなに対応しないか確認する""" - values = list(openjtalk_mora2text.values()) - uniq_values = list(set(values)) - self.assertCountEqual(values, uniq_values) diff --git a/test/test_mora_to_text.py b/test/test_mora_to_text.py deleted file mode 100644 index 691681dd1..000000000 --- a/test/test_mora_to_text.py +++ /dev/null @@ -1,29 +0,0 @@ -from unittest import TestCase - -# TODO: import from voicevox_engine.synthesis_engine.mora -from voicevox_engine.synthesis_engine.synthesis_engine_base import mora_to_text - - -class TestMoraToText(TestCase): - def test_voice(self): - self.assertEqual(mora_to_text("a"), "ア") - self.assertEqual(mora_to_text("i"), "イ") - self.assertEqual(mora_to_text("ka"), "カ") - self.assertEqual(mora_to_text("N"), "ン") - self.assertEqual(mora_to_text("cl"), "ッ") - self.assertEqual(mora_to_text("gye"), "ギェ") - self.assertEqual(mora_to_text("ye"), "イェ") - self.assertEqual(mora_to_text("wo"), "ウォ") - - def test_unvoice(self): - self.assertEqual(mora_to_text("A"), "ア") - self.assertEqual(mora_to_text("I"), "イ") - self.assertEqual(mora_to_text("kA"), "カ") - self.assertEqual(mora_to_text("gyE"), "ギェ") - self.assertEqual(mora_to_text("yE"), "イェ") - self.assertEqual(mora_to_text("wO"), "ウォ") - - def test_invalid_mora(self): - """変なモーラが来ても例外を投げない""" - self.assertEqual(mora_to_text("x"), "x") - self.assertEqual(mora_to_text(""), "") diff --git a/test/test_synthesis_engine.py b/test/test_synthesis_engine.py deleted file mode 100644 index f9bfa2078..000000000 --- a/test/test_synthesis_engine.py +++ /dev/null @@ -1,897 +0,0 @@ -import math -from copy import deepcopy -from random import random -from typing import Union -from unittest import TestCase -from unittest.mock import Mock - -import numpy - -from voicevox_engine.acoustic_feature_extractor import OjtPhoneme -from voicevox_engine.model import AccentPhrase, AudioQuery, Mora -from voicevox_engine.synthesis_engine import SynthesisEngine - -# TODO: import from voicevox_engine.synthesis_engine.mora -from voicevox_engine.synthesis_engine.synthesis_engine import ( - calc_frame_per_phoneme, - calc_frame_phoneme, - calc_frame_pitch, - mora_phoneme_list, - pad_with_silence, - pre_process, - split_mora, - to_flatten_moras, - unvoiced_mora_phoneme_list, -) - -TRUE_NUM_PHONEME = 45 - - -def is_same_phoneme(p1: OjtPhoneme, p2: OjtPhoneme) -> bool: - """2つのOjtPhonemeが同じ `.phoneme` を持つ""" - return p1.phoneme == p2.phoneme - - -def is_same_ojt_phoneme_list( - p1s: list[OjtPhoneme | None], p2s: list[OjtPhoneme | None] -) -> bool: - """2つのOjtPhonemeリストで全要素ペアが同じ `.phoneme` を持つ""" - if len(p1s) != len(p2s): - return False - - for p1, p2 in zip(p1s, p2s): - if p1 is None and p2 is None: # None vs None -> equal - pass - elif p1 is None: # None vs OjtOhoneme -> not equal - return False - elif p2 is None: # OjtOhoneme vs None -> not equal - return False - elif is_same_phoneme(p1, p2): - pass - else: - return False - return True - - -def yukarin_s_mock(length: int, phoneme_list: numpy.ndarray, style_id: numpy.ndarray): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - result.append(float(phoneme_list[i] * 0.5 + style_id)) - return numpy.array(result) - - -def yukarin_sa_mock( - length: int, - vowel_phoneme_list: numpy.ndarray, - consonant_phoneme_list: numpy.ndarray, - start_accent_list: numpy.ndarray, - end_accent_list: numpy.ndarray, - start_accent_phrase_list: numpy.ndarray, - end_accent_phrase_list: numpy.ndarray, - style_id: numpy.ndarray, -): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - result.append( - float( - ( - vowel_phoneme_list[0][i] - + consonant_phoneme_list[0][i] - + start_accent_list[0][i] - + end_accent_list[0][i] - + start_accent_phrase_list[0][i] - + end_accent_phrase_list[0][i] - ) - * 0.5 - + style_id - ) - ) - return numpy.array(result)[numpy.newaxis] - - -def decode_mock( - length: int, - phoneme_size: int, - f0: numpy.ndarray, - phoneme: numpy.ndarray, - style_id: Union[numpy.ndarray, int], -): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - # decode forwardはデータサイズがlengthの256倍になるのでとりあえず256回データをresultに入れる - for _ in range(256): - result.append( - float( - f0[i][0] * (numpy.where(phoneme[i] == 1)[0] / phoneme_size) - + style_id - ) - ) - return numpy.array(result) - - -class MockCore: - default_sampling_rate = 24000 - yukarin_s_forward = Mock(side_effect=yukarin_s_mock) - yukarin_sa_forward = Mock(side_effect=yukarin_sa_mock) - decode_forward = Mock(side_effect=decode_mock) - - def metas(self): - return "" - - def supported_devices(self): - return "" - - def is_model_loaded(self, style_id): - return True - - -def _gen_query( - accent_phrases: list[AccentPhrase] | None = None, - speedScale: float = 1.0, - pitchScale: float = 1.0, - intonationScale: float = 1.0, - prePhonemeLength: float = 0.0, - postPhonemeLength: float = 0.0, - volumeScale: float = 1.0, - outputSamplingRate: int = 24000, - outputStereo: bool = False, -): - """Generate AudioQuery with default meaningless arguments for test simplicity.""" - accent_phrases = [] if accent_phrases is None else accent_phrases - return AudioQuery( - accent_phrases=accent_phrases, - speedScale=speedScale, - pitchScale=pitchScale, - intonationScale=intonationScale, - prePhonemeLength=prePhonemeLength, - postPhonemeLength=postPhonemeLength, - volumeScale=volumeScale, - outputSamplingRate=outputSamplingRate, - outputStereo=outputStereo, - ) - - -def _gen_mora( - text: str, - consonant: str | None, - consonant_length: float | None, - vowel: str, - vowel_length: float, - pitch: float, -) -> Mora: - """Generate Mora with positional arguments for test simplicity.""" - return Mora( - text=text, - consonant=consonant, - consonant_length=consonant_length, - vowel=vowel, - vowel_length=vowel_length, - pitch=pitch, - ) - - -def test_pad_with_silence(): - """Test `pad_with_silence`.""" - # Inputs - query = _gen_query(prePhonemeLength=2 * 0.01067, postPhonemeLength=6 * 0.01067) - moras = [ - _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 100.0), - ] - - # Expects - true_moras_with_silence = [ - _gen_mora(" ", None, None, "sil", 2 * 0.01067, 0.0), - _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 100.0), - _gen_mora(" ", None, None, "sil", 6 * 0.01067, 0.0), - ] - - # Outputs - moras_with_silence = pad_with_silence(moras, query) - - assert moras_with_silence == true_moras_with_silence - - -def test_calc_frame_per_phoneme(): - """Test `calc_frame_per_phoneme`.""" - # Inputs - query = _gen_query(speedScale=2.0) - moras = [ - _gen_mora(" ", None, None, " ", 2 * 0.01067, 0.0), # 0.01067 [sec/frame] - _gen_mora("コ", "k", 2 * 0.01067, "o", 4 * 0.01067, 0.0), - _gen_mora("ン", None, None, "N", 4 * 0.01067, 0.0), - _gen_mora("、", None, None, "pau", 2 * 0.01067, 0.0), - _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 0.0), - _gen_mora("ホ", "h", 4 * 0.01067, "O", 2 * 0.01067, 0.0), - _gen_mora(" ", None, None, " ", 6 * 0.01067, 0.0), - ] - - # Expects - # Pre k o N pau h i h O Pst - true_frame_per_phoneme = [1, 1, 2, 2, 1, 1, 2, 2, 1, 3] - true_frame_per_phoneme = numpy.array(true_frame_per_phoneme, dtype=numpy.int32) - - # Outputs - frame_per_phoneme = calc_frame_per_phoneme(query, moras) - - assert numpy.array_equal(frame_per_phoneme, true_frame_per_phoneme) - - -def test_calc_frame_pitch(): - """Test `test_calc_frame_pitch`.""" - # Inputs - query = _gen_query(pitchScale=2.0, intonationScale=0.5) - moras = [ - _gen_mora(" ", None, None, " ", 0.0, 0.0), - _gen_mora("コ", "k", 0.0, "o", 0.0, 50.0), - _gen_mora("ン", None, None, "N", 0.0, 50.0), - _gen_mora("、", None, None, "pau", 0.0, 0.0), - _gen_mora("ヒ", "h", 0.0, "i", 0.0, 125.0), - _gen_mora("ホ", "h", 0.0, "O", 0.0, 0.0), - _gen_mora(" ", None, None, " ", 0.0, 0.0), - ] - phoneme_str = "pau k o N pau h i h O pau" - phonemes = [OjtPhoneme(p) for p in phoneme_str.split()] - # Pre k o N pau h i h O Pst - frame_per_phoneme = [1, 1, 2, 2, 1, 1, 2, 2, 1, 3] - frame_per_phoneme = numpy.array(frame_per_phoneme, dtype=numpy.int32) - - # Expects - x4 value scaled -> mean=300 var x0.5 intonation scaling - # pau ko ko ko N N - true1_f0 = [0.0, 250.0, 250.0, 250.0, 250.0, 250.0] - # pau hi hi hi - true2_f0 = [0.0, 400.0, 400.0, 400.0] - # hO hO hO paw paw paw - true3_f0 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - true_f0 = numpy.array(true1_f0 + true2_f0 + true3_f0, dtype=numpy.float32) - - # Outputs - f0 = calc_frame_pitch(query, moras, phonemes, frame_per_phoneme) - - assert numpy.array_equal(f0, true_f0) - - -def test_calc_frame_phoneme(): - """Test `calc_frame_phoneme`.""" - # Inputs - phoneme_str = "pau k o N pau h i h O pau" - phonemes = [OjtPhoneme(p) for p in phoneme_str.split()] - # Pre k o N pau h i h O Pst - frame_per_phoneme = [1, 1, 2, 2, 1, 1, 2, 2, 1, 3] - n_frame = sum(frame_per_phoneme) - frame_per_phoneme = numpy.array(frame_per_phoneme, dtype=numpy.int32) - - # Expects - # Pr k o o N N pau h i i h h O Pt Pt Pt - phoneme_ids = [0, 23, 30, 30, 4, 4, 0, 19, 21, 21, 19, 19, 5, 0, 0, 0] - true_frame_phoneme = numpy.zeros([n_frame, TRUE_NUM_PHONEME], dtype=numpy.float32) - for frame_idx, phoneme_idx in enumerate(phoneme_ids): - true_frame_phoneme[frame_idx, phoneme_idx] = 1.0 - - # Outputs - frame_phoneme = calc_frame_phoneme(phonemes, frame_per_phoneme) - - assert numpy.array_equal(frame_phoneme, true_frame_phoneme) - - -def test_feat_to_framescale(): - """Test Mora/Phonemefeature-to-framescaleFeature pipeline.""" - # Inputs - query = _gen_query( - speedScale=2.0, - pitchScale=2.0, - intonationScale=0.5, - prePhonemeLength=2 * 0.01067, - postPhonemeLength=6 * 0.01067, - ) - flatten_moras = [ - _gen_mora("コ", "k", 2 * 0.01067, "o", 4 * 0.01067, 50.0), - _gen_mora("ン", None, None, "N", 4 * 0.01067, 50.0), - _gen_mora("、", None, None, "pau", 2 * 0.01067, 0.0), - _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 125.0), - _gen_mora("ホ", "h", 4 * 0.01067, "O", 2 * 0.01067, 0.0), - ] - phoneme_str = "pau k o N pau h i h O pau" - phoneme_data_list = [OjtPhoneme(p) for p in phoneme_str.split()] - - # Expects - # frame_per_phoneme - # Pre k o N pau h i h O Pst - true_frame_per_phoneme = [1, 1, 2, 2, 1, 1, 2, 2, 1, 3] - n_frame = sum(true_frame_per_phoneme) - true_frame_per_phoneme = numpy.array(true_frame_per_phoneme, dtype=numpy.int32) - # phoneme - # Pr k o o N N pau h i i h h O Pt Pt Pt - frame_phoneme_idxs = [0, 23, 30, 30, 4, 4, 0, 19, 21, 21, 19, 19, 5, 0, 0, 0] - true_frame_phoneme = numpy.zeros([n_frame, TRUE_NUM_PHONEME], dtype=numpy.float32) - for frame_idx, phoneme_idx in enumerate(frame_phoneme_idxs): - true_frame_phoneme[frame_idx, phoneme_idx] = 1.0 - # Pitch - # Pre ko N pau hi hO Pst - true_f0 = [0.0, 200.0, 200.0, 0.0, 500.0, 0.0, 0.0] # mean 300 - true_f0 = [0.0, 250.0, 250.0, 0.0, 400.0, 0.0, 0.0] # intonationScale 0.5 - # paw ko N pau hi hO paw - # frame_per_vowel = [1, 3, 2, 1, 3, 3, 3] - # pau ko ko ko N N - true1_f0 = [0.0, 250.0, 250.0, 250.0, 250.0, 250.0] - # pau hi hi hi - true2_f0 = [0.0, 400.0, 400.0, 400.0] - # hO hO hO paw paw paw - true3_f0 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - true_f0 = numpy.array(true1_f0 + true2_f0 + true3_f0, dtype=numpy.float32) - - assert true_frame_per_phoneme.shape[0] == len(phoneme_data_list), "Prerequisites" - - # Outputs - flatten_moras = pad_with_silence(flatten_moras, query) - frame_per_phoneme = calc_frame_per_phoneme(query, flatten_moras) - f0 = calc_frame_pitch(query, flatten_moras, phoneme_data_list, frame_per_phoneme) - frame_phoneme = calc_frame_phoneme(phoneme_data_list, frame_per_phoneme) - - assert numpy.array_equal(frame_phoneme, true_frame_phoneme) - assert numpy.array_equal(f0, true_f0) - - -class TestSynthesisEngine(TestCase): - def setUp(self): - super().setUp() - self.str_list_hello_hiho = ( - "sil k o N n i ch i w a pau h i h o d e s U sil".split() - ) - self.phoneme_data_list_hello_hiho = [ - OjtPhoneme(p) - for p in "pau k o N n i ch i w a pau h i h o d e s U pau".split() - ] - self.accent_phrases_hello_hiho = [ - AccentPhrase( - moras=[ - Mora( - text="コ", - consonant="k", - consonant_length=0.0, - vowel="o", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ン", - consonant=None, - consonant_length=None, - vowel="N", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ニ", - consonant="n", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="チ", - consonant="ch", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ワ", - consonant="w", - consonant_length=0.0, - vowel="a", - vowel_length=0.0, - pitch=0.0, - ), - ], - accent=5, - pause_mora=Mora( - text="、", - consonant=None, - consonant_length=None, - vowel="pau", - vowel_length=0.0, - pitch=0.0, - ), - ), - AccentPhrase( - moras=[ - Mora( - text="ヒ", - consonant="h", - consonant_length=0.0, - vowel="i", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ホ", - consonant="h", - consonant_length=0.0, - vowel="o", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="デ", - consonant="d", - consonant_length=0.0, - vowel="e", - vowel_length=0.0, - pitch=0.0, - ), - Mora( - text="ス", - consonant="s", - consonant_length=0.0, - vowel="U", - vowel_length=0.0, - pitch=0.0, - ), - ], - accent=1, - pause_mora=None, - ), - ] - core = MockCore() - self.yukarin_s_mock = core.yukarin_s_forward - self.yukarin_sa_mock = core.yukarin_sa_forward - self.decode_mock = core.decode_forward - self.synthesis_engine = SynthesisEngine( - core=core, - ) - - def test_to_flatten_moras(self): - flatten_moras = to_flatten_moras(self.accent_phrases_hello_hiho) - self.assertEqual( - flatten_moras, - self.accent_phrases_hello_hiho[0].moras - + [self.accent_phrases_hello_hiho[0].pause_mora] - + self.accent_phrases_hello_hiho[1].moras, - ) - - def test_split_mora(self): - consonant_phoneme_list, vowel_phoneme_list, vowel_indexes = split_mora( - self.phoneme_data_list_hello_hiho - ) - - self.assertEqual(vowel_indexes, [0, 2, 3, 5, 7, 9, 10, 12, 14, 16, 18, 19]) - - self.assertTrue( - is_same_ojt_phoneme_list( - vowel_phoneme_list, - [ - OjtPhoneme("pau"), - OjtPhoneme("o"), - OjtPhoneme("N"), - OjtPhoneme("i"), - OjtPhoneme("i"), - OjtPhoneme("a"), - OjtPhoneme("pau"), - OjtPhoneme("i"), - OjtPhoneme("o"), - OjtPhoneme("e"), - OjtPhoneme("U"), - OjtPhoneme("pau"), - ], - ) - ) - self.assertTrue( - is_same_ojt_phoneme_list( - consonant_phoneme_list, - [ - None, - OjtPhoneme("k"), - None, - OjtPhoneme("n"), - OjtPhoneme("ch"), - OjtPhoneme("w"), - None, - OjtPhoneme("h"), - OjtPhoneme("h"), - OjtPhoneme("d"), - OjtPhoneme("s"), - None, - ], - ) - ) - - def test_pre_process(self): - flatten_moras, phoneme_data_list = pre_process( - deepcopy(self.accent_phrases_hello_hiho) - ) - - mora_index = 0 - phoneme_index = 1 - - self.assertTrue(is_same_phoneme(phoneme_data_list[0], OjtPhoneme("pau"))) - for accent_phrase in self.accent_phrases_hello_hiho: - moras = accent_phrase.moras - for mora in moras: - self.assertEqual(flatten_moras[mora_index], mora) - mora_index += 1 - if mora.consonant is not None: - self.assertTrue( - is_same_phoneme( - phoneme_data_list[phoneme_index], - OjtPhoneme(mora.consonant), - ) - ) - phoneme_index += 1 - self.assertTrue( - is_same_phoneme( - phoneme_data_list[phoneme_index], - OjtPhoneme(mora.vowel), - ) - ) - phoneme_index += 1 - if accent_phrase.pause_mora: - self.assertEqual(flatten_moras[mora_index], accent_phrase.pause_mora) - mora_index += 1 - self.assertTrue( - is_same_phoneme( - phoneme_data_list[phoneme_index], - OjtPhoneme("pau"), - ) - ) - phoneme_index += 1 - self.assertTrue( - is_same_phoneme( - phoneme_data_list[phoneme_index], - OjtPhoneme("pau"), - ) - ) - - def test_replace_phoneme_length(self): - result = self.synthesis_engine.replace_phoneme_length( - accent_phrases=deepcopy(self.accent_phrases_hello_hiho), style_id=1 - ) - - # yukarin_sに渡される値の検証 - yukarin_s_args = self.yukarin_s_mock.call_args[1] - list_length = yukarin_s_args["length"] - phoneme_list = yukarin_s_args["phoneme_list"] - self.assertEqual(list_length, 20) - self.assertEqual(list_length, len(phoneme_list)) - numpy.testing.assert_array_equal( - phoneme_list, - numpy.array( - [ - 0, - 23, - 30, - 4, - 28, - 21, - 10, - 21, - 42, - 7, - 0, - 19, - 21, - 19, - 30, - 12, - 14, - 35, - 6, - 0, - ], - dtype=numpy.int64, - ), - ) - self.assertEqual(yukarin_s_args["style_id"], 1) - - # flatten_morasを使わずに愚直にaccent_phrasesにデータを反映させてみる - true_result = deepcopy(self.accent_phrases_hello_hiho) - index = 1 - - def result_value(i: int): - return float(phoneme_list[i] * 0.5 + 1) - - for accent_phrase in true_result: - moras = accent_phrase.moras - for mora in moras: - if mora.consonant is not None: - mora.consonant_length = result_value(index) - index += 1 - mora.vowel_length = result_value(index) - index += 1 - if accent_phrase.pause_mora is not None: - accent_phrase.pause_mora.vowel_length = result_value(index) - index += 1 - - self.assertEqual(result, true_result) - - def test_replace_mora_pitch(self): - # 空のリストでエラーを吐かないか - empty_accent_phrases = [] - self.assertEqual( - self.synthesis_engine.replace_mora_pitch( - accent_phrases=empty_accent_phrases, style_id=1 - ), - [], - ) - - result = self.synthesis_engine.replace_mora_pitch( - accent_phrases=deepcopy(self.accent_phrases_hello_hiho), style_id=1 - ) - - # yukarin_saに渡される値の検証 - yukarin_sa_args = self.yukarin_sa_mock.call_args[1] - list_length = yukarin_sa_args["length"] - vowel_phoneme_list = yukarin_sa_args["vowel_phoneme_list"][0] - consonant_phoneme_list = yukarin_sa_args["consonant_phoneme_list"][0] - start_accent_list = yukarin_sa_args["start_accent_list"][0] - end_accent_list = yukarin_sa_args["end_accent_list"][0] - start_accent_phrase_list = yukarin_sa_args["start_accent_phrase_list"][0] - end_accent_phrase_list = yukarin_sa_args["end_accent_phrase_list"][0] - self.assertEqual(list_length, 12) - self.assertEqual(list_length, len(vowel_phoneme_list)) - self.assertEqual(list_length, len(consonant_phoneme_list)) - self.assertEqual(list_length, len(start_accent_list)) - self.assertEqual(list_length, len(end_accent_list)) - self.assertEqual(list_length, len(start_accent_phrase_list)) - self.assertEqual(list_length, len(end_accent_phrase_list)) - self.assertEqual(yukarin_sa_args["style_id"], 1) - - numpy.testing.assert_array_equal( - vowel_phoneme_list, - numpy.array( - [ - 0, - 30, - 4, - 21, - 21, - 7, - 0, - 21, - 30, - 14, - 6, - 0, - ] - ), - ) - numpy.testing.assert_array_equal( - consonant_phoneme_list, - numpy.array( - [ - -1, - 23, - -1, - 28, - 10, - 42, - -1, - 19, - 19, - 12, - 35, - -1, - ] - ), - ) - numpy.testing.assert_array_equal( - start_accent_list, numpy.array([0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]) - ) - numpy.testing.assert_array_equal( - end_accent_list, numpy.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]) - ) - numpy.testing.assert_array_equal( - start_accent_phrase_list, numpy.array([0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]) - ) - numpy.testing.assert_array_equal( - end_accent_phrase_list, numpy.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]) - ) - - # flatten_morasを使わずに愚直にaccent_phrasesにデータを反映させてみる - true_result = deepcopy(self.accent_phrases_hello_hiho) - index = 1 - - def result_value(i: int): - # unvoiced_mora_phoneme_listのPhoneme ID版 - unvoiced_mora_phoneme_id_list = [ - OjtPhoneme(p).phoneme_id for p in unvoiced_mora_phoneme_list - ] - if vowel_phoneme_list[i] in unvoiced_mora_phoneme_id_list: - return 0 - return ( - vowel_phoneme_list[i] - + consonant_phoneme_list[i] - + start_accent_list[i] - + end_accent_list[i] - + start_accent_phrase_list[i] - + end_accent_phrase_list[i] - ) * 0.5 + 1 - - for accent_phrase in true_result: - moras = accent_phrase.moras - for mora in moras: - mora.pitch = result_value(index) - index += 1 - if accent_phrase.pause_mora is not None: - accent_phrase.pause_mora.pitch = result_value(index) - index += 1 - - self.assertEqual(result, true_result) - - def synthesis_test_base(self, audio_query: AudioQuery): - accent_phrases = audio_query.accent_phrases - - # decode forwardのために適当にpitchとlengthを設定し、リストで持っておく - phoneme_length_list = [0.0] - phoneme_id_list = [0] - f0_list = [0.0] - for accent_phrase in accent_phrases: - moras = accent_phrase.moras - for mora in moras: - if mora.consonant is not None: - mora.consonant_length = 0.1 - phoneme_length_list.append(0.1) - phoneme_id_list.append(OjtPhoneme(mora.consonant).phoneme_id) - mora.vowel_length = 0.2 - phoneme_length_list.append(0.2) - phoneme_id_list.append(OjtPhoneme(mora.vowel).phoneme_id) - if mora.vowel not in unvoiced_mora_phoneme_list: - mora.pitch = 5.0 + random() - f0_list.append(mora.pitch) - if accent_phrase.pause_mora is not None: - accent_phrase.pause_mora.vowel_length = 0.2 - phoneme_length_list.append(0.2) - phoneme_id_list.append(OjtPhoneme("pau").phoneme_id) - f0_list.append(0.0) - phoneme_length_list.append(0.0) - phoneme_id_list.append(0) - f0_list.append(0.0) - - phoneme_length_list[0] = audio_query.prePhonemeLength - phoneme_length_list[-1] = audio_query.postPhonemeLength - - for i in range(len(phoneme_length_list)): - phoneme_length_list[i] /= audio_query.speedScale - - result = self.synthesis_engine.synthesis(query=audio_query, style_id=1) - - # decodeに渡される値の検証 - decode_args = self.decode_mock.call_args[1] - list_length = decode_args["length"] - self.assertEqual( - list_length, - int(sum([round(p * 24000 / 256) for p in phoneme_length_list])), - ) - - num_phoneme = OjtPhoneme.num_phoneme - # mora_phoneme_listのPhoneme ID版 - mora_phoneme_id_list = [OjtPhoneme(p).phoneme_id for p in mora_phoneme_list] - - # numpy.repeatをfor文でやる - f0 = [] - phoneme = [] - f0_index = 0 - mean_f0 = [] - for i, phoneme_length in enumerate(phoneme_length_list): - f0_single = numpy.array(f0_list[f0_index], dtype=numpy.float32) * ( - 2**audio_query.pitchScale - ) - for _ in range(int(round(phoneme_length * (24000 / 256)))): - f0.append([f0_single]) - phoneme_s = [] - for _ in range(num_phoneme): - phoneme_s.append(0) - # one hot - phoneme_s[phoneme_id_list[i]] = 1 - phoneme.append(phoneme_s) - # consonantとvowelを判別し、vowelであればf0_indexを一つ進める - if phoneme_id_list[i] in mora_phoneme_id_list: - if f0_single > 0: - mean_f0.append(f0_single) - f0_index += 1 - - mean_f0 = numpy.array(mean_f0, dtype=numpy.float32).mean() - f0 = numpy.array(f0, dtype=numpy.float32) - for i in range(len(f0)): - if f0[i][0] != 0.0: - f0[i][0] = (f0[i][0] - mean_f0) * audio_query.intonationScale + mean_f0 - - phoneme = numpy.array(phoneme, dtype=numpy.float32) - - # 乱数の影響で数値の位置がずれが生じるので、大半(4/5)があっていればよしとする - # また、上の部分のint(round(phoneme_length * (24000 / 256)))の影響で - # 本来のf0/phonemeとテスト生成したf0/phonemeの長さが変わることがあり、 - # テスト生成したものが若干長くなることがあるので、本来のものの長さを基準にassertする - assert_f0_count = 0 - decode_f0 = decode_args["f0"] - for i in range(len(decode_f0)): - # 乱数の影響等で数値にずれが生じるので、10の-5乗までの近似値であれば許容する - assert_f0_count += math.isclose(f0[i][0], decode_f0[i][0], rel_tol=10e-5) - self.assertTrue(assert_f0_count >= int(len(decode_f0) / 5) * 4) - assert_phoneme_count = 0 - decode_phoneme = decode_args["phoneme"] - for i in range(len(decode_phoneme)): - assert_true_count = 0 - for j in range(len(decode_phoneme[i])): - assert_true_count += bool(phoneme[i][j] == decode_phoneme[i][j]) - assert_phoneme_count += assert_true_count == num_phoneme - self.assertTrue(assert_phoneme_count >= int(len(decode_phoneme) / 5) * 4) - self.assertEqual(decode_args["style_id"], 1) - - # decode forwarderのmockを使う - true_result = decode_mock(list_length, num_phoneme, f0, phoneme, 1) - - true_result *= audio_query.volumeScale - - # TODO: resampyの部分は値の検証しようがないので、パスする - if audio_query.outputSamplingRate != 24000: - return - - assert_result_count = 0 - for i in range(len(true_result)): - if audio_query.outputStereo: - assert_result_count += math.isclose( - true_result[i], result[i][0], rel_tol=10e-5 - ) and math.isclose(true_result[i], result[i][1], rel_tol=10e-5) - else: - assert_result_count += math.isclose( - true_result[i], result[i], rel_tol=10e-5 - ) - self.assertTrue(assert_result_count >= int(len(true_result) / 5) * 4) - - def test_synthesis(self): - audio_query = AudioQuery( - accent_phrases=deepcopy(self.accent_phrases_hello_hiho), - speedScale=1.0, - pitchScale=1.0, - intonationScale=1.0, - volumeScale=1.0, - prePhonemeLength=0.1, - postPhonemeLength=0.1, - outputSamplingRate=24000, - outputStereo=False, - # このテスト内では使わないので生成不要 - kana="", - ) - - self.synthesis_test_base(audio_query) - - # speed scaleのテスト - audio_query.speedScale = 1.2 - self.synthesis_test_base(audio_query) - - # pitch scaleのテスト - audio_query.pitchScale = 1.5 - audio_query.speedScale = 1.0 - self.synthesis_test_base(audio_query) - - # intonation scaleのテスト - audio_query.pitchScale = 1.0 - audio_query.intonationScale = 1.4 - self.synthesis_test_base(audio_query) - - # volume scaleのテスト - audio_query.intonationScale = 1.0 - audio_query.volumeScale = 2.0 - self.synthesis_test_base(audio_query) - - # pre/post phoneme lengthのテスト - audio_query.volumeScale = 1.0 - audio_query.prePhonemeLength = 0.5 - audio_query.postPhonemeLength = 0.5 - self.synthesis_test_base(audio_query) - - # output sampling rateのテスト - audio_query.prePhonemeLength = 0.1 - audio_query.postPhonemeLength = 0.1 - audio_query.outputSamplingRate = 48000 - self.synthesis_test_base(audio_query) - - # output stereoのテスト - audio_query.outputSamplingRate = 24000 - audio_query.outputStereo = True - self.synthesis_test_base(audio_query) diff --git a/test/test_synthesis_engine_base.py b/test/test_synthesis_engine_base.py deleted file mode 100644 index c49dcbe01..000000000 --- a/test/test_synthesis_engine_base.py +++ /dev/null @@ -1,412 +0,0 @@ -from typing import List, Union -from unittest import TestCase -from unittest.mock import Mock - -import numpy - -from voicevox_engine.model import AccentPhrase, AudioQuery, Mora -from voicevox_engine.synthesis_engine import SynthesisEngine - - -def yukarin_s_mock(length: int, phoneme_list: numpy.ndarray, style_id: numpy.ndarray): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - result.append(round(float(phoneme_list[i] * 0.0625 + style_id), 2)) - return numpy.array(result) - - -def yukarin_sa_mock( - length: int, - vowel_phoneme_list: numpy.ndarray, - consonant_phoneme_list: numpy.ndarray, - start_accent_list: numpy.ndarray, - end_accent_list: numpy.ndarray, - start_accent_phrase_list: numpy.ndarray, - end_accent_phrase_list: numpy.ndarray, - style_id: numpy.ndarray, -): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - result.append( - round( - float( - ( - vowel_phoneme_list[0][i] - + consonant_phoneme_list[0][i] - + start_accent_list[0][i] - + end_accent_list[0][i] - + start_accent_phrase_list[0][i] - + end_accent_phrase_list[0][i] - ) - * 0.0625 - + style_id - ), - 2, - ) - ) - return numpy.array(result)[numpy.newaxis] - - -def decode_mock( - length: int, - phoneme_size: int, - f0: numpy.ndarray, - phoneme: numpy.ndarray, - style_id: Union[numpy.ndarray, int], -): - result = [] - # mockとしての適当な処理、特に意味はない - for i in range(length): - # decode forwardはデータサイズがlengthの256倍になるのでとりあえず256回データをresultに入れる - for _ in range(256): - result.append( - float( - f0[i][0] * (numpy.where(phoneme[i] == 1)[0] / phoneme_size) - + style_id - ) - ) - return numpy.array(result) - - -def koreha_arimasuka_base_expected(): - return [ - AccentPhrase( - moras=[ - Mora( - text="コ", - consonant="k", - consonant_length=2.44, - vowel="o", - vowel_length=2.88, - pitch=4.38, - ), - Mora( - text="レ", - consonant="r", - consonant_length=3.06, - vowel="e", - vowel_length=1.88, - pitch=4.0, - ), - Mora( - text="ワ", - consonant="w", - consonant_length=3.62, - vowel="a", - vowel_length=1.44, - pitch=4.19, - ), - ], - accent=3, - pause_mora=None, - is_interrogative=False, - ), - AccentPhrase( - moras=[ - Mora( - text="ア", - consonant=None, - consonant_length=None, - vowel="a", - vowel_length=1.44, - pitch=1.44, - ), - Mora( - text="リ", - consonant="r", - consonant_length=3.06, - vowel="i", - vowel_length=2.31, - pitch=4.44, - ), - Mora( - text="マ", - consonant="m", - consonant_length=2.62, - vowel="a", - vowel_length=1.44, - pitch=3.12, - ), - Mora( - text="ス", - consonant="s", - consonant_length=3.19, - vowel="U", - vowel_length=1.38, - pitch=0.0, - ), - Mora( - text="カ", - consonant="k", - consonant_length=2.44, - vowel="a", - vowel_length=1.44, - pitch=2.94, - ), - ], - accent=3, - pause_mora=None, - is_interrogative=False, - ), - ] - - -def create_mock_query(accent_phrases): - return AudioQuery( - accent_phrases=accent_phrases, - speedScale=1, - pitchScale=0, - intonationScale=1, - volumeScale=1, - prePhonemeLength=0.1, - postPhonemeLength=0.1, - outputSamplingRate=24000, - outputStereo=False, - kana="", - ) - - -class MockCore: - default_sampling_rate = 24000 - yukarin_s_forward = Mock(side_effect=yukarin_s_mock) - yukarin_sa_forward = Mock(side_effect=yukarin_sa_mock) - decode_forward = Mock(side_effect=decode_mock) - - def metas(self): - return "" - - def supported_devices(self): - return "" - - def is_model_loaded(self, style_id): - return True - - -class TestSynthesisEngineBase(TestCase): - def setUp(self): - super().setUp() - self.synthesis_engine = SynthesisEngine( - core=MockCore(), - ) - self.synthesis_engine._synthesis_impl = Mock() - - def create_accent_phrases_test_base(self, text: str, expected: List[AccentPhrase]): - actual = self.synthesis_engine.create_accent_phrases(text, 1) - self.assertEqual( - expected, - actual, - "case(text:" + text + ")", - ) - - def create_synthesis_test_base( - self, - text: str, - expected: List[AccentPhrase], - enable_interrogative_upspeak: bool, - ): - """音声合成時に疑問文モーラ処理を行っているかどうかを検証 - (https://github.com/VOICEVOX/voicevox_engine/issues/272#issuecomment-1022610866) - """ - accent_phrases = self.synthesis_engine.create_accent_phrases(text, 1) - query = create_mock_query(accent_phrases=accent_phrases) - self.synthesis_engine.synthesis( - query, 0, enable_interrogative_upspeak=enable_interrogative_upspeak - ) - # _synthesis_implの第一引数に与えられたqueryを検証 - actual = self.synthesis_engine._synthesis_impl.call_args[0][0].accent_phrases - - self.assertEqual( - expected, - actual, - "case(text:" + text + ")", - ) - - def test_create_accent_phrases(self): - """accent_phrasesの作成時では疑問文モーラ処理を行わない - (https://github.com/VOICEVOX/voicevox_engine/issues/272#issuecomment-1022610866) - """ - expected = koreha_arimasuka_base_expected() - expected[-1].is_interrogative = True - self.create_accent_phrases_test_base(text="これはありますか?", expected=expected) - - def test_synthesis_interrogative(self): - expected = koreha_arimasuka_base_expected() - expected[-1].is_interrogative = True - expected[-1].moras += [ - Mora( - text="ア", - consonant=None, - consonant_length=None, - vowel="a", - vowel_length=0.15, - pitch=expected[-1].moras[-1].pitch + 0.3, - ) - ] - self.create_synthesis_test_base( - text="これはありますか?", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = koreha_arimasuka_base_expected() - expected[-1].is_interrogative = True - self.create_synthesis_test_base( - text="これはありますか?", - expected=expected, - enable_interrogative_upspeak=False, - ) - - expected = koreha_arimasuka_base_expected() - self.create_synthesis_test_base( - text="これはありますか", - expected=expected, - enable_interrogative_upspeak=True, - ) - - def nn_base_expected(): - return [ - AccentPhrase( - moras=[ - Mora( - text="ン", - consonant=None, - consonant_length=None, - vowel="N", - vowel_length=1.25, - pitch=1.44, - ) - ], - accent=1, - pause_mora=None, - is_interrogative=False, - ) - ] - - expected = nn_base_expected() - self.create_synthesis_test_base( - text="ん", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = nn_base_expected() - expected[-1].is_interrogative = True - expected[-1].moras += [ - Mora( - text="ン", - consonant=None, - consonant_length=None, - vowel="N", - vowel_length=0.15, - pitch=expected[-1].moras[-1].pitch + 0.3, - ) - ] - self.create_synthesis_test_base( - text="ん?", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = nn_base_expected() - expected[-1].is_interrogative = True - self.create_synthesis_test_base( - text="ん?", - expected=expected, - enable_interrogative_upspeak=False, - ) - - def ltu_base_expected(): - return [ - AccentPhrase( - moras=[ - Mora( - text="ッ", - consonant=None, - consonant_length=None, - vowel="cl", - vowel_length=1.69, - pitch=0.0, - ) - ], - accent=1, - pause_mora=None, - is_interrogative=False, - ) - ] - - expected = ltu_base_expected() - self.create_synthesis_test_base( - text="っ", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = ltu_base_expected() - expected[-1].is_interrogative = True - self.create_synthesis_test_base( - text="っ?", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = ltu_base_expected() - expected[-1].is_interrogative = True - self.create_synthesis_test_base( - text="っ?", - expected=expected, - enable_interrogative_upspeak=False, - ) - - def su_base_expected(): - return [ - AccentPhrase( - moras=[ - Mora( - text="ス", - consonant="s", - consonant_length=3.19, - vowel="u", - vowel_length=3.5, - pitch=5.94, - ) - ], - accent=1, - pause_mora=None, - is_interrogative=False, - ) - ] - - expected = su_base_expected() - self.create_synthesis_test_base( - text="す", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = su_base_expected() - expected[-1].is_interrogative = True - expected[-1].moras += [ - Mora( - text="ウ", - consonant=None, - consonant_length=None, - vowel="u", - vowel_length=0.15, - pitch=expected[-1].moras[-1].pitch + 0.3, - ) - ] - self.create_synthesis_test_base( - text="す?", - expected=expected, - enable_interrogative_upspeak=True, - ) - - expected = su_base_expected() - expected[-1].is_interrogative = True - self.create_synthesis_test_base( - text="す?", - expected=expected, - enable_interrogative_upspeak=False, - ) diff --git a/test/tts_pipeline/__init__.py b/test/tts_pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_from_kana_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_from_kana_output.json new file mode 100644 index 000000000..671e117de --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_from_kana_output.json @@ -0,0 +1,95 @@ +[ + { + "accent": 5, + "is_interrogative": false, + "moras": [ + { + "consonant": "k", + "consonant_length": 2.44, + "pitch": 4.38, + "text": "コ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": null, + "consonant_length": null, + "pitch": 1.25, + "text": "ン", + "vowel": "N", + "vowel_length": 1.25 + }, + { + "consonant": "n", + "consonant_length": 2.75, + "pitch": 4.06, + "text": "ニ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "ch", + "consonant_length": 1.62, + "pitch": 2.94, + "text": "チ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "w", + "consonant_length": 3.62, + "pitch": 4.19, + "text": "ワ", + "vowel": "a", + "vowel_length": 1.44 + } + ], + "pause_mora": { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "、", + "vowel": "pau", + "vowel_length": 1.0 + } + }, + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 3.69, + "text": "ヒ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 4.06, + "text": "ホ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": "d", + "consonant_length": 1.75, + "pitch": 2.62, + "text": "デ", + "vowel": "e", + "vowel_length": 1.88 + }, + { + "consonant": "s", + "consonant_length": 3.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 1.38 + } + ], + "pause_mora": null + } +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_output.json new file mode 100644 index 000000000..671e117de --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_create_accent_phrases_output.json @@ -0,0 +1,95 @@ +[ + { + "accent": 5, + "is_interrogative": false, + "moras": [ + { + "consonant": "k", + "consonant_length": 2.44, + "pitch": 4.38, + "text": "コ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": null, + "consonant_length": null, + "pitch": 1.25, + "text": "ン", + "vowel": "N", + "vowel_length": 1.25 + }, + { + "consonant": "n", + "consonant_length": 2.75, + "pitch": 4.06, + "text": "ニ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "ch", + "consonant_length": 1.62, + "pitch": 2.94, + "text": "チ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "w", + "consonant_length": 3.62, + "pitch": 4.19, + "text": "ワ", + "vowel": "a", + "vowel_length": 1.44 + } + ], + "pause_mora": { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "、", + "vowel": "pau", + "vowel_length": 1.0 + } + }, + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 3.69, + "text": "ヒ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 4.06, + "text": "ホ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": "d", + "consonant_length": 1.75, + "pitch": 2.62, + "text": "デ", + "vowel": "e", + "vowel_length": 1.88 + }, + { + "consonant": "s", + "consonant_length": 3.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 1.38 + } + ], + "pause_mora": null + } +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[query].json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[query].json new file mode 100644 index 000000000..ed97c822c --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[query].json @@ -0,0 +1,268 @@ +[ + [ + { + "frame_length": 4, + "phoneme": "pau" + }, + { + "frame_length": 6, + "phoneme": "d" + }, + { + "frame_length": 4, + "phoneme": "o" + }, + { + "frame_length": 8, + "phoneme": "r" + }, + { + "frame_length": 13, + "phoneme": "e" + }, + { + "frame_length": 4, + "phoneme": "m" + }, + { + "frame_length": 21, + "phoneme": "i" + }, + { + "frame_length": 3, + "phoneme": "pau" + }, + { + "frame_length": 2, + "phoneme": "f" + }, + { + "frame_length": 6, + "phoneme": "a" + }, + { + "frame_length": 6, + "phoneme": "s" + }, + { + "frame_length": 17, + "phoneme": "o" + }, + { + "frame_length": 10, + "phoneme": "pau" + } + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 262.93, + 262.93, + 262.93, + 262.93, + 262.93, + 262.93, + 264.0, + 264.0, + 264.0, + 264.0, + 296.53, + 296.53, + 296.53, + 296.53, + 296.53, + 296.53, + 296.53, + 296.53, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 295.27, + 332.32, + 332.32, + 332.32, + 332.32, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 331.95, + 0.0, + 0.0, + 0.0, + 351.21, + 351.21, + 350.58, + 350.58, + 350.58, + 350.58, + 350.58, + 350.58, + 396.0, + 396.0, + 396.0, + 396.0, + 396.0, + 396.0, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 395.56, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.61, + 0.61, + 0.61, + 0.61, + 0.61, + 0.61, + 1.53, + 1.53, + 1.53, + 1.53, + 1.96, + 1.96, + 1.96, + 1.96, + 1.96, + 1.96, + 1.96, + 1.96, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 0.83, + 1.79, + 1.79, + 1.79, + 1.79, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 1.44, + 0.0, + 0.0, + 0.0, + 1.11, + 1.11, + 0.51, + 0.51, + 0.51, + 0.51, + 0.51, + 0.51, + 3.0, + 3.0, + 3.0, + 3.0, + 3.0, + 3.0, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 2.57, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[wave].json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[wave].json new file mode 100644 index 000000000..3b711372a --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_from_score_output[wave].json @@ -0,0 +1,3995 @@ +[ + [ + 0.7 + ], + [ + 1.41 + ], + [ + 1.24 + ], + [ + 1.34 + ], + [ + 1.27 + ], + [ + 1.32 + ], + [ + 1.28 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.35 + ], + [ + 1.45 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.45 + ], + [ + 1.44 + ], + [ + 1.45 + ], + [ + 1.44 + ], + [ + 1.45 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.44 + ], + [ + 1.45 + ], + [ + 1.44 + ], + [ + 1.45 + ], + [ + 1.43 + ], + [ + 1.46 + ], + [ + 1.42 + ], + [ + 1.48 + ], + [ + 1.38 + ], + [ + 1.84 + ], + [ + 2.27 + ], + [ + 2.16 + ], + [ + 2.22 + ], + [ + 2.18 + ], + [ + 2.21 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.2 + ], + [ + 2.19 + ], + [ + 2.21 + ], + [ + 2.19 + ], + [ + 2.21 + ], + [ + 2.18 + ], + [ + 2.22 + ], + [ + 2.15 + ], + [ + 2.37 + ], + [ + 2.75 + ], + [ + 2.7 + ], + [ + 2.73 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.72 + ], + [ + 2.71 + ], + [ + 2.73 + ], + [ + 2.71 + ], + [ + 2.73 + ], + [ + 2.7 + ], + [ + 2.73 + ], + [ + 2.7 + ], + [ + 2.73 + ], + [ + 2.7 + ], + [ + 2.73 + ], + [ + 2.7 + ], + [ + 2.73 + ], + [ + 2.68 + ], + [ + 1.7 + ], + [ + 1.5 + ], + [ + 1.58 + ], + [ + 1.53 + ], + [ + 1.57 + ], + [ + 1.53 + ], + [ + 1.57 + ], + [ + 1.54 + ], + [ + 1.57 + ], + [ + 1.54 + ], + [ + 1.56 + ], + [ + 1.54 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.55 + ], + [ + 1.56 + ], + [ + 1.54 + ], + [ + 1.57 + ], + [ + 1.53 + ], + [ + 1.59 + ], + [ + 1.48 + ], + [ + 2.02 + ], + [ + 2.52 + ], + [ + 2.4 + ], + [ + 2.47 + ], + [ + 2.42 + ], + [ + 2.45 + ], + [ + 2.43 + ], + [ + 2.45 + ], + [ + 2.43 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.44 + ], + [ + 2.43 + ], + [ + 2.45 + ], + [ + 2.43 + ], + [ + 2.45 + ], + [ + 2.42 + ], + [ + 2.47 + ], + [ + 2.31 + ], + [ + 2.02 + ], + [ + 2.05 + ], + [ + 2.03 + ], + [ + 2.05 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.04 + ], + [ + 2.05 + ], + [ + 2.03 + ], + [ + 2.05 + ], + [ + 2.03 + ], + [ + 2.06 + ], + [ + 2.01 + ], + [ + 2.11 + ], + [ + 1.65 + ], + [ + 1.23 + ], + [ + 1.33 + ], + [ + 1.28 + ], + [ + 1.32 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.28 + ], + [ + 1.37 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.74 + ], + [ + 1.72 + ], + [ + 1.74 + ], + [ + 1.72 + ], + [ + 1.74 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.73 + ], + [ + 1.72 + ], + [ + 1.74 + ], + [ + 1.72 + ], + [ + 1.76 + ], + [ + 1.55 + ], + [ + 1.36 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.4 + ], + [ + 1.38 + ], + [ + 1.4 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.38 + ], + [ + 1.4 + ], + [ + 1.39 + ], + [ + 1.38 + ], + [ + 1.41 + ], + [ + 1.35 + ], + [ + 1.47 + ], + [ + 1.24 + ], + [ + 3.51 + ], + [ + 4.6 + ], + [ + 4.24 + ], + [ + 4.46 + ], + [ + 4.3 + ], + [ + 4.43 + ], + [ + 4.32 + ], + [ + 4.41 + ], + [ + 4.34 + ], + [ + 4.4 + ], + [ + 4.35 + ], + [ + 4.39 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.37 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.36 + ], + [ + 4.38 + ], + [ + 4.34 + ], + [ + 3.65 + ], + [ + 3.52 + ], + [ + 3.57 + ], + [ + 3.53 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.55 + ], + [ + 3.54 + ], + [ + 3.56 + ], + [ + 3.54 + ], + [ + 3.57 + ], + [ + 3.53 + ], + [ + 3.58 + ], + [ + 3.51 + ], + [ + 3.59 + ], + [ + 3.49 + ], + [ + 3.62 + ], + [ + 3.44 + ], + [ + 3.74 + ], + [ + 2.8 + ], + [ + 1.15 + ], + [ + 1.37 + ], + [ + 1.26 + ], + [ + 1.32 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.3 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.31 + ], + [ + 1.29 + ], + [ + 1.32 + ], + [ + 1.28 + ], + [ + 1.33 + ], + [ + 1.27 + ], + [ + 1.34 + ], + [ + 1.24 + ], + [ + 1.41 + ] +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_output.json new file mode 100644 index 000000000..22caa02ba --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_synthesize_wave_output.json @@ -0,0 +1,7172 @@ +[ + [ + [ + 0.96, + 0.96 + ], + [ + 1.4, + 1.4 + ], + [ + 1.24, + 1.24 + ], + [ + 1.34, + 1.34 + ], + [ + 1.27, + 1.27 + ], + [ + 1.33, + 1.33 + ], + [ + 1.28, + 1.28 + ], + [ + 1.32, + 1.32 + ], + [ + 1.28, + 1.28 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.29, + 1.29 + ], + [ + 1.31, + 1.31 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.3, + 1.3 + ], + [ + 1.29, + 1.29 + ], + [ + 1.32, + 1.32 + ], + [ + 1.27, + 1.27 + ], + [ + 1.37, + 1.37 + ] + ] +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_and_pitch_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_and_pitch_output.json new file mode 100644 index 000000000..671e117de --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_and_pitch_output.json @@ -0,0 +1,95 @@ +[ + { + "accent": 5, + "is_interrogative": false, + "moras": [ + { + "consonant": "k", + "consonant_length": 2.44, + "pitch": 4.38, + "text": "コ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": null, + "consonant_length": null, + "pitch": 1.25, + "text": "ン", + "vowel": "N", + "vowel_length": 1.25 + }, + { + "consonant": "n", + "consonant_length": 2.75, + "pitch": 4.06, + "text": "ニ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "ch", + "consonant_length": 1.62, + "pitch": 2.94, + "text": "チ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "w", + "consonant_length": 3.62, + "pitch": 4.19, + "text": "ワ", + "vowel": "a", + "vowel_length": 1.44 + } + ], + "pause_mora": { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "、", + "vowel": "pau", + "vowel_length": 1.0 + } + }, + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 3.69, + "text": "ヒ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 4.06, + "text": "ホ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": "d", + "consonant_length": 1.75, + "pitch": 2.62, + "text": "デ", + "vowel": "e", + "vowel_length": 1.88 + }, + { + "consonant": "s", + "consonant_length": 3.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 1.38 + } + ], + "pause_mora": null + } +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_output.json new file mode 100644 index 000000000..c50c36191 --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_length_output.json @@ -0,0 +1,95 @@ +[ + { + "accent": 5, + "is_interrogative": false, + "moras": [ + { + "consonant": "k", + "consonant_length": 2.44, + "pitch": 0.0, + "text": "コ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "ン", + "vowel": "N", + "vowel_length": 1.25 + }, + { + "consonant": "n", + "consonant_length": 2.75, + "pitch": 0.0, + "text": "ニ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "ch", + "consonant_length": 1.62, + "pitch": 0.0, + "text": "チ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "w", + "consonant_length": 3.62, + "pitch": 0.0, + "text": "ワ", + "vowel": "a", + "vowel_length": 1.44 + } + ], + "pause_mora": { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "、", + "vowel": "pau", + "vowel_length": 1.0 + } + }, + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 0.0, + "text": "ヒ", + "vowel": "i", + "vowel_length": 2.31 + }, + { + "consonant": "h", + "consonant_length": 2.19, + "pitch": 0.0, + "text": "ホ", + "vowel": "o", + "vowel_length": 2.88 + }, + { + "consonant": "d", + "consonant_length": 1.75, + "pitch": 0.0, + "text": "デ", + "vowel": "e", + "vowel_length": 1.88 + }, + { + "consonant": "s", + "consonant_length": 3.19, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 1.38 + } + ], + "pause_mora": null + } +] diff --git a/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_pitch_output.json b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_pitch_output.json new file mode 100644 index 000000000..c1b0b844e --- /dev/null +++ b/test/tts_pipeline/__snapshots__/test_tts_engine/test_mocked_update_pitch_output.json @@ -0,0 +1,95 @@ +[ + { + "accent": 5, + "is_interrogative": false, + "moras": [ + { + "consonant": "k", + "consonant_length": 0.0, + "pitch": 4.38, + "text": "コ", + "vowel": "o", + "vowel_length": 0.0 + }, + { + "consonant": null, + "consonant_length": null, + "pitch": 1.25, + "text": "ン", + "vowel": "N", + "vowel_length": 0.0 + }, + { + "consonant": "n", + "consonant_length": 0.0, + "pitch": 4.06, + "text": "ニ", + "vowel": "i", + "vowel_length": 0.0 + }, + { + "consonant": "ch", + "consonant_length": 0.0, + "pitch": 2.94, + "text": "チ", + "vowel": "i", + "vowel_length": 0.0 + }, + { + "consonant": "w", + "consonant_length": 0.0, + "pitch": 4.19, + "text": "ワ", + "vowel": "a", + "vowel_length": 0.0 + } + ], + "pause_mora": { + "consonant": null, + "consonant_length": null, + "pitch": 0.0, + "text": "、", + "vowel": "pau", + "vowel_length": 0.0 + } + }, + { + "accent": 1, + "is_interrogative": false, + "moras": [ + { + "consonant": "h", + "consonant_length": 0.0, + "pitch": 3.69, + "text": "ヒ", + "vowel": "i", + "vowel_length": 0.0 + }, + { + "consonant": "h", + "consonant_length": 0.0, + "pitch": 4.06, + "text": "ホ", + "vowel": "o", + "vowel_length": 0.0 + }, + { + "consonant": "d", + "consonant_length": 0.0, + "pitch": 2.62, + "text": "デ", + "vowel": "e", + "vowel_length": 0.0 + }, + { + "consonant": "s", + "consonant_length": 0.0, + "pitch": 0.0, + "text": "ス", + "vowel": "U", + "vowel_length": 0.0 + } + ], + "pause_mora": null + } +] diff --git a/test/test_kana_parser.py b/test/tts_pipeline/test_kana_converter.py similarity index 98% rename from test/test_kana_parser.py rename to test/tts_pipeline/test_kana_converter.py index ef800b600..94309d7c7 100644 --- a/test/test_kana_parser.py +++ b/test/tts_pipeline/test_kana_converter.py @@ -1,13 +1,12 @@ -from typing import List from unittest import TestCase -from voicevox_engine import kana_parser -from voicevox_engine.kana_parser import create_kana from voicevox_engine.model import AccentPhrase, Mora, ParseKanaError, ParseKanaErrorCode +from voicevox_engine.tts_pipeline import kana_converter +from voicevox_engine.tts_pipeline.kana_converter import create_kana -def parse_kana(text: str) -> List[AccentPhrase]: - accent_phrases = kana_parser.parse_kana(text) +def parse_kana(text: str) -> list[AccentPhrase]: + accent_phrases = kana_converter.parse_kana(text) return accent_phrases @@ -57,9 +56,9 @@ def test_roundtrip(self): self.assertEqual(create_kana(parse_kana(text)), text) def _accent_phrase_marks_base( - self, text: str, expected_accent_phrases: List[AccentPhrase] + self, text: str, expected_accent_phrases: list[AccentPhrase] ) -> None: - accent_phrases = kana_parser.parse_kana(text) + accent_phrases = kana_converter.parse_kana(text) self.assertEqual(expected_accent_phrases, accent_phrases) def test_accent_phrase_marks(self): @@ -530,7 +529,7 @@ def a_pause_a_question_pause_a_question_a_question_mark_accent_phrases(): class TestParseKanaException(TestCase): - def _assert_error_code(self, kana: str, code: ParseKanaErrorCode): + def _assert_error_code(self, kana: str, code: ParseKanaErrorCode) -> None: with self.assertRaises(ParseKanaError) as err: parse_kana(kana) self.assertEqual(err.exception.errcode, code) @@ -556,7 +555,7 @@ def test_exceptions(self): self.assertEqual(err.exception.kwargs, {"position": "2"}) with self.assertRaises(ParseKanaError) as err: - kana_parser.parse_kana("ア?ア'") + kana_converter.parse_kana("ア?ア'") self.assertEqual( err.exception.errcode, ParseKanaErrorCode.INTERROGATION_MARK_NOT_AT_END ) diff --git a/test/tts_pipeline/test_mora_mapping.py b/test/tts_pipeline/test_mora_mapping.py new file mode 100644 index 000000000..0791c0ef4 --- /dev/null +++ b/test/tts_pipeline/test_mora_mapping.py @@ -0,0 +1,20 @@ +from unittest import TestCase + +from voicevox_engine.tts_pipeline.mora_mapping import mora_phonemes_to_mora_kana + + +class TestOpenJTalkMoraList(TestCase): + def test_mora2text(self): + self.assertEqual("ッ", mora_phonemes_to_mora_kana["cl"]) + self.assertEqual("ティ", mora_phonemes_to_mora_kana["ti"]) + self.assertEqual("トゥ", mora_phonemes_to_mora_kana["tu"]) + self.assertEqual("ディ", mora_phonemes_to_mora_kana["di"]) + # GitHub issue #60 + self.assertEqual("ギェ", mora_phonemes_to_mora_kana["gye"]) + self.assertEqual("イェ", mora_phonemes_to_mora_kana["ye"]) + + def test_mora2text_injective(self): + """異なるモーラが同じ読みがなに対応しないか確認する""" + values = list(mora_phonemes_to_mora_kana.values()) + uniq_values = list(set(values)) + self.assertCountEqual(values, uniq_values) diff --git a/test/test_acoustic_feature_extractor.py b/test/tts_pipeline/test_phoneme.py similarity index 52% rename from test/test_acoustic_feature_extractor.py rename to test/tts_pipeline/test_phoneme.py index 94ef7ac63..cc6dd8315 100644 --- a/test/test_acoustic_feature_extractor.py +++ b/test/tts_pipeline/test_phoneme.py @@ -1,33 +1,43 @@ from unittest import TestCase -from voicevox_engine.acoustic_feature_extractor import OjtPhoneme +import pytest +from voicevox_engine.tts_pipeline.phoneme import Phoneme -class TestOjtPhoneme(TestCase): +TRUE_NUM_PHONEME = 45 + + +def test_unknown_phoneme(): + """Unknown音素 `xx` のID取得を拒否する""" + # Inputs + unknown_phoneme = Phoneme("xx") + + # Tests + with pytest.raises(ValueError) as _: + _ = unknown_phoneme.id + + +class TestPhoneme(TestCase): def setUp(self): super().setUp() # list_idx 0 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 hello_hiho = "sil k o N n i ch i w a pau h i h o d e s U sil".split() - self.ojt_hello_hiho = [OjtPhoneme(s) for s in hello_hiho] - - def test_phoneme_list(self): - self.assertEqual(OjtPhoneme.phoneme_list[1], "A") - self.assertEqual(OjtPhoneme.phoneme_list[14], "e") - self.assertEqual(OjtPhoneme.phoneme_list[26], "m") - self.assertEqual(OjtPhoneme.phoneme_list[38], "ts") - self.assertEqual(OjtPhoneme.phoneme_list[41], "v") + self.ojt_hello_hiho = [Phoneme(s) for s in hello_hiho] def test_const(self): - TRUE_NUM_PHONEME = 45 - self.assertEqual(OjtPhoneme.num_phoneme, TRUE_NUM_PHONEME) - self.assertEqual(OjtPhoneme.space_phoneme, "pau") + self.assertEqual(Phoneme._NUM_PHONEME, TRUE_NUM_PHONEME) + self.assertEqual(Phoneme._PHONEME_LIST[1], "A") + self.assertEqual(Phoneme._PHONEME_LIST[14], "e") + self.assertEqual(Phoneme._PHONEME_LIST[26], "m") + self.assertEqual(Phoneme._PHONEME_LIST[38], "ts") + self.assertEqual(Phoneme._PHONEME_LIST[41], "v") def test_convert(self): - sil_phoneme = OjtPhoneme("sil") - self.assertEqual(sil_phoneme.phoneme, "pau") + sil_phoneme = Phoneme("sil") + self.assertEqual(sil_phoneme._phoneme, "pau") def test_phoneme_id(self): - ojt_str_hello_hiho = " ".join([str(p.phoneme_id) for p in self.ojt_hello_hiho]) + ojt_str_hello_hiho = " ".join([str(p.id) for p in self.ojt_hello_hiho]) self.assertEqual( ojt_str_hello_hiho, "0 23 30 4 28 21 10 21 42 7 0 19 21 19 30 12 14 35 6 0" ) @@ -56,7 +66,7 @@ def test_onehot(self): 0, ] for i, phoneme in enumerate(self.ojt_hello_hiho): - for j in range(OjtPhoneme.num_phoneme): + for j in range(TRUE_NUM_PHONEME): if phoneme_id_list[i] == j: self.assertEqual(phoneme.onehot[j], 1.0) else: diff --git a/test/tts_pipeline/test_text_analyzer.py b/test/tts_pipeline/test_text_analyzer.py new file mode 100644 index 000000000..ebaf30977 --- /dev/null +++ b/test/tts_pipeline/test_text_analyzer.py @@ -0,0 +1,423 @@ +from unittest import TestCase + +from voicevox_engine.model import AccentPhrase, Mora +from voicevox_engine.tts_pipeline.text_analyzer import ( + AccentPhraseLabel, + BreathGroupLabel, + Label, + MoraLabel, + UtteranceLabel, + mora_to_text, + text_to_accent_phrases, +) + + +def contexts_to_feature(contexts: dict[str, str]) -> str: + """ラベルの contexts を feature へ変換する""" + return ( + "{p1}^{p2}-{p3}+{p4}={p5}" + "/A:{a1}+{a2}+{a3}" + "/B:{b1}-{b2}_{b3}" + "/C:{c1}_{c2}+{c3}" + "/D:{d1}+{d2}_{d3}" + "/E:{e1}_{e2}!{e3}_{e4}-{e5}" + "/F:{f1}_{f2}#{f3}_{f4}@{f5}_{f6}|{f7}_{f8}" + "/G:{g1}_{g2}%{g3}_{g4}_{g5}" + "/H:{h1}_{h2}" + "/I:{i1}-{i2}@{i3}+{i4}&{i5}-{i6}|{i7}+{i8}" + "/J:{j1}_{j2}" + "/K:{k1}+{k2}-{k3}" + ).format(**contexts) + + +# OpenJTalk コンテナクラス +OjtContainer = MoraLabel | AccentPhraseLabel | BreathGroupLabel | UtteranceLabel + + +def features(ojt_container: OjtContainer) -> list[str]: + """コンテナインスタンスに直接的・間接的に含まれる全ての feature を返す""" + return [contexts_to_feature(p.contexts) for p in ojt_container.labels] + + +class TestBaseLabels(TestCase): + def setUp(self): + super().setUp() + # pyopenjtalk.extract_fullcontext("こんにちは、ヒホです。")の結果 + # 出来る限りテスト内で他のライブラリに依存しないため、 + # またテスト内容を透明化するために、テストケースを生成している + self.test_case_hello_hiho = [ + # sil (無音) + "xx^xx-sil+k=o/A:xx+xx+xx/B:xx-xx_xx/C:xx_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:5_5%0_xx_xx/H:xx_xx/I:xx-xx" + + "@xx+xx&xx-xx|xx+xx/J:1_5/K:2+2-9", + # k + "xx^sil-k+o=N/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # o + "sil^k-o+N=n/A:-4+1+5/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # N (ん) + "k^o-N+n=i/A:-3+2+4/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # n + "o^N-n+i=ch/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # i + "N^n-i+ch=i/A:-2+3+3/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # ch + "n^i-ch+i=w/A:-1+4+2/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # i + "i^ch-i+w=a/A:-1+4+2/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # w + "ch^i-w+a=pau/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # a + "i^w-a+pau=h/A:0+5+1/B:xx-xx_xx/C:09_xx+xx/D:09+xx_xx/E:xx_xx!xx_xx-xx" + + "/F:5_5#0_xx@1_1|1_5/G:4_1%0_xx_0/H:xx_xx/I:1-5" + + "@1+2&1-2|1+9/J:1_4/K:2+2-9", + # pau (読点) + "w^a-pau+h=i/A:xx+xx+xx/B:09-xx_xx/C:xx_xx+xx/D:09+xx_xx/E:5_5!0_xx-xx" + + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:4_1%0_xx_xx/H:1_5/I:xx-xx" + + "@xx+xx&xx-xx|xx+xx/J:1_4/K:2+2-9", + # h + "a^pau-h+i=h/A:0+1+4/B:09-xx_xx/C:09_xx+xx/D:22+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # i + "pau^h-i+h=o/A:0+1+4/B:09-xx_xx/C:09_xx+xx/D:22+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # h + "h^i-h+o=d/A:1+2+3/B:09-xx_xx/C:22_xx+xx/D:10+7_2/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # o + "i^h-o+d=e/A:1+2+3/B:09-xx_xx/C:22_xx+xx/D:10+7_2/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # d + "h^o-d+e=s/A:2+3+2/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # e + "o^d-e+s=U/A:2+3+2/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # s + "d^e-s+U=sil/A:3+4+1/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # U (無声母音) + "e^s-U+sil=xx/A:3+4+1/B:22-xx_xx/C:10_7+2/D:xx+xx_xx/E:5_5!0_xx-0" + + "/F:4_1#0_xx@1_1|1_4/G:xx_xx%xx_xx_xx/H:1_5/I:1-4" + + "@2+1&2-1|6+4/J:xx_xx/K:2+2-9", + # sil (無音) + "s^U-sil+xx=xx/A:xx+xx+xx/B:10-7_2/C:xx_xx+xx/D:xx+xx_xx/E:4_1!0_xx-xx" + + "/F:xx_xx#xx_xx@xx_xx|xx_xx/G:xx_xx%xx_xx_xx/H:1_4/I:xx-xx" + + "@xx+xx&xx-xx|xx+xx/J:xx_xx/K:2+2-9", + ] + self.labels_hello_hiho = [ + Label.from_feature(feature) for feature in self.test_case_hello_hiho + ] + + +def jointed_phonemes(ojt_container: OjtContainer) -> str: + """コンテナインスタンスに直接的・間接的に含まれる全ラベルの音素文字を結合してを返す""" + return "".join([label.phoneme for label in ojt_container.labels]) + + +def space_jointed_phonemes(ojt_container: OjtContainer) -> str: + """コンテナインスタンスに直接的・間接的に含まれる全ラベルの音素文字を ` ` 挟みながら結合してを返す""" + return " ".join([label.phoneme for label in ojt_container.labels]) + + +class TestLabel(TestBaseLabels): + def test_phoneme(self): + """Label に含まれる音素をテスト""" + self.assertEqual( + " ".join([label.phoneme for label in self.labels_hello_hiho]), + "sil k o N n i ch i w a pau h i h o d e s U sil", + ) + + def test_is_pause(self): + """Label のポーズ判定をテスト""" + self.assertEqual( + [label.is_pause() for label in self.labels_hello_hiho], + [ + True, # sil + False, # k + False, # o + False, # N + False, # n + False, # i + False, # ch + False, # i + False, # w + False, # a + True, # pau + False, # h + False, # i + False, # h + False, # o + False, # d + False, # e + False, # s + False, # u + True, # sil + ], + ) + + def test_feature(self) -> None: + """Label に含まれる features をテスト""" + self.assertEqual( + [contexts_to_feature(label.contexts) for label in self.labels_hello_hiho], + self.test_case_hello_hiho, + ) + + +class TestMoraLabel(TestBaseLabels): + def setUp(self) -> None: + super().setUp() + # contexts["a2"] == "1" ko + self.mora_hello_1 = MoraLabel( + consonant=self.labels_hello_hiho[1], vowel=self.labels_hello_hiho[2] + ) + # contexts["a2"] == "2" N + self.mora_hello_2 = MoraLabel(consonant=None, vowel=self.labels_hello_hiho[3]) + # contexts["a2"] == "3" ni + self.mora_hello_3 = MoraLabel( + consonant=self.labels_hello_hiho[4], vowel=self.labels_hello_hiho[5] + ) + # contexts["a2"] == "4" chi + self.mora_hello_4 = MoraLabel( + consonant=self.labels_hello_hiho[6], vowel=self.labels_hello_hiho[7] + ) + # contexts["a2"] == "5" wa + self.mora_hello_5 = MoraLabel( + consonant=self.labels_hello_hiho[8], vowel=self.labels_hello_hiho[9] + ) + # contexts["a2"] == "1" hi + self.mora_hiho_1 = MoraLabel( + consonant=self.labels_hello_hiho[11], vowel=self.labels_hello_hiho[12] + ) + # contexts["a2"] == "2" ho + self.mora_hiho_2 = MoraLabel( + consonant=self.labels_hello_hiho[13], vowel=self.labels_hello_hiho[14] + ) + # contexts["a2"] == "3" de + self.mora_hiho_3 = MoraLabel( + consonant=self.labels_hello_hiho[15], vowel=self.labels_hello_hiho[16] + ) + # contexts["a2"] == "1" sU + self.mora_hiho_4 = MoraLabel( + consonant=self.labels_hello_hiho[17], vowel=self.labels_hello_hiho[18] + ) + + def test_phonemes(self) -> None: + """MoraLabel に含まれる音素系列をテスト""" + self.assertEqual(jointed_phonemes(self.mora_hello_1), "ko") + self.assertEqual(jointed_phonemes(self.mora_hello_2), "N") + self.assertEqual(jointed_phonemes(self.mora_hello_3), "ni") + self.assertEqual(jointed_phonemes(self.mora_hello_4), "chi") + self.assertEqual(jointed_phonemes(self.mora_hello_5), "wa") + self.assertEqual(jointed_phonemes(self.mora_hiho_1), "hi") + self.assertEqual(jointed_phonemes(self.mora_hiho_2), "ho") + self.assertEqual(jointed_phonemes(self.mora_hiho_3), "de") + self.assertEqual(jointed_phonemes(self.mora_hiho_4), "sU") + + def test_features(self) -> None: + """MoraLabel に含まれる features をテスト""" + expects = self.test_case_hello_hiho + self.assertEqual(features(self.mora_hello_1), expects[1:3]) + self.assertEqual(features(self.mora_hello_2), expects[3:4]) + self.assertEqual(features(self.mora_hello_3), expects[4:6]) + self.assertEqual(features(self.mora_hello_4), expects[6:8]) + self.assertEqual(features(self.mora_hello_5), expects[8:10]) + self.assertEqual(features(self.mora_hiho_1), expects[11:13]) + self.assertEqual(features(self.mora_hiho_2), expects[13:15]) + self.assertEqual(features(self.mora_hiho_3), expects[15:17]) + self.assertEqual(features(self.mora_hiho_4), expects[17:19]) + + +class TestAccentPhraseLabel(TestBaseLabels): + def setUp(self) -> None: + super().setUp() + # TODO: ValueErrorを吐く作為的ではない自然な例の模索 + # 存在しないなら放置でよい + self.accent_phrase_hello = AccentPhraseLabel.from_labels( + self.labels_hello_hiho[1:10] + ) + self.accent_phrase_hiho = AccentPhraseLabel.from_labels( + self.labels_hello_hiho[11:19] + ) + + def test_accent(self): + """AccentPhraseLabel に含まれるアクセント位置をテスト""" + self.assertEqual(self.accent_phrase_hello.accent, 5) + self.assertEqual(self.accent_phrase_hiho.accent, 1) + + def test_phonemes(self): + """AccentPhraseLabel に含まれる音素系列をテスト""" + outputs_hello = space_jointed_phonemes(self.accent_phrase_hello) + outputs_hiho = space_jointed_phonemes(self.accent_phrase_hiho) + self.assertEqual(outputs_hello, "k o N n i ch i w a") + self.assertEqual(outputs_hiho, "h i h o d e s U") + + def test_features(self): + """AccentPhraseLabel に含まれる features をテスト""" + expects = self.test_case_hello_hiho + self.assertEqual(features(self.accent_phrase_hello), expects[1:10]) + self.assertEqual(features(self.accent_phrase_hiho), expects[11:19]) + + +class TestBreathGroupLabel(TestBaseLabels): + def setUp(self) -> None: + super().setUp() + self.breath_group_hello = BreathGroupLabel.from_labels( + self.labels_hello_hiho[1:10] + ) + self.breath_group_hiho = BreathGroupLabel.from_labels( + self.labels_hello_hiho[11:19] + ) + + def test_phonemes(self): + """BreathGroupLabel に含まれる音素系列をテスト""" + outputs_hello = space_jointed_phonemes(self.breath_group_hello) + outputs_hiho = space_jointed_phonemes(self.breath_group_hiho) + self.assertEqual(outputs_hello, "k o N n i ch i w a") + self.assertEqual(outputs_hiho, "h i h o d e s U") + + def test_features(self): + """BreathGroupLabel に含まれる features をテスト""" + expects = self.test_case_hello_hiho + self.assertEqual(features(self.breath_group_hello), expects[1:10]) + self.assertEqual(features(self.breath_group_hiho), expects[11:19]) + + +class TestUtteranceLabel(TestBaseLabels): + def setUp(self) -> None: + super().setUp() + self.utterance_hello_hiho = UtteranceLabel.from_labels(self.labels_hello_hiho) + + def test_phonemes(self): + """UtteranceLabel に含まれる音素系列をテスト""" + outputs_hello_hiho = space_jointed_phonemes(self.utterance_hello_hiho) + expects_hello_hiho = "sil k o N n i ch i w a pau h i h o d e s U sil" + self.assertEqual(outputs_hello_hiho, expects_hello_hiho) + + def test_features(self): + """UtteranceLabel に含まれる features をテスト""" + self.assertEqual(features(self.utterance_hello_hiho), self.test_case_hello_hiho) + + +class TestMoraToText(TestCase): + def test_voice(self): + self.assertEqual(mora_to_text("a"), "ア") + self.assertEqual(mora_to_text("i"), "イ") + self.assertEqual(mora_to_text("ka"), "カ") + self.assertEqual(mora_to_text("N"), "ン") + self.assertEqual(mora_to_text("cl"), "ッ") + self.assertEqual(mora_to_text("gye"), "ギェ") + self.assertEqual(mora_to_text("ye"), "イェ") + self.assertEqual(mora_to_text("wo"), "ウォ") + + def test_unvoice(self): + self.assertEqual(mora_to_text("A"), "ア") + self.assertEqual(mora_to_text("I"), "イ") + self.assertEqual(mora_to_text("kA"), "カ") + self.assertEqual(mora_to_text("gyE"), "ギェ") + self.assertEqual(mora_to_text("yE"), "イェ") + self.assertEqual(mora_to_text("wO"), "ウォ") + + def test_invalid_mora(self): + """変なモーラが来ても例外を投げない""" + self.assertEqual(mora_to_text("x"), "x") + self.assertEqual(mora_to_text(""), "") + + +def _gen_mora(text: str, consonant: str | None, vowel: str) -> Mora: + return Mora( + text=text, + consonant=consonant, + consonant_length=0 if consonant else None, + vowel=vowel, + vowel_length=0, + pitch=0, + ) + + +def test_text_to_accent_phrases_normal(): + """`text_to_accent_phrases` は正常な日本語文をパースする""" + # Inputs + text = "こんにちは、ヒホです。" + # Expects + true_accent_phrases = [ + AccentPhrase( + moras=[ + _gen_mora("コ", "k", "o"), + _gen_mora("ン", None, "N"), + _gen_mora("ニ", "n", "i"), + _gen_mora("チ", "ch", "i"), + _gen_mora("ワ", "w", "a"), + ], + accent=5, + pause_mora=_gen_mora("、", None, "pau"), + ), + AccentPhrase( + moras=[ + _gen_mora("ヒ", "h", "i"), + _gen_mora("ホ", "h", "o"), + _gen_mora("デ", "d", "e"), + _gen_mora("ス", "s", "U"), + ], + accent=1, + pause_mora=None, + ), + ] + # Outputs + accent_phrases = text_to_accent_phrases(text) + # Tests + assert accent_phrases == true_accent_phrases + + +def stub_unknown_features_koxx(_: str) -> list[str]: + """`sil-k-o-xx-sil` に相当する features を常に返す `text_to_features()` のStub""" + return [ + ".^.-sil+.=./A:.+xx+./B:.-._./C:._.+./D:.+._./E:._.!._.-./F:xx_xx#xx_.@xx_.|._./G:._.%._._./H:._./I:.-.@xx+.&.-.|.+./J:._./K:.+.-.", + ".^.-k+.=./A:.+1+./B:.-._./C:._.+./D:.+._./E:._.!._.-./F:2_1#0_.@1_.|._./G:._.%._._./H:._./I:.-.@1+.&.-.|.+./J:._./K:.+.-.", + ".^.-o+.=./A:.+1+./B:.-._./C:._.+./D:.+._./E:._.!._.-./F:2_1#0_.@1_.|._./G:._.%._._./H:._./I:.-.@1+.&.-.|.+./J:._./K:.+.-.", + ".^.-xx+.=./A:.+2+./B:.-._./C:._.+./D:.+._./E:._.!._.-./F:2_1#0_.@1_.|._./G:._.%._._./H:._./I:.-.@1+.&.-.|.+./J:._./K:.+.-.", + ".^.-sil+.=./A:.+xx+./B:.-._./C:._.+./D:.+._./E:._.!._.-./F:xx_xx#xx_.@xx_.|._./G:._.%._._./H:._./I:.-.@xx+.&.-.|.+./J:._./K:.+.-.", + ] + + +def test_text_to_accent_phrases_unknown(): + """`text_to_accent_phrases` は unknown 音素を含む features をパースする""" + # Expects + true_accent_phrases = [ + AccentPhrase( + moras=[ + _gen_mora("コ", "k", "o"), + _gen_mora("xx", None, "xx"), + ], + accent=1, + pause_mora=None, + ), + ] + # Outputs + accent_phrases = text_to_accent_phrases( + "dummy", text_to_features=stub_unknown_features_koxx + ) + # Tests + assert accent_phrases == true_accent_phrases diff --git a/test/tts_pipeline/test_tts_engine.py b/test/tts_pipeline/test_tts_engine.py new file mode 100644 index 000000000..8d4c322b6 --- /dev/null +++ b/test/tts_pipeline/test_tts_engine.py @@ -0,0 +1,723 @@ +from test.utility import pydantic_to_native_type, round_floats +from unittest import TestCase +from unittest.mock import Mock + +import numpy as np +import pytest +from numpy.typing import NDArray +from syrupy.assertion import SnapshotAssertion + +from voicevox_engine.dev.core.mock import MockCoreWrapper +from voicevox_engine.metas.Metas import StyleId +from voicevox_engine.model import ( + AccentPhrase, + AudioQuery, + FrameAudioQuery, + Mora, + Note, + Score, +) +from voicevox_engine.tts_pipeline.text_analyzer import text_to_accent_phrases +from voicevox_engine.tts_pipeline.tts_engine import ( + TTSEngine, + apply_interrogative_upspeak, + to_flatten_moras, + to_flatten_phonemes, +) + +from .test_text_analyzer import stub_unknown_features_koxx + + +def yukarin_s_mock( + length: int, phoneme_list: NDArray[np.int64], style_id: NDArray[np.int64] +) -> NDArray[np.float32]: + result = [] + # mockとしての適当な処理、特に意味はない + for i in range(length): + result.append(round((phoneme_list[i] * 0.0625 + style_id).item(), 2)) + return np.array(result, dtype=np.float32) + + +def yukarin_sa_mock( + length: int, + vowel_phoneme_list: NDArray[np.int64], + consonant_phoneme_list: NDArray[np.int64], + start_accent_list: NDArray[np.int64], + end_accent_list: NDArray[np.int64], + start_accent_phrase_list: NDArray[np.int64], + end_accent_phrase_list: NDArray[np.int64], + style_id: NDArray[np.int64], +) -> NDArray[np.float32]: + result = [] + # mockとしての適当な処理、特に意味はない + for i in range(length): + result.append( + round( + ( + ( + vowel_phoneme_list[0][i] + + consonant_phoneme_list[0][i] + + start_accent_list[0][i] + + end_accent_list[0][i] + + start_accent_phrase_list[0][i] + + end_accent_phrase_list[0][i] + ) + * 0.0625 + + style_id + ).item(), + 2, + ) + ) + return np.array(result, dtype=np.float32)[np.newaxis] + + +def decode_mock( + length: int, + phoneme_size: int, + f0: NDArray[np.float32], + phoneme: NDArray[np.float32], + style_id: NDArray[np.int64], +) -> NDArray[np.float32]: + result = [] + # mockとしての適当な処理、特に意味はない + for i in range(length): + result += [ + (f0[i, 0] * (np.where(phoneme[i] == 1)[0] / phoneme_size) + style_id) + ] * 256 + return np.array(result, dtype=np.float32) + + +class MockCore: + default_sampling_rate = 24000 + yukarin_s_forward = Mock(side_effect=yukarin_s_mock) + yukarin_sa_forward = Mock(side_effect=yukarin_sa_mock) + decode_forward = Mock(side_effect=decode_mock) + + def metas(self): + return "" + + def supported_devices(self): + return "" + + def is_model_loaded(self, style_id): + return True + + +def _gen_mora( + text: str, + consonant: str | None, + consonant_length: float | None, + vowel: str, + vowel_length: float, + pitch: float, +) -> Mora: + """Generate Mora with positional arguments for test simplicity.""" + return Mora( + text=text, + consonant=consonant, + consonant_length=consonant_length, + vowel=vowel, + vowel_length=vowel_length, + pitch=pitch, + ) + + +def test_to_flatten_phonemes(): + """Test `to_flatten_phonemes`.""" + # Inputs + moras = [ + _gen_mora(" ", None, None, "sil", 2 * 0.01067, 0.0), + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 100.0), + _gen_mora(" ", None, None, "sil", 6 * 0.01067, 0.0), + ] + + # Expects + true_phonemes = ["pau", "h", "i", "pau"] + + # Outputs + phonemes = list(map(lambda p: p._phoneme, to_flatten_phonemes(moras))) + + assert true_phonemes == phonemes + + +def _gen_hello_hiho_text() -> str: + return "こんにちは、ヒホです" + + +def _gen_hello_hiho_kana() -> str: + return "コンニチワ'、ヒ'ホデ_ス" + + +def _gen_hello_hiho_accent_phrases() -> list[AccentPhrase]: + return [ + AccentPhrase( + moras=[ + _gen_mora("コ", "k", 0.0, "o", 0.0, 0.0), + _gen_mora("ン", None, None, "N", 0.0, 0.0), + _gen_mora("ニ", "n", 0.0, "i", 0.0, 0.0), + _gen_mora("チ", "ch", 0.0, "i", 0.0, 0.0), + _gen_mora("ワ", "w", 0.0, "a", 0.0, 0.0), + ], + accent=5, + pause_mora=_gen_mora("、", None, None, "pau", 0.0, 0.0), + ), + AccentPhrase( + moras=[ + _gen_mora("ヒ", "h", 0.0, "i", 0.0, 0.0), + _gen_mora("ホ", "h", 0.0, "o", 0.0, 0.0), + _gen_mora("デ", "d", 0.0, "e", 0.0, 0.0), + _gen_mora("ス", "s", 0.0, "U", 0.0, 0.0), + ], + accent=1, + pause_mora=None, + ), + ] + + +def _gen_hello_hiho_query() -> AudioQuery: + return AudioQuery( + accent_phrases=_gen_hello_hiho_accent_phrases(), + speedScale=2.0, + pitchScale=1.1, + intonationScale=0.9, + volumeScale=1.3, + prePhonemeLength=0.1, + postPhonemeLength=0.2, + outputSamplingRate=12000, + outputStereo=True, + kana=_gen_hello_hiho_kana(), + ) + + +def _gen_doremi_score() -> Score: + return Score( + notes=[ + Note(key=None, frame_length=10, lyric=""), + Note(key=60, frame_length=12, lyric="ど"), + Note(key=62, frame_length=17, lyric="れ"), + Note(key=64, frame_length=21, lyric="み"), + Note(key=None, frame_length=5, lyric=""), + Note(key=65, frame_length=12, lyric="ふぁ"), + Note(key=67, frame_length=17, lyric="そ"), + Note(key=None, frame_length=10, lyric=""), + ] + ) + + +class TestTTSEngine(TestCase): + def setUp(self): + super().setUp() + core = MockCore() + self.yukarin_s_mock = core.yukarin_s_forward + self.yukarin_sa_mock = core.yukarin_sa_forward + self.decode_mock = core.decode_forward + self.tts_engine = TTSEngine(core=core) # type: ignore[arg-type] + + def test_to_flatten_moras(self): + flatten_moras = to_flatten_moras(_gen_hello_hiho_accent_phrases()) + true_accent_phrases_hello_hiho = _gen_hello_hiho_accent_phrases() + self.assertEqual( + flatten_moras, + true_accent_phrases_hello_hiho[0].moras + + [true_accent_phrases_hello_hiho[0].pause_mora] + + true_accent_phrases_hello_hiho[1].moras, + ) + + def test_update_length(self): + # Inputs + hello_hiho = _gen_hello_hiho_accent_phrases() + # Indirect Outputs(yukarin_sに渡される値) + self.tts_engine.update_length(hello_hiho, StyleId(1)) + yukarin_s_args = self.yukarin_s_mock.call_args[1] + list_length = yukarin_s_args["length"] + phoneme_list = yukarin_s_args["phoneme_list"] + style_id = yukarin_s_args["style_id"] + # Expects + true_list_length = 20 + true_style_id = 1 + true_phoneme_list_1 = [0, 23, 30, 4, 28, 21, 10, 21, 42, 7] + true_phoneme_list_2 = [0, 19, 21, 19, 30, 12, 14, 35, 6, 0] + true_phoneme_list = true_phoneme_list_1 + true_phoneme_list_2 + + self.assertEqual(list_length, true_list_length) + self.assertEqual(list_length, len(phoneme_list)) + self.assertEqual(style_id, true_style_id) + np.testing.assert_array_equal( + phoneme_list, + np.array(true_phoneme_list, dtype=np.int64), + ) + + def test_update_pitch(self): + # 空のリストでエラーを吐かないか + # Inputs + phrases: list = [] + # Outputs + result = self.tts_engine.update_pitch(phrases, StyleId(1)) + # Expects + true_result: list = [] + # Tests + self.assertEqual(result, true_result) + + # Inputs + hello_hiho = _gen_hello_hiho_accent_phrases() + # Indirect Outputs(yukarin_saに渡される値) + self.tts_engine.update_pitch(hello_hiho, StyleId(1)) + yukarin_sa_args = self.yukarin_sa_mock.call_args[1] + list_length = yukarin_sa_args["length"] + vowel_phoneme_list = yukarin_sa_args["vowel_phoneme_list"][0] + consonant_phoneme_list = yukarin_sa_args["consonant_phoneme_list"][0] + start_accent_list = yukarin_sa_args["start_accent_list"][0] + end_accent_list = yukarin_sa_args["end_accent_list"][0] + start_accent_phrase_list = yukarin_sa_args["start_accent_phrase_list"][0] + end_accent_phrase_list = yukarin_sa_args["end_accent_phrase_list"][0] + style_id = yukarin_sa_args["style_id"] + # Expects + true_vowels = np.array([0, 30, 4, 21, 21, 7, 0, 21, 30, 14, 6, 0]) + true_consonants = np.array([-1, 23, -1, 28, 10, 42, -1, 19, 19, 12, 35, -1]) + true_accent_starts = np.array([0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0]) + true_accent_ends = np.array([0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0]) + true_phrase_starts = np.array([0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]) + true_phrase_ends = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]) + + # Tests + self.assertEqual(list_length, 12) + self.assertEqual(list_length, len(vowel_phoneme_list)) + self.assertEqual(list_length, len(consonant_phoneme_list)) + self.assertEqual(list_length, len(start_accent_list)) + self.assertEqual(list_length, len(end_accent_list)) + self.assertEqual(list_length, len(start_accent_phrase_list)) + self.assertEqual(list_length, len(end_accent_phrase_list)) + self.assertEqual(style_id, 1) + np.testing.assert_array_equal(vowel_phoneme_list, true_vowels) + np.testing.assert_array_equal(consonant_phoneme_list, true_consonants) + np.testing.assert_array_equal(start_accent_list, true_accent_starts) + np.testing.assert_array_equal(end_accent_list, true_accent_ends) + np.testing.assert_array_equal(start_accent_phrase_list, true_phrase_starts) + np.testing.assert_array_equal(end_accent_phrase_list, true_phrase_ends) + + +def test_create_accent_phrases_toward_unknown(): + """`TTSEngine.create_accent_phrases()` は unknown 音素の Phoneme 化に失敗する""" + engine = TTSEngine(MockCoreWrapper()) + + # NOTE: TTSEngine.create_accent_phrases() のコールで unknown feature を得ることが難しいため、疑似再現 + accent_phrases = text_to_accent_phrases( + "dummy", text_to_features=stub_unknown_features_koxx + ) + with pytest.raises(ValueError) as e: + accent_phrases = engine.update_length_and_pitch(accent_phrases, StyleId(0)) + assert str(e.value) == "tuple.index(x): x not in tuple" + + +def test_mocked_update_length_output(snapshot_json: SnapshotAssertion) -> None: + """モックされた `TTSEngine.update_length()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_accent_phrases() + # Outputs + result = tts_engine.update_length(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(pydantic_to_native_type(result), round_value=2) + + +def test_mocked_update_pitch_output(snapshot_json: SnapshotAssertion) -> None: + """モックされた `TTSEngine.update_pitch()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_accent_phrases() + # Outputs + result = tts_engine.update_pitch(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(pydantic_to_native_type(result), round_value=2) + + +def test_mocked_update_length_and_pitch_output( + snapshot_json: SnapshotAssertion, +) -> None: + """モックされた `TTSEngine.update_length_and_pitch()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_accent_phrases() + # Outputs + result = tts_engine.update_length_and_pitch(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(pydantic_to_native_type(result), round_value=2) + + +def test_mocked_create_accent_phrases_output( + snapshot_json: SnapshotAssertion, +) -> None: + """モックされた `TTSEngine.create_accent_phrases()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_text() + # Outputs + result = tts_engine.create_accent_phrases(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(pydantic_to_native_type(result), round_value=2) + + +def test_mocked_create_accent_phrases_from_kana_output( + snapshot_json: SnapshotAssertion, +) -> None: + """モックされた `TTSEngine.create_accent_phrases_from_kana()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_kana() + # Outputs + result = tts_engine.create_accent_phrases_from_kana(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(pydantic_to_native_type(result), round_value=2) + + +def test_mocked_synthesize_wave_output(snapshot_json: SnapshotAssertion) -> None: + """モックされた `TTSEngine.synthesize_wave()` の出力スナップショットが一定である""" + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + hello_hiho = _gen_hello_hiho_query() + # Outputs + result = tts_engine.synthesize_wave(hello_hiho, StyleId(1)) + # Tests + assert snapshot_json == round_floats(result.tolist(), round_value=2) + + +def test_mocked_synthesize_wave_from_score_output( + snapshot_json: SnapshotAssertion, +) -> None: + """ + モックされた `TTSEngine.create_sing_phoneme_and_f0_and_volume()` と + `TTSEngine.frame_synthsize_wave()` の出力スナップショットが一定である + """ + # Inputs + tts_engine = TTSEngine(MockCoreWrapper()) + doremi_srore = _gen_doremi_score() + # Outputs + result = tts_engine.create_sing_phoneme_and_f0_and_volume(doremi_srore, StyleId(1)) + # Tests + assert snapshot_json(name="query") == round_floats( + pydantic_to_native_type(result), round_value=2 + ) + + # Inputs + phonemes, f0, volume = result + doremi_query = FrameAudioQuery( + f0=f0, + volume=volume, + phonemes=phonemes, + volumeScale=1.3, + outputSamplingRate=1200, + outputStereo=False, + ) + # Outputs + result_wave = tts_engine.frame_synthsize_wave(doremi_query, StyleId(1)) + # Tests + assert snapshot_json(name="wave") == round_floats( + result_wave.tolist(), round_value=2 + ) + + +def koreha_arimasuka_base_expected(): + return [ + AccentPhrase( + moras=[ + Mora( + text="コ", + consonant="k", + consonant_length=np.float32(2.44), + vowel="o", + vowel_length=np.float32(2.88), + pitch=np.float32(4.38), + ), + Mora( + text="レ", + consonant="r", + consonant_length=np.float32(3.06), + vowel="e", + vowel_length=np.float32(1.88), + pitch=np.float32(4.0), + ), + Mora( + text="ワ", + consonant="w", + consonant_length=np.float32(3.62), + vowel="a", + vowel_length=np.float32(1.44), + pitch=np.float32(4.19), + ), + ], + accent=3, + pause_mora=None, + is_interrogative=False, + ), + AccentPhrase( + moras=[ + Mora( + text="ア", + consonant=None, + consonant_length=None, + vowel="a", + vowel_length=np.float32(1.44), + pitch=np.float32(1.44), + ), + Mora( + text="リ", + consonant="r", + consonant_length=np.float32(3.06), + vowel="i", + vowel_length=np.float32(2.31), + pitch=np.float32(4.44), + ), + Mora( + text="マ", + consonant="m", + consonant_length=np.float32(2.62), + vowel="a", + vowel_length=np.float32(1.44), + pitch=np.float32(3.12), + ), + Mora( + text="ス", + consonant="s", + consonant_length=np.float32(3.19), + vowel="U", + vowel_length=np.float32(1.38), + pitch=np.float32(0.0), + ), + Mora( + text="カ", + consonant="k", + consonant_length=np.float32(2.44), + vowel="a", + vowel_length=np.float32(1.44), + pitch=np.float32(2.94), + ), + ], + accent=3, + pause_mora=None, + is_interrogative=False, + ), + ] + + +class TestTTSEngineBase(TestCase): + def setUp(self): + super().setUp() + self.tts_engine = TTSEngine(core=MockCoreWrapper()) + + def create_synthesis_test_base( + self, + text: str, + expected: list[AccentPhrase], + enable_interrogative_upspeak: bool, + ) -> None: + """音声合成時に疑問文モーラ処理を行っているかどうかを検証 + (https://github.com/VOICEVOX/voicevox_engine/issues/272#issuecomment-1022610866) + """ + inputs = self.tts_engine.create_accent_phrases(text, StyleId(1)) + outputs = apply_interrogative_upspeak(inputs, enable_interrogative_upspeak) + self.assertEqual(expected, outputs, f"case(text:{text})") + + def test_create_accent_phrases(self): + """accent_phrasesの作成時では疑問文モーラ処理を行わない + (https://github.com/VOICEVOX/voicevox_engine/issues/272#issuecomment-1022610866) + """ + text = "これはありますか?" + expected = koreha_arimasuka_base_expected() + expected[-1].is_interrogative = True + actual = self.tts_engine.create_accent_phrases(text, StyleId(1)) + self.assertEqual(expected, actual, f"case(text:{text})") + + def test_upspeak_voiced_last_mora(self): + # voiced + "?" + flagON -> upspeak + expected = koreha_arimasuka_base_expected() + expected[-1].is_interrogative = True + expected[-1].moras += [ + Mora( + text="ア", + consonant=None, + consonant_length=None, + vowel="a", + vowel_length=0.15, + pitch=np.float32(expected[-1].moras[-1].pitch) + 0.3, + ) + ] + self.create_synthesis_test_base( + text="これはありますか?", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # voiced + "?" + flagOFF -> non-upspeak + expected = koreha_arimasuka_base_expected() + expected[-1].is_interrogative = True + self.create_synthesis_test_base( + text="これはありますか?", + expected=expected, + enable_interrogative_upspeak=False, + ) + + # voiced + "" + flagON -> non-upspeak + expected = koreha_arimasuka_base_expected() + self.create_synthesis_test_base( + text="これはありますか", + expected=expected, + enable_interrogative_upspeak=True, + ) + + def test_upspeak_voiced_N_last_mora(self): + def nn_base_expected(): + return [ + AccentPhrase( + moras=[ + Mora( + text="ン", + consonant=None, + consonant_length=None, + vowel="N", + vowel_length=np.float32(1.25), + pitch=np.float32(1.44), + ) + ], + accent=1, + pause_mora=None, + is_interrogative=False, + ) + ] + + # voiced + "" + flagON -> upspeak + expected = nn_base_expected() + self.create_synthesis_test_base( + text="ん", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # voiced + "?" + flagON -> upspeak + expected = nn_base_expected() + expected[-1].is_interrogative = True + expected[-1].moras += [ + Mora( + text="ン", + consonant=None, + consonant_length=None, + vowel="N", + vowel_length=0.15, + pitch=np.float32(expected[-1].moras[-1].pitch) + 0.3, + ) + ] + self.create_synthesis_test_base( + text="ん?", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # voiced + "?" + flagOFF -> non-upspeak + expected = nn_base_expected() + expected[-1].is_interrogative = True + self.create_synthesis_test_base( + text="ん?", + expected=expected, + enable_interrogative_upspeak=False, + ) + + def test_upspeak_unvoiced_last_mora(self): + def ltu_base_expected(): + return [ + AccentPhrase( + moras=[ + Mora( + text="ッ", + consonant=None, + consonant_length=None, + vowel="cl", + vowel_length=np.float32(1.69), + pitch=np.float32(0.0), + ) + ], + accent=1, + pause_mora=None, + is_interrogative=False, + ) + ] + + # unvoiced + "" + flagON -> non-upspeak + expected = ltu_base_expected() + self.create_synthesis_test_base( + text="っ", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # unvoiced + "?" + flagON -> non-upspeak + expected = ltu_base_expected() + expected[-1].is_interrogative = True + self.create_synthesis_test_base( + text="っ?", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # unvoiced + "?" + flagOFF -> non-upspeak + expected = ltu_base_expected() + expected[-1].is_interrogative = True + self.create_synthesis_test_base( + text="っ?", + expected=expected, + enable_interrogative_upspeak=False, + ) + + def test_upspeak_voiced_u_last_mora(self): + def su_base_expected(): + return [ + AccentPhrase( + moras=[ + Mora( + text="ス", + consonant="s", + consonant_length=np.float32(3.19), + vowel="u", + vowel_length=np.float32(3.5), + pitch=np.float32(5.94), + ) + ], + accent=1, + pause_mora=None, + is_interrogative=False, + ) + ] + + # voiced + "" + flagON -> non-upspeak + expected = su_base_expected() + self.create_synthesis_test_base( + text="す", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # voiced + "?" + flagON -> upspeak + expected = su_base_expected() + expected[-1].is_interrogative = True + expected[-1].moras += [ + Mora( + text="ウ", + consonant=None, + consonant_length=None, + vowel="u", + vowel_length=0.15, + pitch=expected[-1].moras[-1].pitch + 0.3, + ) + ] + self.create_synthesis_test_base( + text="す?", + expected=expected, + enable_interrogative_upspeak=True, + ) + + # voiced + "?" + flagOFF -> non-upspeak + expected = su_base_expected() + expected[-1].is_interrogative = True + self.create_synthesis_test_base( + text="す?", + expected=expected, + enable_interrogative_upspeak=False, + ) diff --git a/test/tts_pipeline/test_wave_synthesizer.py b/test/tts_pipeline/test_wave_synthesizer.py new file mode 100644 index 000000000..084edcec6 --- /dev/null +++ b/test/tts_pipeline/test_wave_synthesizer.py @@ -0,0 +1,332 @@ +"""波形合成のテスト""" + +import numpy as np + +from voicevox_engine.model import AccentPhrase, AudioQuery, Mora +from voicevox_engine.tts_pipeline.tts_engine import ( + apply_intonation_scale, + apply_output_sampling_rate, + apply_output_stereo, + apply_pitch_scale, + apply_prepost_silence, + apply_speed_scale, + apply_volume_scale, + count_frame_per_unit, + query_to_decoder_feature, + raw_wave_to_output_wave, +) + +TRUE_NUM_PHONEME = 45 + + +def _gen_query( + accent_phrases: list[AccentPhrase] | None = None, + speedScale: float = 1.0, + pitchScale: float = 1.0, + intonationScale: float = 1.0, + prePhonemeLength: float = 0.0, + postPhonemeLength: float = 0.0, + volumeScale: float = 1.0, + outputSamplingRate: int = 24000, + outputStereo: bool = False, +) -> AudioQuery: + """Generate AudioQuery with default meaningless arguments for test simplicity.""" + accent_phrases = [] if accent_phrases is None else accent_phrases + return AudioQuery( + accent_phrases=accent_phrases, + speedScale=speedScale, + pitchScale=pitchScale, + intonationScale=intonationScale, + prePhonemeLength=prePhonemeLength, + postPhonemeLength=postPhonemeLength, + volumeScale=volumeScale, + outputSamplingRate=outputSamplingRate, + outputStereo=outputStereo, + ) + + +def _gen_mora( + text: str, + consonant: str | None, + consonant_length: float | None, + vowel: str, + vowel_length: float, + pitch: float, +) -> Mora: + """Generate Mora with positional arguments for test simplicity.""" + return Mora( + text=text, + consonant=consonant, + consonant_length=consonant_length, + vowel=vowel, + vowel_length=vowel_length, + pitch=pitch, + ) + + +def test_apply_prepost_silence(): + """Test `apply_prepost_silence`.""" + # Inputs + query = _gen_query(prePhonemeLength=2 * 0.01067, postPhonemeLength=6 * 0.01067) + moras = [ + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 100.0), + ] + + # Expects + true_moras_with_silence = [ + _gen_mora(" ", None, None, "sil", 2 * 0.01067, 0.0), + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 100.0), + _gen_mora(" ", None, None, "sil", 6 * 0.01067, 0.0), + ] + + # Outputs + moras_with_silence = apply_prepost_silence(moras, query) + + assert moras_with_silence == true_moras_with_silence + + +def test_apply_speed_scale(): + """Test `apply_speed_scale`.""" + # Inputs + query = _gen_query(speedScale=2.0) + input_moras = [ + _gen_mora("コ", "k", 2 * 0.01067, "o", 4 * 0.01067, 50.0), + _gen_mora("ン", None, None, "N", 4 * 0.01067, 50.0), + _gen_mora("、", None, None, "pau", 2 * 0.01067, 0.0), + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 125.0), + _gen_mora("ホ", "h", 4 * 0.01067, "O", 2 * 0.01067, 0.0), + ] + + # Expects - x2 fast + true_moras = [ + _gen_mora("コ", "k", 1 * 0.01067, "o", 2 * 0.01067, 50.0), + _gen_mora("ン", None, None, "N", 2 * 0.01067, 50.0), + _gen_mora("、", None, None, "pau", 1 * 0.01067, 0.0), + _gen_mora("ヒ", "h", 1 * 0.01067, "i", 2 * 0.01067, 125.0), + _gen_mora("ホ", "h", 2 * 0.01067, "O", 1 * 0.01067, 0.0), + ] + + # Outputs + moras = apply_speed_scale(input_moras, query) + + assert moras == true_moras + + +def test_apply_pitch_scale(): + """Test `apply_pitch_scale`.""" + # Inputs + query = _gen_query(pitchScale=2.0) + input_moras = [ + _gen_mora("コ", "k", 0.0, "o", 0.0, 50.0), + _gen_mora("ン", None, None, "N", 0.0, 50.0), + _gen_mora("、", None, None, "pau", 0.0, 0.0), + _gen_mora("ヒ", "h", 0.0, "i", 0.0, 125.0), + _gen_mora("ホ", "h", 0.0, "O", 0.0, 0.0), + ] + + # Expects - x4 value scaled + true_moras = [ + _gen_mora("コ", "k", 0.0, "o", 0.0, 200.0), + _gen_mora("ン", None, None, "N", 0.0, 200.0), + _gen_mora("、", None, None, "pau", 0.0, 0.0), + _gen_mora("ヒ", "h", 0.0, "i", 0.0, 500.0), + _gen_mora("ホ", "h", 0.0, "O", 0.0, 0.0), + ] + + # Outputs + moras = apply_pitch_scale(input_moras, query) + + assert moras == true_moras + + +def test_apply_intonation_scale(): + """Test `apply_intonation_scale`.""" + # Inputs + query = _gen_query(intonationScale=0.5) + input_moras = [ + _gen_mora("コ", "k", 0.0, "o", 0.0, 200.0), + _gen_mora("ン", None, None, "N", 0.0, 200.0), + _gen_mora("、", None, None, "pau", 0.0, 0.0), + _gen_mora("ヒ", "h", 0.0, "i", 0.0, 500.0), + _gen_mora("ホ", "h", 0.0, "O", 0.0, 0.0), + ] + + # Expects - mean=300 var x0.5 intonation scaling + true_moras = [ + _gen_mora("コ", "k", 0.0, "o", 0.0, 250.0), + _gen_mora("ン", None, None, "N", 0.0, 250.0), + _gen_mora("、", None, None, "pau", 0.0, 0.0), + _gen_mora("ヒ", "h", 0.0, "i", 0.0, 400.0), + _gen_mora("ホ", "h", 0.0, "O", 0.0, 0.0), + ] + + # Outputs + moras = apply_intonation_scale(input_moras, query) + + assert moras == true_moras + + +def test_apply_volume_scale(): + """Test `apply_volume_scale`.""" + # Inputs + query = _gen_query(volumeScale=3.0) + input_wave = np.array([0.0, 1.0, 2.0]) + + # Expects - x3 scale + true_wave = np.array([0.0, 3.0, 6.0]) + + # Outputs + wave = apply_volume_scale(input_wave, query) + + assert np.allclose(wave, true_wave) + + +def test_apply_output_sampling_rate(): + """Test `apply_output_sampling_rate`.""" + # Inputs + query = _gen_query(outputSamplingRate=12000) + input_wave = np.array([1.0 for _ in range(120)]) + input_sr_wave = 24000 + + # Expects - half sampling rate + true_wave = np.array([1.0 for _ in range(60)]) + assert true_wave.shape == (60,), "Prerequisites" + + # Outputs + wave = apply_output_sampling_rate(input_wave, input_sr_wave, query) + + assert wave.shape[0] == true_wave.shape[0] + + +def test_apply_output_stereo(): + """Test `apply_output_stereo`.""" + # Inputs + query = _gen_query(outputStereo=True) + input_wave = np.array([1.0, 0.0, 2.0]) + + # Expects - Stereo :: (Time, Channel) + true_wave = np.array([[1.0, 1.0], [0.0, 0.0], [2.0, 2.0]]) + + # Outputs + wave = apply_output_stereo(input_wave, query) + + assert np.array_equal(wave, true_wave) + + +def test_count_frame_per_unit(): + """Test `count_frame_per_unit`.""" + # Inputs + moras = [ + _gen_mora(" ", None, None, " ", 2 * 0.01067, 0.0), # 0.01067 [sec/frame] + _gen_mora("コ", "k", 2 * 0.01067, "o", 4 * 0.01067, 0.0), + _gen_mora("ン", None, None, "N", 4 * 0.01067, 0.0), + _gen_mora("、", None, None, "pau", 2 * 0.01067, 0.0), + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 0.0), + _gen_mora("ホ", "h", 4 * 0.01067, "O", 2 * 0.01067, 0.0), + _gen_mora(" ", None, None, " ", 6 * 0.01067, 0.0), + ] + + # Expects + # Pre k o N pau h i h O Pst + true_frame_per_phoneme_list = [2, 2, 4, 4, 2, 2, 4, 4, 2, 6] + true_frame_per_phoneme = np.array(true_frame_per_phoneme_list, dtype=np.int32) + # Pre ko N pau hi hO Pst + true_frame_per_mora_list = [2, 6, 4, 2, 6, 6, 6] + true_frame_per_mora = np.array(true_frame_per_mora_list, dtype=np.int32) + + # Outputs + frame_per_phoneme, frame_per_mora = count_frame_per_unit(moras) + + assert np.array_equal(frame_per_phoneme, true_frame_per_phoneme) + assert np.array_equal(frame_per_mora, true_frame_per_mora) + + +def test_query_to_decoder_feature(): + """Test `query_to_decoder_feature`.""" + # Inputs + accent_phrases = [ + AccentPhrase( + moras=[ + _gen_mora("コ", "k", 2 * 0.01067, "o", 4 * 0.01067, 50.0), + _gen_mora("ン", None, None, "N", 4 * 0.01067, 50.0), + ], + accent=1, + pause_mora=_gen_mora("、", None, None, "pau", 2 * 0.01067, 0.0), + ), + AccentPhrase( + moras=[ + _gen_mora("ヒ", "h", 2 * 0.01067, "i", 4 * 0.01067, 125.0), + _gen_mora("ホ", "h", 4 * 0.01067, "O", 2 * 0.01067, 0.0), + ], + accent=1, + pause_mora=None, + ), + ] + query = _gen_query( + accent_phrases=accent_phrases, + speedScale=2.0, + pitchScale=2.0, + intonationScale=0.5, + prePhonemeLength=2 * 0.01067, + postPhonemeLength=6 * 0.01067, + ) + + # Expects + # frame_per_phoneme + # Pre k o N pau h i h O Pst + true_frame_per_phoneme = [1, 1, 2, 2, 1, 1, 2, 2, 1, 3] + n_frame = sum(true_frame_per_phoneme) + # phoneme + # Pr k o o N N pau h i i h h O Pt Pt Pt + frame_phoneme_idxs = [0, 23, 30, 30, 4, 4, 0, 19, 21, 21, 19, 19, 5, 0, 0, 0] + true_phoneme = np.zeros([n_frame, TRUE_NUM_PHONEME], dtype=np.float32) + for frame_idx, phoneme_idx in enumerate(frame_phoneme_idxs): + true_phoneme[frame_idx, phoneme_idx] = 1.0 + # Pitch + # paw ko N pau hi hO paw + # frame_per_vowel = [1, 3, 2, 1, 3, 3, 3] + # pau ko ko ko N N + true1_f0 = [0.0, 250.0, 250.0, 250.0, 250.0, 250.0] + # pau hi hi hi + true2_f0 = [0.0, 400.0, 400.0, 400.0] + # hO hO hO paw paw paw + true3_f0 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + true_f0 = np.array(true1_f0 + true2_f0 + true3_f0, dtype=np.float32) + + # Outputs + phoneme, f0 = query_to_decoder_feature(query) + + assert np.array_equal(phoneme, true_phoneme) + assert np.array_equal(f0, true_f0) + + +def test_raw_wave_to_output_wave_with_resample(): + """Test `raw_wave_to_output_wave` with resampling option.""" + # Inputs + query = _gen_query(volumeScale=2, outputSamplingRate=48000, outputStereo=True) + raw_wave = np.random.rand(240).astype(np.float32) + sr_raw_wave = 24000 + + # Expects + true_wave_shape = (480, 2) + + # Outputs + wave = raw_wave_to_output_wave(query, raw_wave, sr_raw_wave) + + assert wave.shape == true_wave_shape + + +def test_raw_wave_to_output_wave_without_resample(): + """Test `raw_wave_to_output_wave` without resampling option.""" + # Inputs + query = _gen_query(volumeScale=2, outputStereo=True) + raw_wave = np.random.rand(240).astype(np.float32) + sr_raw_wave = 24000 + + # Expects + true_wave = np.array([2 * raw_wave, 2 * raw_wave]).T + + # Outputs + wave = raw_wave_to_output_wave(query, raw_wave, sr_raw_wave) + + assert np.allclose(wave, true_wave) diff --git a/test/test_user_dict.py b/test/user_dict/test_user_dict.py similarity index 97% rename from test/test_user_dict.py rename to test/user_dict/test_user_dict.py index 4280bbe53..f284b74e3 100644 --- a/test/test_user_dict.py +++ b/test/user_dict/test_user_dict.py @@ -2,17 +2,19 @@ from copy import deepcopy from pathlib import Path from tempfile import TemporaryDirectory -from typing import Dict from unittest import TestCase from fastapi import HTTPException from pyopenjtalk import g2p, unset_user_dict from voicevox_engine.model import UserDictWord, WordTypes -from voicevox_engine.part_of_speech_data import MAX_PRIORITY, part_of_speech_data -from voicevox_engine.user_dict import ( +from voicevox_engine.user_dict.part_of_speech_data import ( + MAX_PRIORITY, + part_of_speech_data, +) +from voicevox_engine.user_dict.user_dict import ( + _create_word, apply_word, - create_word, delete_word, import_user_dict, read_dict, @@ -61,7 +63,7 @@ ) -def get_new_word(user_dict: Dict[str, UserDictWord]): +def get_new_word(user_dict: dict[str, UserDictWord]) -> UserDictWord: assert len(user_dict) == 2 or ( len(user_dict) == 1 and "aab7dda2-0d97-43c8-8cb7-3f440dab9b4e" not in user_dict ) @@ -90,7 +92,7 @@ def test_read_not_exist_json(self): def test_create_word(self): # 将来的に品詞などが追加された時にテストを増やす self.assertEqual( - create_word(surface="test", pronunciation="テスト", accent_type=1), + _create_word(surface="test", pronunciation="テスト", accent_type=1), UserDictWord( surface="test", priority=5, @@ -219,7 +221,7 @@ def test_priority(self): for pos in part_of_speech_data: for i in range(MAX_PRIORITY + 1): self.assertEqual( - create_word( + _create_word( surface="test", pronunciation="テスト", accent_type=1, diff --git a/test/test_user_dict_model.py b/test/user_dict/test_user_dict_model.py similarity index 88% rename from test/test_user_dict_model.py rename to test/user_dict/test_user_dict_model.py index 9a3a49021..159909233 100644 --- a/test/test_user_dict_model.py +++ b/test/user_dict/test_user_dict_model.py @@ -1,15 +1,32 @@ from copy import deepcopy +from typing import TypedDict from unittest import TestCase from pydantic import ValidationError -from voicevox_engine.kana_parser import parse_kana from voicevox_engine.model import UserDictWord +from voicevox_engine.tts_pipeline.kana_converter import parse_kana + + +class TestModel(TypedDict): + surface: str + priority: int + part_of_speech: str + part_of_speech_detail_1: str + part_of_speech_detail_2: str + part_of_speech_detail_3: str + inflectional_type: str + inflectional_form: str + stem: str + yomi: str + pronunciation: str + accent_type: int + accent_associative_rule: str class TestUserDictWords(TestCase): def setUp(self): - self.test_model = { + self.test_model: TestModel = { "surface": "テスト", "priority": 0, "part_of_speech": "名詞", diff --git a/test/test_word_types.py b/test/user_dict/test_word_types.py similarity index 73% rename from test/test_word_types.py rename to test/user_dict/test_word_types.py index 1f2635b68..e26ce192f 100644 --- a/test/test_word_types.py +++ b/test/user_dict/test_word_types.py @@ -1,7 +1,7 @@ from unittest import TestCase from voicevox_engine.model import WordTypes -from voicevox_engine.part_of_speech_data import part_of_speech_data +from voicevox_engine.user_dict.part_of_speech_data import part_of_speech_data class TestWordTypes(TestCase): diff --git a/test/utility.py b/test/utility.py new file mode 100644 index 000000000..b11e8ded4 --- /dev/null +++ b/test/utility.py @@ -0,0 +1,38 @@ +import hashlib +import json +from typing import Any + +from pydantic.json import pydantic_encoder + + +def round_floats(value: Any, round_value: int) -> Any: + """floatの小数点以下を再帰的に丸める""" + if isinstance(value, float): + return round(value, round_value) + elif isinstance(value, list): + return [round_floats(v, round_value) for v in value] + elif isinstance(value, dict): + return {k: round_floats(v, round_value) for k, v in value.items()} + else: + return value + + +def pydantic_to_native_type(value: Any) -> Any: + """pydanticの型をnativeな型に変換する""" + return json.loads(json.dumps(value, default=pydantic_encoder)) + + +def hash_long_string(value: Any) -> Any: + """文字数が1000文字を超えるものはハッシュ化する""" + + def to_hash(value: str) -> str: + return "MD5:" + hashlib.md5(value.encode()).hexdigest() + + if isinstance(value, str): + return value if len(value) <= 1000 else to_hash(value) + elif isinstance(value, list): + return [hash_long_string(v) for v in value] + elif isinstance(value, dict): + return {k: hash_long_string(v) for k, v in value.items()} + else: + return value diff --git a/ui_template/ui.html b/ui_template/ui.html index 3a156f3f3..b65526e4c 100644 --- a/ui_template/ui.html +++ b/ui_template/ui.html @@ -1,271 +1,368 @@ - + + + + - - - VOICEVOX Engine 設定 - + + + VOICEVOX Engine 設定 + + + + + + + +
+

読み込み中です。表示には数秒かかることがあります。

+
- +