diff --git a/.github/ISSUE_TEMPLATE/bugreport.md b/.github/ISSUE_TEMPLATE/bugreport.md index b9e455e56..28337b5ae 100644 --- a/.github/ISSUE_TEMPLATE/bugreport.md +++ b/.github/ISSUE_TEMPLATE/bugreport.md @@ -1,7 +1,7 @@ --- name: Bug Report about: 不具合の報告 -labels: bug +labels: バグ --- ## 不具合の内容 diff --git a/.github/ISSUE_TEMPLATE/featurerequest.md b/.github/ISSUE_TEMPLATE/featurerequest.md index 1852ec38a..78a3af90c 100644 --- a/.github/ISSUE_TEMPLATE/featurerequest.md +++ b/.github/ISSUE_TEMPLATE/featurerequest.md @@ -1,7 +1,7 @@ --- name: Feature Request about: 機能要望・改善提案 -labels: enhancement +labels: 機能向上 --- ## 内容 diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index dfc3c49bf..856a031e1 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,7 +1,7 @@ --- name: Question about: 質問 (既存のIssueや一般事例を良く調べてからしてください) -labels: question +labels: 要議論 --- ## 質問の内容 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 308f30598..4f1d88638 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -7,15 +7,19 @@ on: types: - created workflow_dispatch: + inputs: + version: + description: "バージョン情報(A.BB.C / A.BB.C-preview.D)" + required: true env: IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/voicevox_engine PYTHON_VERSION: "3.8.10" - VOICEVOX_RESOURCE_VERSION: "0.13.0-preview.3" - VOICEVOX_CORE_VERSION: "0.12.5" + VOICEVOX_RESOURCE_VERSION: "0.13.2" + VOICEVOX_CORE_VERSION: "0.13.2" VOICEVOX_ENGINE_VERSION: - |- # releaseのときはタグが、それ以外はlatestがバージョン名に - ${{ github.event.release.tag_name != '' && github.event.release.tag_name || 'latest' }} + |- # releaseタグ名か、workflow_dispatchでのバージョン名か、latestが入る + ${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }} jobs: build-docker: @@ -79,21 +83,21 @@ jobs: onnxruntime_url: https://github.com/microsoft/onnxruntime/releases/download/v1.10.0/onnxruntime-linux-x64-gpu-1.10.0.tgz steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} # Download VOICEVOX RESOURCE - name: Prepare VOICEVOX RESOURCE cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: voicevox-resource-cache with: key: voicevox-resource-${{ env.VOICEVOX_RESOURCE_VERSION }} @@ -101,7 +105,7 @@ jobs: - name: Checkout VOICEVOX RESOURCE if: steps.voicevox-resource-cache.outputs.cache-hit != 'true' - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: VOICEVOX/voicevox_resource ref: ${{ env.VOICEVOX_RESOURCE_VERSION }} @@ -115,7 +119,7 @@ jobs: run: bash build_util/merge_voicevox_resource.bash - name: Build and Deploy Docker image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 env: IMAGE_TAG: |- # If it's a release, add the version, otherwise add the `latest` diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b017d8fa..a18805f73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,15 +22,14 @@ on: env: IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/voicevox_engine PYTHON_VERSION: "3.8.10" - VOICEVOX_RESOURCE_VERSION: "0.13.0-preview.3" - VOICEVOX_CORE_VERSION: "0.12.5" + VOICEVOX_RESOURCE_VERSION: "0.13.2" + VOICEVOX_CORE_VERSION: "0.13.2" VOICEVOX_ENGINE_VERSION: |- # releaseタグ名か、workflow_dispatchでのバージョン名か、latestが入る ${{ github.event.release.tag_name || github.event.inputs.version || 'latest' }} jobs: - build-all: - environment: ${{ github.event.inputs.code_signing == 'true' && 'code_signing' }} # コード署名用のenvironment + environment: ${{ github.event.inputs.code_signing == 'true' && 'code_signing' || '' }} # コード署名用のenvironment strategy: matrix: include: @@ -79,7 +78,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Show disk space (debug info) shell: bash @@ -104,7 +103,7 @@ jobs: # Download CUDA - name: Prepare CUDA DLL cache if: matrix.cuda_version != '' - uses: actions/cache@v2 + uses: actions/cache@v3 id: cuda-dll-cache with: # update this key when ONNX Runtime CUDA dependency changed @@ -161,7 +160,7 @@ jobs: - name: Prepare cuDNN cache if: matrix.cudnn_url != '' - uses: actions/cache@v2 + uses: actions/cache@v3 id: cudnn-dll-cache with: # update this key when ONNX Runtime cuDNN dependency changed @@ -253,7 +252,7 @@ jobs: - name: Cache DirectML if: endswith(matrix.artifact_name, '-directml') - uses: actions/cache@v2 + uses: actions/cache@v3 id: directml-cache with: key: directml-cache-v1-${{ hashFiles('download/directml_url.txt') }} @@ -278,7 +277,7 @@ jobs: run: echo "${{ matrix.onnxruntime_url }}" > download/onnxruntime_url.txt - name: Prepare ONNX Runtime cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: onnxruntime-cache with: key: ${{ matrix.os }}-onnxruntime-${{ hashFiles('download/onnxruntime_url.txt') }}-v1 @@ -319,7 +318,7 @@ jobs: # Download VOICEVOX RESOURCE - name: Prepare VOICEVOX RESOURCE cache - uses: actions/cache@v2 + uses: actions/cache@v3 id: voicevox-resource-cache with: key: voicevox-resource-${{ env.VOICEVOX_RESOURCE_VERSION }} @@ -327,7 +326,7 @@ jobs: - name: Checkout VOICEVOX RESOURCE if: steps.voicevox-resource-cache.outputs.cache-hit != 'true' - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: VOICEVOX/voicevox_resource ref: ${{ env.VOICEVOX_RESOURCE_VERSION }} @@ -490,7 +489,7 @@ jobs: # https://github.com/actions/toolkit/blob/ea81280a4d48fb0308d40f8f12ae00d117f8acb9/packages/artifact/src/internal/artifact-client.ts#L147 # https://github.com/dawidd6/action-download-artifact/blob/af92a8455a59214b7b932932f2662fdefbd78126/main.js#L113 - name: Upload artifact - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 # env: # VERSIONED_ARTIFACT_NAME: | # ${{ format('{0}-{1}', matrix.artifact_name, (env.VOICEVOX_ENGINE_VERSION != 'latest' && env.VOICEVOX_ENGINE_VERSION) || github.sha) }} @@ -512,7 +511,7 @@ jobs: - windows-directml - windows-nvidia steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install dependencies run: | @@ -521,7 +520,7 @@ jobs: p7zip-full - name: Download and extract artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: ${{ matrix.artifact_name }} path: ${{ matrix.artifact_name }}/ @@ -536,13 +535,13 @@ jobs: mv archives.txt "${{ matrix.artifact_name }}.7z.txt" - name: Upload splitted archives to Release assets - uses: svenstaro/upload-release-action@v2 + uses: softprops/action-gh-release@v1 with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - tag: ${{ env.VOICEVOX_ENGINE_VERSION }} - prelease: ${{ github.event.inputs.prerelease }} - file_glob: true - file: ${{ matrix.artifact_name }}.7z.* + prerelease: ${{ github.event.inputs.prerelease }} + tag_name: ${{ env.VOICEVOX_ENGINE_VERSION }} + files: |- + ${{ matrix.artifact_name }}.7z.* + target_commitish: ${{ github.sha }} run-release-test-workflow: if: (github.event.release.tag_name || github.event.inputs.version) != '' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 228cf95c3..946b74aea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,10 +23,10 @@ jobs: path: ~\AppData\Local\pip\Cache steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} cache: pip @@ -61,7 +61,7 @@ jobs: - name: Upload coverage result if: github.event_name == 'pull_request' && matrix.os == 'ubuntu-latest' - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: report path: report/ diff --git a/Dockerfile b/Dockerfile index d2f8e3522..eaa35ca6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ EOF # assert VOICEVOX_CORE_VERSION >= 0.11.0 (ONNX) ARG VOICEVOX_CORE_ASSET_PREFIX=voicevox_core-linux-x64-cpu -ARG VOICEVOX_CORE_VERSION=0.12.5 +ARG VOICEVOX_CORE_VERSION=0.13.2 RUN <=22 の場合 pip-tools がエラーになります +pip-compile requirements.in # こちらを更新する場合は下2つも更新する必要があります。 pip-compile requirements-dev.in pip-compile requirements-test.in ``` diff --git a/default.csv b/default.csv index 65c1b06e8..9bf96b8bd 100644 --- a/default.csv +++ b/default.csv @@ -7,6 +7,7 @@ キョウコ,1351,1351,0,名詞,固有名詞,人名,名,*,*,*,キョオコ,キョオコ,1/3,C1 玄野,1350,1350,5000,名詞,固有名詞,人名,姓,*,*,*,クロノ,クロノ,1/3,C1 剣崎,1350,1350,5000,名詞,固有名詞,人名,姓,*,*,*,ケンザキ,ケンザキ,1/4,C1 +後鬼,1351,1351,0,名詞,固有名詞,人名,名,*,*,*,ゴキ,ゴキ,1/2,C1 虎太郎,1351,1351,5000,名詞,固有名詞,人名,名,*,*,*,コタロウ,コタロー,4/4,C1 琴葉,1350,1350,0,名詞,固有名詞,人名,姓,*,*,*,コトノハ,コトノハ,0/4,C1 四国,1350,1350,2200,名詞,固有名詞,人名,姓,*,*,*,シコク,シコク,1/3,C1 @@ -17,9 +18,11 @@ 武宏,1351,1351,5000,名詞,固有名詞,人名,名,*,*,*,タケヒロ,タケヒロ,2/4,C1 月読,1350,1350,0,名詞,固有名詞,人名,姓,*,*,*,ツクヨミ,ツクヨミ,0/4,C1 つむぎ,1351,1351,7450,名詞,固有名詞,人名,名,*,*,*,ツムギ,ツムギ,0/3,C1 +No.7,1351,1351,0,名詞,固有名詞,人名,名,*,*,*,ナンバーセブン,ナンバーセブン,5/7,C1 はう,1351,1351,5000,名詞,固有名詞,人名,名,*,*,*,ハウ,ハウ,1/2,C1 桜乃,1350,1350,0,名詞,固有名詞,人名,姓,*,*,*,ハルノ,ハルノ,1/3,C1 ひまり,1351,1351,7000,名詞,固有名詞,人名,名,*,*,*,ヒマリ,ヒマリ,0/3,C1 +WhiteCUL,1351,1351,0,名詞,固有名詞,人名,名,*,*,*,ホワイトカル,ホワイトカル,5/6,C1 水奈瀬,1350,1350,0,名詞,固有名詞,人名,姓,*,*,*,ミナセ,ミナセ,2/3,C1 冥鳴,1350,1350,5000,名詞,固有名詞,人名,姓,*,*,*,メイメイ,メイメイ,1/4,C1 鳴花,1350,1350,0,名詞,固有名詞,人名,姓,*,*,*,メイカ,メイカ,1/3,C1 diff --git a/engine_manifest.json b/engine_manifest.json index 057bf2737..efaebd4ad 100644 --- a/engine_manifest.json +++ b/engine_manifest.json @@ -1,5 +1,5 @@ { - "manifest_version": "0.13.0", + "manifest_version": "0.13.1", "name": "DUMMY VOICEVOX ENGINE", "uuid": "c7b58856-bd56-4aa1-afb7-b8415f824b06", "version": "999.999.999", @@ -12,5 +12,42 @@ "update_infos": "engine_manifest_assets/update_infos.json", "dependency_licenses": "engine_manifest_assets/dependency_licenses.json", "downloadable_libraries_path": null, - "downloadable_libraries_url": null + "downloadable_libraries_url": null, + "supported_features": { + "adjust_mora_pitch": { + "type": "bool", + "value": true, + "name": "モーラごとの音高の調整" + }, + "adjust_phoneme_length": { + "type": "bool", + "value": true, + "name": "音素ごとの長さの調整" + }, + "adjust_speed_scale": { + "type": "bool", + "value": true, + "name": "全体の話速の調整" + }, + "adjust_pitch_scale": { + "type": "bool", + "value": true, + "name": "全体の音高の調整" + }, + "adjust_intonation_scale": { + "type": "bool", + "value": true, + "name": "全体の抑揚の調整" + }, + "adjust_volume_scale": { + "type": "bool", + "value": true, + "name": "全体の音量の調整" + }, + "interrogative_upspeak": { + "type": "bool", + "value": true, + "name": "疑問文の自動調整" + } + } } diff --git a/requirements-dev.txt b/requirements-dev.txt index c39311e01..ed1a2c562 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -86,7 +86,7 @@ pyinstaller==5.3 # via -r requirements-dev.in pyinstaller-hooks-contrib==2022.8 # via pyinstaller -pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@50b0296a9e1b666e5a09a41ec9e9284a2a9b608f +pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@f4ade29ef9a4f43d8605103cb5bacc29e0b2ccae # via -r requirements.in python-multipart==0.0.5 # via -r requirements.in diff --git a/requirements-test.txt b/requirements-test.txt index b8722a5b3..7a0e44f31 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -104,7 +104,7 @@ pydantic==1.8.2 # via fastapi pyflakes==2.3.1 # via flake8 -pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@50b0296a9e1b666e5a09a41ec9e9284a2a9b608f +pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@f4ade29ef9a4f43d8605103cb5bacc29e0b2ccae # via -r requirements.in pyparsing==3.0.1 # via packaging @@ -121,7 +121,9 @@ pyyaml==6.0 regex==2021.10.23 # via black requests==2.26.0 - # via coveralls + # via + # -r requirements.in + # coveralls scipy==1.7.1 # via -r requirements.in six==1.16.0 diff --git a/requirements.in b/requirements.in index 6f447b77f..1986f8e8b 100644 --- a/requirements.in +++ b/requirements.in @@ -9,4 +9,4 @@ PyYAML pyworld appdirs requests -git+https://github.com/VOICEVOX/pyopenjtalk@50b0296a9e1b666e5a09a41ec9e9284a2a9b608f#egg=pyopenjtalk +git+https://github.com/VOICEVOX/pyopenjtalk@f4ade29ef9a4f43d8605103cb5bacc29e0b2ccae#egg=pyopenjtalk diff --git a/requirements.txt b/requirements.txt index e5c5ffb0d..1c3a0d040 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,9 @@ fastapi==0.70.0 h11==0.12.0 # via uvicorn idna==3.3 - # via anyio + # via + # anyio + # requests numpy==1.20.0 # via # -r requirements.in @@ -44,7 +46,7 @@ pycparser==2.20 # via cffi pydantic==1.8.2 # via fastapi -pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@50b0296a9e1b666e5a09a41ec9e9284a2a9b608f +pyopenjtalk @ git+https://github.com/VOICEVOX/pyopenjtalk@f4ade29ef9a4f43d8605103cb5bacc29e0b2ccae # via -r requirements.in python-multipart==0.0.5 # via -r requirements.in diff --git a/run.py b/run.py index 3d2c98a26..2da9df0da 100644 --- a/run.py +++ b/run.py @@ -5,12 +5,13 @@ import json import multiprocessing import os +import sys import traceback - -# import sys import zipfile from distutils.version import LooseVersion +from enum import Enum from functools import lru_cache +from io import TextIOWrapper from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryFile from typing import Dict, List, Optional @@ -22,6 +23,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.params import Query from pydantic import ValidationError, conint +from starlette.background import BackgroundTask from starlette.responses import FileResponse from voicevox_engine import __version__ @@ -54,23 +56,56 @@ import_user_dict, read_dict, rewrite_word, - user_dict_startup_processing, + update_dict, ) from voicevox_engine.utility import ( ConnectBase64WavesException, connect_base64_waves, + delete_file, engine_root, ) +class CorsPolicyMode(str, Enum): + all = "all" + localapps = "localapps" + + def b64encode_str(s): return base64.b64encode(s).decode("utf-8") +def set_output_log_utf8() -> None: + """ + stdout/stderrのエンコーディングをUTF-8に切り替える関数 + """ + # コンソールがない環境だとNone https://docs.python.org/ja/3/library/sys.html#sys.__stdin__ + if sys.stdout is not None: + # 必ずしもreconfigure()が実装されているとは限らない + try: + sys.stdout.reconfigure(encoding="utf-8") + except AttributeError: + # バッファを全て出力する + sys.stdout.flush() + sys.stdout = TextIOWrapper( + sys.stdout.buffer, encoding="utf-8", errors="backslashreplace" + ) + if sys.stderr is not None: + try: + sys.stderr.reconfigure(encoding="utf-8") + except AttributeError: + sys.stderr.flush() + sys.stderr = TextIOWrapper( + sys.stderr.buffer, encoding="utf-8", errors="backslashreplace" + ) + + def generate_app( synthesis_engines: Dict[str, SynthesisEngineBase], latest_core_version: str, root_dir: Optional[Path] = None, + cors_policy_mode: CorsPolicyMode = CorsPolicyMode.localapps, + allow_origin: Optional[List[str]] = None, ) -> FastAPI: if root_dir is None: root_dir = engine_root() @@ -83,10 +118,24 @@ def generate_app( version=__version__, ) + # CORS設定 + allowed_origins = ["*"] + if cors_policy_mode == "localapps": + allowed_origins = ["app://."] + if allow_origin is not None: + allowed_origins += allow_origin + if "*" in allow_origin: + print( + 'WARNING: Deprecated use of argument "*" in allow_origin. ' + 'Use option "--cors_policy_mod all" instead. See "--help" for more.', + file=sys.stderr, + ) + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=allowed_origins, allow_credentials=True, + allow_origin_regex="^https?://(localhost|127\\.0\\.0\\.1)(:[0-9]+)?$", allow_methods=["*"], allow_headers=["*"], ) @@ -111,7 +160,7 @@ def generate_app( @app.on_event("startup") def apply_user_dict(): - user_dict_startup_processing() + update_dict() def get_engine(core_version: Optional[str]) -> SynthesisEngineBase: if core_version is None: @@ -308,7 +357,11 @@ def synthesis( file=f, data=wave, samplerate=query.outputSamplingRate, format="WAV" ) - return FileResponse(f.name, media_type="audio/wav") + return FileResponse( + f.name, + media_type="audio/wav", + background=BackgroundTask(delete_file, f.name), + ) @app.post( "/cancellable_synthesis", @@ -343,7 +396,11 @@ def cancellable_synthesis( if f_name == "": raise HTTPException(status_code=422, detail="不明なバージョンです") - return FileResponse(f_name, media_type="audio/wav") + return FileResponse( + f_name, + media_type="audio/wav", + background=BackgroundTask(delete_file, f_name), + ) @app.post( "/multi_synthesis", @@ -391,7 +448,11 @@ def multi_synthesis( wav_file.seek(0) zip_file.writestr(f"{str(i + 1).zfill(3)}.wav", wav_file.read()) - return FileResponse(f.name, media_type="application/zip") + return FileResponse( + f.name, + media_type="application/zip", + background=BackgroundTask(delete_file, f.name), + ) @app.post( "/synthesis_morphing", @@ -441,7 +502,11 @@ def _synthesis_morphing( format="WAV", ) - return FileResponse(f.name, media_type="audio/wav") + return FileResponse( + f.name, + media_type="audio/wav", + background=BackgroundTask(delete_file, f.name), + ) @app.post( "/connect_waves", @@ -473,7 +538,11 @@ def connect_waves(waves: List[str]): format="WAV", ) - return FileResponse(f.name, media_type="audio/wav") + return FileResponse( + f.name, + media_type="audio/wav", + background=BackgroundTask(delete_file, f.name), + ) @app.get("/presets", response_model=List[Preset], tags=["その他"]) def get_presets(): @@ -785,6 +854,16 @@ def engine_manifest(): if __name__ == "__main__": multiprocessing.freeze_support() + + output_log_utf8 = os.getenv("VV_OUTPUT_LOG_UTF8", default="") + if output_log_utf8 == "1": + 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="接続を受け付けるホストアドレスです。" @@ -833,11 +912,35 @@ def engine_manifest(): type=int, default=os.getenv("VV_CPU_NUM_THREADS") or None, help="音声合成を行うスレッド数です。指定しないと、代わりに環境変数VV_CPU_NUM_THREADSの値が使われます。" - "VV_CPU_NUM_THREADSに値がなかった、または数値でなかった場合はエラー終了します。", + "VV_CPU_NUM_THREADSが空文字列でなく数値でもない場合はエラー終了します。", + ) + + parser.add_argument( + "--output_log_utf8", + action="store_true", + help="指定するとログ出力をUTF-8でおこないます。指定しないと、代わりに環境変数 VV_OUTPUT_LOG_UTF8 の値が使われます。" + "VV_OUTPUT_LOG_UTF8 の値が1の場合はUTF-8で、0または空文字、値がない場合は環境によって自動的に決定されます。", + ) + + parser.add_argument( + "--cors_policy_mode", + type=CorsPolicyMode, + choices=list(CorsPolicyMode), + default=CorsPolicyMode.localapps, + help="allまたはlocalappsを指定。allはすべてを許可します。" + "localappsはオリジン間リソース共有ポリシーを、app://.とlocalhost関連に限定します。" + "その他のオリジンはallow_originオプションで追加できます。デフォルトはlocalapps。", + ) + + parser.add_argument( + "--allow_origin", nargs="*", help="許可するオリジンを指定します。複数指定する場合は、直後にスペースで区切って追加できます。" ) args = parser.parse_args() + if args.output_log_utf8: + set_output_log_utf8() + cpu_num_threads: Optional[int] = args.cpu_num_threads synthesis_engines = make_synthesis_engines( @@ -858,7 +961,13 @@ def engine_manifest(): root_dir = args.voicevox_dir if args.voicevox_dir is not None else engine_root() uvicorn.run( - generate_app(synthesis_engines, latest_core_version, root_dir=root_dir), + generate_app( + synthesis_engines, + latest_core_version, + root_dir=root_dir, + cors_policy_mode=args.cors_policy_mode, + allow_origin=args.allow_origin, + ), host=args.host, port=args.port, ) diff --git a/test/test_connect_base64_waves.py b/test/test_connect_base64_waves.py index e1d42ad3f..e50c8f517 100644 --- a/test/test_connect_base64_waves.py +++ b/test/test_connect_base64_waves.py @@ -3,7 +3,9 @@ from unittest import TestCase import numpy as np +import numpy.testing import soundfile +from scipy.signal import resample from voicevox_engine.utility import ConnectBase64WavesException, connect_base64_waves @@ -94,19 +96,35 @@ def test_invalid_wave_file_error(self): ], ) - def test_different_frequency_error(self): - wave_24000hz = generate_sine_wave_base64( + def test_different_frequency(self): + wave_24000hz = generate_sine_wave_ndarray( seconds=1, samplerate=24000, frequency=10 ) - wave_1000hz = generate_sine_wave_base64( + wave_1000hz = generate_sine_wave_ndarray( seconds=2, samplerate=1000, frequency=10 ) + wave_24000_base64 = encode_base64(encode_bytes(wave_24000hz, samplerate=24000)) + wave_1000_base64 = encode_base64(encode_bytes(wave_1000hz, samplerate=1000)) - self.assertRaises( - ConnectBase64WavesException, - connect_base64_waves, - waves=[ - wave_24000hz, - wave_1000hz, - ], + wave_1000hz_to2400hz = resample(wave_1000hz, 24000 * len(wave_1000hz) // 1000) + wave_x2_ref = np.concatenate([wave_24000hz, wave_1000hz_to2400hz]) + + wave_x2, _ = connect_base64_waves(waves=[wave_24000_base64, wave_1000_base64]) + + self.assertEqual(wave_x2_ref.shape, wave_x2.shape) + numpy.testing.assert_array_almost_equal(wave_x2_ref, wave_x2) + + def test_different_channels(self): + wave_1000hz = generate_sine_wave_ndarray( + seconds=2, samplerate=1000, frequency=10 ) + wave_2ch_1000hz = np.array([wave_1000hz, wave_1000hz]).T + wave_1ch_base64 = encode_base64(encode_bytes(wave_1000hz, samplerate=1000)) + wave_2ch_base64 = encode_base64(encode_bytes(wave_2ch_1000hz, samplerate=1000)) + + wave_x2_ref = np.concatenate([wave_2ch_1000hz, wave_2ch_1000hz]) + + wave_x2, _ = connect_base64_waves(waves=[wave_1ch_base64, wave_2ch_base64]) + + self.assertEqual(wave_x2_ref.shape, wave_x2.shape) + self.assertTrue((wave_x2_ref == wave_x2).all()) diff --git a/test/test_user_dict.py b/test/test_user_dict.py index 250d65fa5..4280bbe53 100644 --- a/test/test_user_dict.py +++ b/test/test_user_dict.py @@ -6,7 +6,7 @@ from unittest import TestCase from fastapi import HTTPException -from pyopenjtalk import unset_user_dict +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 @@ -17,6 +17,7 @@ import_user_dict, read_dict, rewrite_word, + update_dict, ) # jsonとして保存される正しい形式の辞書データ @@ -315,3 +316,33 @@ def test_import_invalid_word(self): user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path, ) + + def test_update_dict(self): + user_dict_path = self.tmp_dir_path / "test_update_dict.json" + compiled_dict_path = self.tmp_dir_path / "test_update_dict.dic" + update_dict( + user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path + ) + test_text = "テスト用の文字列" + success_pronunciation = "デフォルトノジショデハゼッタイニセイセイサレナイヨミ" + + # 既に辞書に登録されていないか確認する + self.assertNotEqual(g2p(text=test_text, kana=True), success_pronunciation) + + apply_word( + surface=test_text, + pronunciation=success_pronunciation, + accent_type=1, + priority=10, + user_dict_path=user_dict_path, + compiled_dict_path=compiled_dict_path, + ) + self.assertEqual(g2p(text=test_text, kana=True), success_pronunciation) + + # 疑似的にエンジンを再起動する + unset_user_dict() + update_dict( + user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path + ) + + self.assertEqual(g2p(text=test_text, kana=True), success_pronunciation) diff --git a/voicevox_engine/engine_manifest/EngineManifest.py b/voicevox_engine/engine_manifest/EngineManifest.py index d6356c7f1..d49a052e8 100644 --- a/voicevox_engine/engine_manifest/EngineManifest.py +++ b/voicevox_engine/engine_manifest/EngineManifest.py @@ -24,6 +24,20 @@ class LicenseInfo(BaseModel): text: str = Field(title="依存ライブラリのライセンス本文") +class SupportedFeatures(BaseModel): + """ + エンジンが持つ機能の一覧 + """ + + adjust_mora_pitch: bool = Field(title="モーラごとの音高の調整") + adjust_phoneme_length: bool = Field(title="音素ごとの長さの調整") + adjust_speed_scale: bool = Field(title="全体の話速の調整") + adjust_pitch_scale: bool = Field(title="全体の音高の調整") + adjust_intonation_scale: bool = Field(title="全体の抑揚の調整") + adjust_volume_scale: bool = Field(title="全体の音量の調整") + interrogative_upspeak: bool = Field(title="疑問文の自動調整") + + class EngineManifest(BaseModel): """ エンジン自体に関する情報 @@ -44,3 +58,4 @@ class EngineManifest(BaseModel): downloadable_libraries_url: Optional[str] = Field( title="ダウンロード可能な音声ライブラリ情報を取得するためのAPIのURL" ) + supported_features: SupportedFeatures = Field(title="エンジンが持つ機能") diff --git a/voicevox_engine/engine_manifest/EngineManifestLoader.py b/voicevox_engine/engine_manifest/EngineManifestLoader.py index c423a4b5c..c4b8f409f 100644 --- a/voicevox_engine/engine_manifest/EngineManifestLoader.py +++ b/voicevox_engine/engine_manifest/EngineManifestLoader.py @@ -39,5 +39,9 @@ def load_manifest(self) -> EngineManifest: ], downloadable_libraries_path=manifest["downloadable_libraries_path"], downloadable_libraries_url=manifest["downloadable_libraries_url"], + supported_features={ + key: item["value"] + for key, item in manifest["supported_features"].items() + }, ) return manifest diff --git a/voicevox_engine/synthesis_engine/core_wrapper.py b/voicevox_engine/synthesis_engine/core_wrapper.py index 3e1895fa5..9fa0ab3c4 100644 --- a/voicevox_engine/synthesis_engine/core_wrapper.py +++ b/voicevox_engine/synthesis_engine/core_wrapper.py @@ -197,17 +197,21 @@ class CoreInfo: ] -# version 0.12 以降のコアの名前 +# version 0.12 以降のコアの名前の辞書 +# - version 0.12, 0.13 のコアの名前: core +# - version 0.14 からのコアの名前: voicevox_core CORENAME_DICT = { - "Windows": "core.dll", - "Linux": "libcore.so", - "Darwin": "libcore.dylib", + "Windows": ("voicevox_core.dll", "core.dll"), + "Linux": ("libvoicevox_core.so", "libcore.so"), + "Darwin": ("libvoicevox_core.dylib", "libcore.dylib"), } -def is_version_0_12_core_or_later(core_dir: Path) -> bool: +def find_version_0_12_core_or_later(core_dir: Path) -> Optional[str]: """ - core_dir で指定したディレクトリにあるコアライブラリが Version 0.12 以降であるかどうかを返す。 + core_dir で指定したディレクトリにあるコアライブラリが Version 0.12 以降である場合、 + 見つかった共有ライブラリの名前を返す。 + Version 0.12 以降と判定する条件は、 - core_dir に metas.json が存在しない @@ -216,10 +220,14 @@ def is_version_0_12_core_or_later(core_dir: Path) -> bool: の両方が真のときである。 cf. https://github.com/VOICEVOX/voicevox_engine/issues/385 """ - return ( - not (core_dir / "metas.json").exists() - and (core_dir / CORENAME_DICT[platform.system()]).is_file() - ) + if (core_dir / "metas.json").exists(): + return None + + for core_name in CORENAME_DICT[platform.system()]: + if (core_dir / core_name).is_file(): + return core_name + + return None def get_arch_name() -> Optional[str]: @@ -294,13 +302,12 @@ def check_core_type(core_dir: Path) -> Optional[str]: def load_core(core_dir: Path, use_gpu: bool) -> CDLL: - if is_version_0_12_core_or_later(core_dir): + core_name = find_version_0_12_core_or_later(core_dir) + if core_name: try: # NOTE: CDLL クラスのコンストラクタの引数 name には文字列を渡す必要がある。 # Windows 環境では PathLike オブジェクトを引数として渡すと初期化に失敗する。 - return CDLL( - str((core_dir / CORENAME_DICT[platform.system()]).resolve(strict=True)) - ) + return CDLL(str((core_dir / core_name).resolve(strict=True))) except OSError as err: raise RuntimeError(f"コアの読み込みに失敗しました:{err}") @@ -361,7 +368,10 @@ def __init__( self.exist_load_model = False self.exist_is_model_loaded = False - if is_version_0_12_core_or_later(core_dir): + is_version_0_12_core_or_later = ( + find_version_0_12_core_or_later(core_dir) is not None + ) + if is_version_0_12_core_or_later: model_type = "onnxruntime" self.exist_load_model = True self.exist_is_model_loaded = True @@ -409,15 +419,27 @@ def __init__( cwd = os.getcwd() os.chdir(core_dir) try: - if is_version_0_12_core_or_later(core_dir): + if is_version_0_12_core_or_later: if not self.core.initialize(use_gpu, cpu_num_threads, load_all_models): - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode( + "utf-8", "backslashreplace" + ) + ) elif exist_cpu_num_threads: if not self.core.initialize(".", use_gpu, cpu_num_threads): - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode( + "utf-8", "backslashreplace" + ) + ) else: if not self.core.initialize(".", use_gpu): - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode( + "utf-8", "backslashreplace" + ) + ) finally: os.chdir(cwd) @@ -438,7 +460,9 @@ def yukarin_s_forward( output.ctypes.data_as(POINTER(c_float)), ) if not success: - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode("utf-8", "backslashreplace") + ) return output def yukarin_sa_forward( @@ -471,7 +495,9 @@ def yukarin_sa_forward( output.ctypes.data_as(POINTER(c_float)), ) if not success: - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode("utf-8", "backslashreplace") + ) return output def decode_forward( @@ -492,7 +518,9 @@ def decode_forward( output.ctypes.data_as(POINTER(c_float)), ) if not success: - raise Exception(self.core.last_error_message().decode("utf-8")) + raise Exception( + self.core.last_error_message().decode("utf-8", "backslashreplace") + ) return output def supported_devices(self) -> str: diff --git a/voicevox_engine/synthesis_engine/make_synthesis_engines.py b/voicevox_engine/synthesis_engine/make_synthesis_engines.py index 7c23172a2..425d52fc2 100644 --- a/voicevox_engine/synthesis_engine/make_synthesis_engines.py +++ b/voicevox_engine/synthesis_engine/make_synthesis_engines.py @@ -1,10 +1,9 @@ import json import sys -import traceback from pathlib import Path from typing import Dict, List, Optional -from ..utility import engine_root +from ..utility import engine_root, get_save_dir from .core_wrapper import CoreWrapper, load_runtime_lib from .synthesis_engine import SynthesisEngine, SynthesisEngineBase @@ -68,34 +67,56 @@ def make_synthesis_engines( runtime_dirs = [p.expanduser() for p in runtime_dirs] load_runtime_lib(runtime_dirs) + synthesis_engines = {} - for core_dir in voicelib_dirs: - try: - core = CoreWrapper(use_gpu, core_dir, cpu_num_threads, load_all_models) - metas = json.loads(core.metas()) - core_version = metas[0]["version"] - if core_version in synthesis_engines: - print( - "Warning: Core loading is skipped because of version duplication.", - file=sys.stderr, - ) + + if not enable_mock: + + def load_core_library(core_dir: Path, suppress_error: bool = False): + """ + 指定されたディレクトリにあるコアを読み込む。 + ユーザーディレクトリの場合は存在しないこともあるので、エラーを抑制すると良い。 + """ + try: + core = CoreWrapper(use_gpu, core_dir, cpu_num_threads, load_all_models) + metas = json.loads(core.metas()) + core_version = metas[0]["version"] + if core_version in synthesis_engines: + print( + "Warning: Core loading is skipped because of version duplication.", + file=sys.stderr, + ) + else: + synthesis_engines[core_version] = SynthesisEngine(core=core) + except Exception: + if not suppress_error: + raise + + for core_dir in voicelib_dirs: + load_core_library(core_dir) + + # ユーザーディレクトリにあるコアを読み込む + user_voicelib_dirs = [] + core_libraries_dir = get_save_dir() / "core_libraries" + core_libraries_dir.mkdir(exist_ok=True) + user_voicelib_dirs.append(core_libraries_dir) + for path in core_libraries_dir.glob("*"): + if not path.is_dir(): continue - synthesis_engines[core_version] = SynthesisEngine(core=core) - except Exception: - if not enable_mock: - raise - traceback.print_exc() - print( - "Notice: mock-library will be used. Try re-run with valid --voicevox_dir", - file=sys.stderr, + user_voicelib_dirs.append(path) + + for core_dir in user_voicelib_dirs: + load_core_library(core_dir, suppress_error=True) + + else: + # モック追加 + from ..dev.core import metas as mock_metas + from ..dev.core import supported_devices as mock_supported_devices + from ..dev.synthesis_engine import MockSynthesisEngine + + if "0.0.0" not in synthesis_engines: + synthesis_engines["0.0.0"] = MockSynthesisEngine( + speakers=mock_metas(), supported_devices=mock_supported_devices() ) - from ..dev.core import metas as mock_metas - from ..dev.core import supported_devices as mock_supported_devices - from ..dev.synthesis_engine import MockSynthesisEngine - - if "0.0.0" not in synthesis_engines: - synthesis_engines["0.0.0"] = MockSynthesisEngine( - speakers=mock_metas(), supported_devices=mock_supported_devices() - ) return synthesis_engines diff --git a/voicevox_engine/synthesis_engine/synthesis_engine_base.py b/voicevox_engine/synthesis_engine/synthesis_engine_base.py index 386b8de71..4e17669db 100644 --- a/voicevox_engine/synthesis_engine/synthesis_engine_base.py +++ b/voicevox_engine/synthesis_engine/synthesis_engine_base.py @@ -2,6 +2,8 @@ from abc import ABCMeta, abstractmethod from typing import List, Optional +import numpy as np + from .. import full_context_label from ..full_context_label import extract_full_context_label from ..model import AccentPhrase, AudioQuery, Mora @@ -208,7 +210,7 @@ def synthesis( query: AudioQuery, speaker_id: int, enable_interrogative_upspeak: bool = True, - ) -> str: + ) -> np.ndarray: """ 音声合成クエリ内の疑問文指定されたMoraを変形した後、 継承先における実装`_synthesis_impl`を使い音声合成を行う @@ -234,7 +236,7 @@ def synthesis( return self._synthesis_impl(query, speaker_id) @abstractmethod - def _synthesis_impl(self, query: AudioQuery, speaker_id: int): + def _synthesis_impl(self, query: AudioQuery, speaker_id: int) -> np.ndarray: """ 音声合成クエリから音声合成に必要な情報を構成し、実際に音声合成を行う Parameters diff --git a/voicevox_engine/user_dict.py b/voicevox_engine/user_dict.py index 940c06f15..72828a629 100644 --- a/voicevox_engine/user_dict.py +++ b/voicevox_engine/user_dict.py @@ -1,4 +1,5 @@ import json +import shutil import sys from pathlib import Path from tempfile import NamedTemporaryFile @@ -7,17 +8,15 @@ import numpy as np import pyopenjtalk -from appdirs import user_data_dir from fastapi import HTTPException from pydantic import conint from .model import UserDictWord, WordTypes from .part_of_speech_data import MAX_PRIORITY, MIN_PRIORITY, part_of_speech_data -from .utility import engine_root +from .utility import delete_file, engine_root, get_save_dir root_dir = engine_root() -# FIXME: ファイル保存場所をエンジン固有のIDが入ったものにする -save_dir = Path(user_data_dir("voicevox-engine")) +save_dir = get_save_dir() if not save_dir.is_dir(): save_dir.mkdir(parents=True) @@ -41,19 +40,9 @@ def write_to_json(user_dict: Dict[str, UserDictWord], user_dict_path: Path): user_dict_path.write_text(user_dict_json, encoding="utf-8") -def user_dict_startup_processing( - default_dict_path: Path = default_dict_path, - compiled_dict_path: Path = compiled_dict_path, -): - pyopenjtalk.create_user_dict( - str(default_dict_path.resolve(strict=True)), - str(compiled_dict_path.resolve()), - ) - pyopenjtalk.set_user_dict(str(compiled_dict_path.resolve(strict=True))) - - def update_dict( default_dict_path: Path = default_dict_path, + user_dict_path: Path = user_dict_path, compiled_dict_path: Path = compiled_dict_path, ): with NamedTemporaryFile(encoding="utf-8", mode="w", delete=False) as f: @@ -64,7 +53,7 @@ def update_dict( if default_dict == default_dict.rstrip(): default_dict += "\n" f.write(default_dict) - user_dict = read_dict() + user_dict = read_dict(user_dict_path=user_dict_path) for word_uuid in user_dict: word = user_dict[word_uuid] f.write( @@ -97,11 +86,12 @@ def update_dict( str(Path(f.name).resolve(strict=True)), str(tmp_dict_path), ) + delete_file(f.name) if not tmp_dict_path.is_file(): raise RuntimeError("辞書のコンパイル時にエラーが発生しました。") pyopenjtalk.unset_user_dict() try: - tmp_dict_path.replace(compiled_dict_path) + shutil.move(tmp_dict_path, compiled_dict_path) # ドライブを跨ぐためPath.replaceが使えない finally: if compiled_dict_path.is_file(): pyopenjtalk.set_user_dict(str(compiled_dict_path.resolve(strict=True))) @@ -181,7 +171,7 @@ def apply_word( word_uuid = str(uuid4()) user_dict[word_uuid] = word write_to_json(user_dict, user_dict_path) - update_dict(compiled_dict_path=compiled_dict_path) + update_dict(user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path) return word_uuid @@ -207,7 +197,7 @@ def rewrite_word( raise HTTPException(status_code=422, detail="UUIDに該当するワードが見つかりませんでした") user_dict[word_uuid] = word write_to_json(user_dict, user_dict_path) - update_dict(compiled_dict_path=compiled_dict_path) + update_dict(user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path) def delete_word( @@ -220,7 +210,7 @@ def delete_word( raise HTTPException(status_code=422, detail="IDに該当するワードが見つかりませんでした") del user_dict[word_uuid] write_to_json(user_dict, user_dict_path) - update_dict(compiled_dict_path=compiled_dict_path) + update_dict(user_dict_path=user_dict_path, compiled_dict_path=compiled_dict_path) def import_user_dict( @@ -259,7 +249,9 @@ def import_user_dict( new_dict = {**dict_data, **old_dict} write_to_json(user_dict=new_dict, user_dict_path=user_dict_path) update_dict( - default_dict_path=default_dict_path, compiled_dict_path=compiled_dict_path + default_dict_path=default_dict_path, + user_dict_path=user_dict_path, + compiled_dict_path=compiled_dict_path, ) diff --git a/voicevox_engine/utility/__init__.py b/voicevox_engine/utility/__init__.py index 0248a9d6a..6b715aba7 100644 --- a/voicevox_engine/utility/__init__.py +++ b/voicevox_engine/utility/__init__.py @@ -3,11 +3,13 @@ connect_base64_waves, decode_base64_waves, ) -from .engine_root import engine_root +from .path_utility import delete_file, engine_root, get_save_dir __all__ = [ "ConnectBase64WavesException", "connect_base64_waves", "decode_base64_waves", + "delete_file", "engine_root", + "get_save_dir", ] diff --git a/voicevox_engine/utility/connect_base64_waves.py b/voicevox_engine/utility/connect_base64_waves.py index b40d960a0..37f952409 100644 --- a/voicevox_engine/utility/connect_base64_waves.py +++ b/voicevox_engine/utility/connect_base64_waves.py @@ -4,6 +4,7 @@ import numpy as np import soundfile +from scipy.signal import resample class ConnectBase64WavesException(Exception): @@ -11,36 +12,49 @@ def __init__(self, message: str): self.message = message -def decode_base64_waves(waves: List[str]) -> Tuple[List[np.ndarray], float]: +def decode_base64_waves(waves: List[str]) -> List[Tuple[np.ndarray, int]]: + """ + base64エンコードされた複数のwavデータをデコードする + Parameters + ---------- + waves: list[str] + base64エンコードされたwavデータのリスト + Returns + ------- + waves_nparray_sr: List[Tuple[np.ndarray, int]] + (NumPy配列の音声波形データ, サンプリングレート) 形式のタプルのリスト + """ if len(waves) == 0: raise ConnectBase64WavesException("wavファイルが含まれていません") - waves_nparray = [] - for i in range(len(waves)): + waves_nparray_sr = [] + for wave in waves: try: - wav_bin = base64.standard_b64decode(waves[i]) + wav_bin = base64.standard_b64decode(wave) except ValueError: raise ConnectBase64WavesException("base64デコードに失敗しました") try: - _data, _sampling_rate = soundfile.read(io.BytesIO(wav_bin)) + _data = soundfile.read(io.BytesIO(wav_bin)) except Exception: raise ConnectBase64WavesException("wavファイルを読み込めませんでした") - if i == 0: - sampling_rate = _sampling_rate - channels = _data.ndim - else: - if sampling_rate != _sampling_rate: - raise ConnectBase64WavesException("ファイル間でサンプリングレートが異なります") - if channels != _data.ndim: - if channels == 1: - _data = _data.T[0] - else: - _data = np.array([_data, _data]).T - waves_nparray.append(_data) - - return waves_nparray, sampling_rate - - -def connect_base64_waves(waves: List[str]) -> Tuple[np.ndarray, float]: - waves_nparray_list, sampling_rate = decode_base64_waves(waves) - return np.concatenate(waves_nparray_list), sampling_rate + waves_nparray_sr.append(_data) + + return waves_nparray_sr + + +def connect_base64_waves(waves: List[str]) -> Tuple[np.ndarray, int]: + waves_nparray_sr = decode_base64_waves(waves) + + max_sampling_rate = max([sr for _, sr in waves_nparray_sr]) + max_channels = max([x.ndim for x, _ in waves_nparray_sr]) + assert 0 < max_channels <= 2 + + waves_nparray_list = [] + for nparray, sr in waves_nparray_sr: + if sr != max_sampling_rate: + nparray = resample(nparray, max_sampling_rate * len(nparray) // sr) + if nparray.ndim < max_channels: + nparray = np.array([nparray, nparray]).T + waves_nparray_list.append(nparray) + + return np.concatenate(waves_nparray_list), max_sampling_rate diff --git a/voicevox_engine/utility/engine_root.py b/voicevox_engine/utility/engine_root.py deleted file mode 100644 index dd911610c..000000000 --- a/voicevox_engine/utility/engine_root.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys -from pathlib import Path - - -def engine_root() -> Path: - # nuitkaビルドをした際はグローバルに__compiled__が含まれる - if "__compiled__" in globals(): - root_dir = Path(sys.argv[0]).parent - - # pyinstallerでビルドをした際はsys.frozenが設定される - elif getattr(sys, "frozen", False): - root_dir = Path(sys.argv[0]).parent - - else: - root_dir = Path(__file__).parents[2] - - return root_dir.resolve(strict=True) diff --git a/voicevox_engine/utility/path_utility.py b/voicevox_engine/utility/path_utility.py new file mode 100644 index 000000000..4de943624 --- /dev/null +++ b/voicevox_engine/utility/path_utility.py @@ -0,0 +1,51 @@ +import os +import sys +import traceback +from pathlib import Path + +from appdirs import user_data_dir + + +def engine_root() -> Path: + if is_development(): + root_dir = Path(__file__).parents[2] + + # Nuitka/Pyinstallerでビルドされている場合 + else: + root_dir = Path(sys.argv[0]).parent + + return root_dir.resolve(strict=True) + + +def is_development() -> bool: + """ + 開発版かどうか判定する関数 + Nuitka/Pyinstallerでコンパイルされていない場合は開発環境とする。 + """ + # nuitkaビルドをした際はグローバルに__compiled__が含まれる + if "__compiled__" in globals(): + return False + + # pyinstallerでビルドをした際はsys.frozenが設定される + elif getattr(sys, "frozen", False): + return False + + return True + + +def get_save_dir(): + # FIXME: ファイル保存場所をエンジン固有のIDが入ったものにする + # FIXME: Windowsは`voicevox-engine/voicevox-engine`ディレクトリに保存されているので + # `VOICEVOX/voicevox-engine`に変更する + if is_development(): + app_name = "voicevox-engine-dev" + else: + app_name = "voicevox-engine" + return Path(user_data_dir(app_name)) + + +def delete_file(file_path: str) -> None: + try: + os.remove(file_path) + except OSError: + traceback.print_exc()