diff --git a/.env.production b/.env.production index 2406326a46..2cf04cbb1d 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,2 @@ -ENGINE_PATH=run.exe -VUE_APP_ENGINE_URL=http://127.0.0.1:50021 -VUE_APP_GTM_CONTAINER_ID=GTM-DUMMY \ No newline at end of file +DEFAULT_ENGINE_INFOS=[{"key":"074fc39e-678b-4c13-8916-ffca8d505d1d","executionEnabled":true,"executionFilePath":"run.exe","host":"http://127.0.0.1:50021"}] +VUE_APP_GTM_CONTAINER_ID=GTM-DUMMY diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 840ad61474..a824c0e9eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,10 +11,11 @@ on: env: VOICEVOX_ENGINE_REPO_URL: "https://github.com/VOICEVOX/voicevox_engine" - VOICEVOX_ENGINE_VERSION: 0.10.4 - VOICEVOX_RESOURCE_VERSION: 0.10.0 - VOICEVOX_EDITOR_VERSION: |- # releaseのときはタグが、それ以外は0.0.0がバージョン名に - ${{ github.event.release.tag_name != '' && github.event.release.tag_name || '0.0.0' }} + VOICEVOX_ENGINE_VERSION: 0.11.4 + VOICEVOX_RESOURCE_VERSION: 0.11.4 + VOICEVOX_EDITOR_VERSION: + |- # releaseのときはタグが、それ以外は999.999.999がバージョン名に + ${{ github.event.release.tag_name != '' && github.event.release.tag_name || '999.999.999' }} jobs: build-noengine-prepackage: @@ -37,7 +38,7 @@ jobs: - artifact_name: linux-noengine-prepackage artifact_path: dist_electron/linux-unpacked package_name: voicevox - linux_artifact_name: 'VOICEVOX.${ext}' + linux_artifact_name: "VOICEVOX.${ext}" linux_executable_name: voicevox sed_name: sed os: ubuntu-18.04 @@ -45,7 +46,7 @@ jobs: - artifact_name: linux-noengine-cpu-prepackage artifact_path: dist_electron/linux-unpacked package_name: voicevox-cpu - linux_artifact_name: 'VOICEVOX.${ext}' + linux_artifact_name: "VOICEVOX.${ext}" linux_executable_name: voicevox sed_name: sed os: ubuntu-18.04 @@ -53,21 +54,21 @@ jobs: - artifact_name: windows-noengine-prepackage artifact_path: dist_electron/win-unpacked package_name: voicevox - nsis_web_artifact_name: 'VOICEVOX Web Setup ${version}.${ext}' + nsis_web_artifact_name: "VOICEVOX Web Setup ${version}.${ext}" sed_name: sed os: windows-2019 # Windows CPU - artifact_name: windows-noengine-cpu-prepackage artifact_path: dist_electron/win-unpacked package_name: voicevox-cpu - nsis_web_artifact_name: 'VOICEVOX-CPU Web Setup ${version}.${ext}' + nsis_web_artifact_name: "VOICEVOX-CPU Web Setup ${version}.${ext}" sed_name: sed os: windows-2019 # macOS CPU - artifact_name: macos-noengine-cpu-prepackage artifact_path: dist_electron/mac package_name: voicevox-cpu - macos_artifact_name: 'VOICEVOX.${ext}' + macos_artifact_name: "VOICEVOX.${ext}" sed_name: gsed os: macos-11 @@ -93,7 +94,7 @@ jobs: "${{ matrix.sed_name }}" -i 's/"name": "voicevox"/"name": "${{ matrix.package_name }}"/' package.json # "${{ matrix.sed_name }}" -i 's/productName: "VOICEVOX"/productName: "${{ matrix.product_name }}"/' vue.config.js - "${{ matrix.sed_name }}" -i 's/"version": "0.0.0"/"version": "${{ env.VOICEVOX_EDITOR_VERSION }}"/' package.json + "${{ matrix.sed_name }}" -i 's/"version": "999.999.999"/"version": "${{ env.VOICEVOX_EDITOR_VERSION }}"/' package.json # NOTE: The extraFiles of electron-builder uses VOICEVOX.app/Contents/ as the file copy destination. # However, since the executable file is located in VOICEVOX.app/Contents/MacOS/, @@ -156,14 +157,6 @@ jobs: run: | df -h - - name: Overwrite .env.production for Linux and macOS - if: startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') - shell: bash - run: | - echo "ENGINE_PATH=./run" > .env.production - echo "VUE_APP_ENGINE_URL=http://127.0.0.1:50021" >> .env.production - echo "VUE_APP_GTM_CONTAINER_ID=GTM-DUMMY" >> .env.production - - name: Checkout Product Version Resource uses: actions/checkout@v2 with: @@ -223,6 +216,19 @@ jobs: cp resource/editor/PRIVACYPOLICY.md public/privacyPolicy.md + - name: Overwrite .env.production for Linux and macOS + if: startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') + shell: bash + run: | + "${{ matrix.sed_name }}" -i 's|run.exe|./run|g' .env.production + + - name: Replace .env.production infomations + shell: bash + run: | + # GTM ID + gtm_id=$(jq -r '.VUE_APP_GTM_CONTAINER_ID' resource/editor/metas.json) + "${{ matrix.sed_name }}" -i 's/VUE_APP_GTM_CONTAINER_ID=.*/VUE_APP_GTM_CONTAINER_ID='"$gtm_id"'/' .env.production + - name: Generate public/licenses.json shell: bash run: npm run license:generate -- -o public/licenses.json @@ -249,7 +255,6 @@ jobs: path: | ${{ matrix.artifact_path }} - build-engine-prepackage: env: cache-version: v2 @@ -483,7 +488,6 @@ jobs: name: ${{ matrix.artifact_name }}-zip path: "${{ matrix.compressed_artifact_name }}-${{ env.VOICEVOX_EDITOR_VERSION }}.zip" - build-distributable: if: github.event.release.tag_name != '' # If release needs: [build-engine-prepackage] @@ -506,7 +510,7 @@ jobs: - artifact_name: linux-nvidia-appimage engine_artifact_name: linux-nvidia-prepackage package_name: voicevox - linux_artifact_name: 'VOICEVOX.${ext}' + linux_artifact_name: "VOICEVOX.${ext}" linux_executable_name: voicevox sed_name: sed os: ubuntu-18.04 @@ -514,7 +518,7 @@ jobs: - artifact_name: linux-cpu-appimage engine_artifact_name: linux-cpu-prepackage package_name: voicevox-cpu - linux_artifact_name: 'VOICEVOX.${ext}' + linux_artifact_name: "VOICEVOX.${ext}" linux_executable_name: voicevox sed_name: sed os: ubuntu-18.04 @@ -522,21 +526,21 @@ jobs: - artifact_name: windows-nvidia-nsis-web engine_artifact_name: windows-nvidia-prepackage package_name: voicevox - nsis_web_artifact_name: 'VOICEVOX Web Setup ${version}.${ext}' + nsis_web_artifact_name: "VOICEVOX Web Setup ${version}.${ext}" sed_name: sed os: windows-2019 # Windows CPU - artifact_name: windows-cpu-nsis-web engine_artifact_name: windows-cpu-prepackage package_name: voicevox-cpu - nsis_web_artifact_name: 'VOICEVOX-CPU Web Setup ${version}.${ext}' + nsis_web_artifact_name: "VOICEVOX-CPU Web Setup ${version}.${ext}" sed_name: sed os: windows-2019 # macOS CPU - artifact_name: macos-cpu-dmg engine_artifact_name: macos-cpu-prepackage package_name: voicevox-cpu - macos_artifact_name: 'VOICEVOX ${version}.${ext}' + macos_artifact_name: "VOICEVOX ${version}.${ext}" macos_executable_name: VOICEVOX sed_name: gsed os: macos-11 @@ -568,7 +572,7 @@ jobs: "${{ matrix.sed_name }}" -i 's/"name": "voicevox"/"name": "${{ matrix.package_name }}"/' package.json # "${{ matrix.sed_name }}" -i 's/productName: "VOICEVOX"/productName: "${{ matrix.product_name }}"/' vue.config.js - "${{ matrix.sed_name }}" -i 's/"version": "0.0.0"/"version": "${{ env.VOICEVOX_EDITOR_VERSION }}"/' package.json + "${{ matrix.sed_name }}" -i 's/"version": "999.999.999"/"version": "${{ env.VOICEVOX_EDITOR_VERSION }}"/' package.json - name: Download and extract engine-prepackage artifact uses: actions/download-artifact@v2 @@ -714,7 +718,6 @@ jobs: path: | nsis-web-artifact/* - upload-distributable-to-release: if: github.event.release.tag_name != '' # If release needs: [build-distributable] diff --git a/README.md b/README.md index 0f40a2fe1f..10f3d43474 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ npm ci ## 実行 -`.env.production`をコピーして`.env`を作成し、`ENGINE_PATH`に`voicevox_engine`があるパスを指定します。 +`.env.production`をコピーして`.env`を作成し、`DEFAULT_ENGINE_INFOS`内の`executionFilePath`に`voicevox_engine`があるパスを指定します。 とりあえず [製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) のディレクトリのパスを指定すれば動きます。 ```bash diff --git a/openapi.json b/openapi.json index 70d3b1a1e1..d112915d93 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.2","info":{"title":"VOICEVOX ENGINE","description":"VOICEVOXの音声合成エンジンです。","version":"0.10.2"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio_query_from_preset":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリをプリセットを用いて作成する","description":"クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_from_preset_audio_query_from_preset_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Preset Id","type":"integer"},"name":"preset_id","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/accent_phrases":{"post":{"tags":["クエリ編集"],"summary":"テキストからアクセント句を得る","description":"テキストからアクセント句を得ます。\nis_kanaが`true`のとき、テキストは次のようなAquesTalkライクな記法に従う読み仮名として処理されます。デフォルトは`false`です。\n* 全てのカナはカタカナで記述される\n* アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。\n* カナの手前に`_`を入れるとそのカナは無声化される\n* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。\n* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。","operationId":"accent_phrases_accent_phrases_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Is Kana","type":"boolean","default":false},"name":"is_kana","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Accent Phrases Accent Phrases Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"400":{"description":"読み仮名のパースに失敗","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_data":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高・音素長を得る","operationId":"mora_data_mora_data_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Data Mora Data Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Length Mora Length Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Pitch Mora Pitch Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"description":"疑問系のテキストが与えられたら語尾を自動調整する","required":false,"schema":{"title":"Enable Interrogative Upspeak","type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true},"name":"enable_interrogative_upspeak","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cancellable_synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する(キャンセル可能)","operationId":"cancellable_synthesis_cancellable_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/multi_synthesis":{"post":{"tags":["音声合成"],"summary":"複数まとめて音声合成する","operationId":"multi_synthesis_multi_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Queries","type":"array","items":{"$ref":"#/components/schemas/AudioQuery"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis_morphing":{"post":{"tags":["音声合成"],"summary":"2人の話者でモーフィングした音声を合成する","description":"指定された2人の話者で音声を合成、指定した割合でモーフィングした音声を得ます。\nモーフィングの割合は`morph_rate`で指定でき、0.0でベースの話者、1.0でターゲットの話者に近づきます。","operationId":"_synthesis_morphing_synthesis_morphing_post","parameters":[{"required":true,"schema":{"title":"Base Speaker","type":"integer"},"name":"base_speaker","in":"query"},{"required":true,"schema":{"title":"Target Speaker","type":"integer"},"name":"target_speaker","in":"query"},{"required":true,"schema":{"title":"Morph Rate","maximum":1.0,"minimum":0.0,"type":"number"},"name":"morph_rate","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connect_waves":{"post":{"tags":["その他"],"summary":"base64エンコードされた複数のwavデータを一つに結合する","description":"base64エンコードされたwavデータを一纏めにし、wavファイルで返します。","operationId":"connect_waves_connect_waves_post","requestBody":{"content":{"application/json":{"schema":{"title":"Waves","type":"array","items":{"type":"string"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/presets":{"get":{"tags":["その他"],"summary":"Get Presets","description":"エンジンが保持しているプリセットの設定を返します\n\nReturns\n-------\npresets: List[Preset]\n プリセットのリスト","operationId":"get_presets_presets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Get Presets Presets Get","type":"array","items":{"$ref":"#/components/schemas/Preset"}}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","operationId":"speakers_speakers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Speakers Speakers Get","type":"array","items":{"$ref":"#/components/schemas/Speaker"}}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"指定されたspeaker_uuidに関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。\n\nReturns\n-------\nret_data: SpeakerInfo","operationId":"speaker_info_speaker_info_get","parameters":[{"required":true,"schema":{"title":"Speaker Uuid","type":"string"},"name":"speaker_uuid","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"title":"AccentPhrase","required":["moras","accent"],"type":"object","properties":{"moras":{"title":"モーラのリスト","type":"array","items":{"$ref":"#/components/schemas/Mora"}},"accent":{"title":"アクセント箇所","type":"integer"},"pause_mora":{"title":"後ろに無音を付けるかどうか","allOf":[{"$ref":"#/components/schemas/Mora"}]},"is_interrogative":{"title":"疑問系かどうか","type":"boolean","default":false}},"description":"アクセント句ごとの情報"},"AudioQuery":{"title":"AudioQuery","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"type":"object","properties":{"accent_phrases":{"title":"アクセント句のリスト","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}},"speedScale":{"title":"全体の話速","type":"number"},"pitchScale":{"title":"全体の音高","type":"number"},"intonationScale":{"title":"全体の抑揚","type":"number"},"volumeScale":{"title":"全体の音量","type":"number"},"prePhonemeLength":{"title":"音声の前の無音時間","type":"number"},"postPhonemeLength":{"title":"音声の後の無音時間","type":"number"},"outputSamplingRate":{"title":"音声データの出力サンプリングレート","type":"integer"},"outputStereo":{"title":"音声データをステレオ出力するか否か","type":"boolean"},"kana":{"title":"[読み取り専用]AquesTalkライクな読み仮名。音声合成クエリとしては無視される","type":"string"}},"description":"音声合成用のクエリ"},"HTTPValidationError":{"title":"HTTPValidationError","type":"object","properties":{"detail":{"title":"Detail","type":"array","items":{"$ref":"#/components/schemas/ValidationError"}}}},"Mora":{"title":"Mora","required":["text","vowel","vowel_length","pitch"],"type":"object","properties":{"text":{"title":"文字","type":"string"},"consonant":{"title":"子音の音素","type":"string"},"consonant_length":{"title":"子音の音長","type":"number"},"vowel":{"title":"母音の音素","type":"string"},"vowel_length":{"title":"母音の音長","type":"number"},"pitch":{"title":"音高","type":"number"}},"description":"モーラ(子音+母音)ごとの情報"},"ParseKanaBadRequest":{"title":"ParseKanaBadRequest","required":["text","error_name","error_args"],"type":"object","properties":{"text":{"title":"エラーメッセージ","type":"string"},"error_name":{"title":"エラー名","type":"string","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 | 処理時に無限ループになってしまいました...バグ報告をお願いします。 |"},"error_args":{"title":"エラーを起こした箇所","type":"object","additionalProperties":{"type":"string"}}}},"Preset":{"title":"Preset","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"type":"object","properties":{"id":{"title":"プリセットID","type":"integer"},"name":{"title":"プリセット名","type":"string"},"speaker_uuid":{"title":"スピーカーのUUID","type":"string"},"style_id":{"title":"スタイルID","type":"integer"},"speedScale":{"title":"全体の話速","type":"number"},"pitchScale":{"title":"全体の音高","type":"number"},"intonationScale":{"title":"全体の抑揚","type":"number"},"volumeScale":{"title":"全体の音量","type":"number"},"prePhonemeLength":{"title":"音声の前の無音時間","type":"number"},"postPhonemeLength":{"title":"音声の後の無音時間","type":"number"}},"description":"プリセット情報"},"Speaker":{"title":"Speaker","required":["name","speaker_uuid","styles"],"type":"object","properties":{"name":{"title":"名前","type":"string"},"speaker_uuid":{"title":"スピーカーのUUID","type":"string"},"styles":{"title":"スピーカースタイルの一覧","type":"array","items":{"$ref":"#/components/schemas/SpeakerStyle"}},"version":{"title":"Version","type":"string","default":"スピーカーのバージョン"}},"description":"スピーカー情報"},"SpeakerInfo":{"title":"SpeakerInfo","required":["policy","portrait","style_infos"],"type":"object","properties":{"policy":{"title":"policy.md","type":"string"},"portrait":{"title":"portrait.pngをbase64エンコードしたもの","type":"string"},"style_infos":{"title":"スタイルの追加情報","type":"array","items":{"$ref":"#/components/schemas/StyleInfo"}}},"description":"話者の追加情報"},"SpeakerStyle":{"title":"SpeakerStyle","required":["name","id"],"type":"object","properties":{"name":{"title":"スタイル名","type":"string"},"id":{"title":"スタイルID","type":"integer"}},"description":"スピーカーのスタイル情報"},"StyleInfo":{"title":"StyleInfo","required":["id","icon","voice_samples"],"type":"object","properties":{"id":{"title":"スタイルID","type":"integer"},"icon":{"title":"当該スタイルのアイコンをbase64エンコードしたもの","type":"string"},"voice_samples":{"title":"voice_sampleのwavファイルをbase64エンコードしたもの","type":"array","items":{"type":"string"}}},"description":"スタイルの追加情報"},"ValidationError":{"title":"ValidationError","required":["loc","msg","type"],"type":"object","properties":{"loc":{"title":"Location","type":"array","items":{"type":"string"}},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.2","info":{"title":"VOICEVOX ENGINE","description":"VOICEVOXの音声合成エンジンです。","version":"0.10.4"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio_query_from_preset":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリをプリセットを用いて作成する","description":"クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_from_preset_audio_query_from_preset_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Preset Id","type":"integer"},"name":"preset_id","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/accent_phrases":{"post":{"tags":["クエリ編集"],"summary":"テキストからアクセント句を得る","description":"テキストからアクセント句を得ます。\nis_kanaが`true`のとき、テキストは次のようなAquesTalkライクな記法に従う読み仮名として処理されます。デフォルトは`false`です。\n* 全てのカナはカタカナで記述される\n* アクセント句は`/`または`、`で区切る。`、`で区切った場合に限り無音区間が挿入される。\n* カナの手前に`_`を入れるとそのカナは無声化される\n* アクセント位置を`'`で指定する。全てのアクセント句にはアクセント位置を1つ指定する必要がある。\n* アクセント句末に`?`(全角)を入れることにより疑問文の発音ができる。","operationId":"accent_phrases_accent_phrases_post","parameters":[{"required":true,"schema":{"title":"Text","type":"string"},"name":"text","in":"query"},{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Is Kana","type":"boolean","default":false},"name":"is_kana","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Accent Phrases Accent Phrases Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"400":{"description":"読み仮名のパースに失敗","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_data":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高・音素長を得る","operationId":"mora_data_mora_data_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Data Mora Data Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Length Mora Length Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Accent Phrases","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Mora Pitch Mora Pitch Post","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"description":"疑問系のテキストが与えられたら語尾を自動調整する","required":false,"schema":{"title":"Enable Interrogative Upspeak","type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true},"name":"enable_interrogative_upspeak","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/cancellable_synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する(キャンセル可能)","operationId":"cancellable_synthesis_cancellable_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/multi_synthesis":{"post":{"tags":["音声合成"],"summary":"複数まとめて音声合成する","operationId":"multi_synthesis_multi_synthesis_post","parameters":[{"required":true,"schema":{"title":"Speaker","type":"integer"},"name":"speaker","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"title":"Queries","type":"array","items":{"$ref":"#/components/schemas/AudioQuery"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis_morphing":{"post":{"tags":["音声合成"],"summary":"2人の話者でモーフィングした音声を合成する","description":"指定された2人の話者で音声を合成、指定した割合でモーフィングした音声を得ます。\nモーフィングの割合は`morph_rate`で指定でき、0.0でベースの話者、1.0でターゲットの話者に近づきます。","operationId":"_synthesis_morphing_synthesis_morphing_post","parameters":[{"required":true,"schema":{"title":"Base Speaker","type":"integer"},"name":"base_speaker","in":"query"},{"required":true,"schema":{"title":"Target Speaker","type":"integer"},"name":"target_speaker","in":"query"},{"required":true,"schema":{"title":"Morph Rate","maximum":1.0,"minimum":0.0,"type":"number"},"name":"morph_rate","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connect_waves":{"post":{"tags":["その他"],"summary":"base64エンコードされた複数のwavデータを一つに結合する","description":"base64エンコードされたwavデータを一纏めにし、wavファイルで返します。","operationId":"connect_waves_connect_waves_post","requestBody":{"content":{"application/json":{"schema":{"title":"Waves","type":"array","items":{"type":"string"}}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"audio/wav":{"schema":{"type":"string","format":"binary"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/presets":{"get":{"tags":["その他"],"summary":"Get Presets","description":"エンジンが保持しているプリセットの設定を返します\n\nReturns\n-------\npresets: List[Preset]\n プリセットのリスト","operationId":"get_presets_presets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Get Presets Presets Get","type":"array","items":{"$ref":"#/components/schemas/Preset"}}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/core_versions":{"get":{"tags":["その他"],"summary":"Core Versions","operationId":"core_versions_core_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Core Versions Core Versions Get","type":"array","items":{"type":"string"}}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","operationId":"speakers_speakers_get","parameters":[{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Speakers Speakers Get","type":"array","items":{"$ref":"#/components/schemas/Speaker"}}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"指定されたspeaker_uuidに関する情報をjson形式で返します。\n画像や音声はbase64エンコードされたものが返されます。\n\nReturns\n-------\nret_data: SpeakerInfo","operationId":"speaker_info_speaker_info_get","parameters":[{"required":true,"schema":{"title":"Speaker Uuid","type":"string"},"name":"speaker_uuid","in":"query"},{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakerInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict":{"get":{"tags":["ユーザー辞書"],"summary":"Get User Dict Words","description":"ユーザ辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。\n\nReturns\n-------\nDict[str, UserDictWord]\n 単語のUUIDとその詳細","operationId":"get_user_dict_words_user_dict_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Get User Dict Words User Dict Get","type":"object","additionalProperties":{"$ref":"#/components/schemas/UserDictWord"}}}}}}}},"/user_dict_word":{"post":{"tags":["ユーザー辞書"],"summary":"Add User Dict Word","description":"ユーザ辞書に言葉を追加します。\n\nParameters\n----------\nsurface : str\n 言葉の表層形\npronunciation: str\n 言葉の発音(カタカナ)\naccent_type: int\n アクセント型(音が下がる場所を指す)","operationId":"add_user_dict_word_user_dict_word_post","parameters":[{"required":true,"schema":{"title":"Surface","type":"string"},"name":"surface","in":"query"},{"required":true,"schema":{"title":"Pronunciation","type":"string"},"name":"pronunciation","in":"query"},{"required":true,"schema":{"title":"Accent Type","type":"integer"},"name":"accent_type","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"title":"Response Add User Dict Word User Dict Word Post","type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict_word/{word_uuid}":{"put":{"tags":["ユーザー辞書"],"summary":"Rewrite User Dict Word","description":"ユーザ辞書に登録されている言葉を更新します。\n\nParameters\n----------\nsurface : str\n 言葉の表層形\npronunciation: str\n 言葉の発音(カタカナ)\naccent_type: int\n アクセント型(音が下がる場所を指す)\nword_uuid: str\n 更新する言葉のUUID","operationId":"rewrite_user_dict_word_user_dict_word__word_uuid__put","parameters":[{"required":true,"schema":{"title":"Word Uuid","type":"string"},"name":"word_uuid","in":"path"},{"required":true,"schema":{"title":"Surface","type":"string"},"name":"surface","in":"query"},{"required":true,"schema":{"title":"Pronunciation","type":"string"},"name":"pronunciation","in":"query"},{"required":true,"schema":{"title":"Accent Type","type":"integer"},"name":"accent_type","in":"query"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["ユーザー辞書"],"summary":"Delete User Dict Word","description":"ユーザ辞書に登録されている言葉を削除します。\n\nParameters\n----------\nword_uuid: str\n 削除する言葉のUUID","operationId":"delete_user_dict_word_user_dict_word__word_uuid__delete","parameters":[{"required":true,"schema":{"title":"Word Uuid","type":"string"},"name":"word_uuid","in":"path"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/supported_devices":{"get":{"tags":["その他"],"summary":"Supported Devices","operationId":"supported_devices_supported_devices_get","parameters":[{"required":false,"schema":{"title":"Core Version","type":"string"},"name":"core_version","in":"query"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupportedDevicesInfo"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"title":"AccentPhrase","required":["moras","accent"],"type":"object","properties":{"moras":{"title":"モーラのリスト","type":"array","items":{"$ref":"#/components/schemas/Mora"}},"accent":{"title":"アクセント箇所","type":"integer"},"pause_mora":{"title":"後ろに無音を付けるかどうか","allOf":[{"$ref":"#/components/schemas/Mora"}]},"is_interrogative":{"title":"疑問系かどうか","type":"boolean","default":false}},"description":"アクセント句ごとの情報"},"AudioQuery":{"title":"AudioQuery","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"type":"object","properties":{"accent_phrases":{"title":"アクセント句のリスト","type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"}},"speedScale":{"title":"全体の話速","type":"number"},"pitchScale":{"title":"全体の音高","type":"number"},"intonationScale":{"title":"全体の抑揚","type":"number"},"volumeScale":{"title":"全体の音量","type":"number"},"prePhonemeLength":{"title":"音声の前の無音時間","type":"number"},"postPhonemeLength":{"title":"音声の後の無音時間","type":"number"},"outputSamplingRate":{"title":"音声データの出力サンプリングレート","type":"integer"},"outputStereo":{"title":"音声データをステレオ出力するか否か","type":"boolean"},"kana":{"title":"[読み取り専用]AquesTalkライクな読み仮名。音声合成クエリとしては無視される","type":"string"}},"description":"音声合成用のクエリ"},"HTTPValidationError":{"title":"HTTPValidationError","type":"object","properties":{"detail":{"title":"Detail","type":"array","items":{"$ref":"#/components/schemas/ValidationError"}}}},"Mora":{"title":"Mora","required":["text","vowel","vowel_length","pitch"],"type":"object","properties":{"text":{"title":"文字","type":"string"},"consonant":{"title":"子音の音素","type":"string"},"consonant_length":{"title":"子音の音長","type":"number"},"vowel":{"title":"母音の音素","type":"string"},"vowel_length":{"title":"母音の音長","type":"number"},"pitch":{"title":"音高","type":"number"}},"description":"モーラ(子音+母音)ごとの情報"},"ParseKanaBadRequest":{"title":"ParseKanaBadRequest","required":["text","error_name","error_args"],"type":"object","properties":{"text":{"title":"エラーメッセージ","type":"string"},"error_name":{"title":"エラー名","type":"string","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 | 処理時に無限ループになってしまいました...バグ報告をお願いします。 |"},"error_args":{"title":"エラーを起こした箇所","type":"object","additionalProperties":{"type":"string"}}}},"Preset":{"title":"Preset","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"type":"object","properties":{"id":{"title":"プリセットID","type":"integer"},"name":{"title":"プリセット名","type":"string"},"speaker_uuid":{"title":"スピーカーのUUID","type":"string"},"style_id":{"title":"スタイルID","type":"integer"},"speedScale":{"title":"全体の話速","type":"number"},"pitchScale":{"title":"全体の音高","type":"number"},"intonationScale":{"title":"全体の抑揚","type":"number"},"volumeScale":{"title":"全体の音量","type":"number"},"prePhonemeLength":{"title":"音声の前の無音時間","type":"number"},"postPhonemeLength":{"title":"音声の後の無音時間","type":"number"}},"description":"プリセット情報"},"Speaker":{"title":"Speaker","required":["name","speaker_uuid","styles"],"type":"object","properties":{"name":{"title":"名前","type":"string"},"speaker_uuid":{"title":"スピーカーのUUID","type":"string"},"styles":{"title":"スピーカースタイルの一覧","type":"array","items":{"$ref":"#/components/schemas/SpeakerStyle"}},"version":{"title":"Version","type":"string","default":"スピーカーのバージョン"}},"description":"スピーカー情報"},"SpeakerInfo":{"title":"SpeakerInfo","required":["policy","portrait","style_infos"],"type":"object","properties":{"policy":{"title":"policy.md","type":"string"},"portrait":{"title":"portrait.pngをbase64エンコードしたもの","type":"string"},"style_infos":{"title":"スタイルの追加情報","type":"array","items":{"$ref":"#/components/schemas/StyleInfo"}}},"description":"話者の追加情報"},"SpeakerStyle":{"title":"SpeakerStyle","required":["name","id"],"type":"object","properties":{"name":{"title":"スタイル名","type":"string"},"id":{"title":"スタイルID","type":"integer"}},"description":"スピーカーのスタイル情報"},"StyleInfo":{"title":"StyleInfo","required":["id","icon","voice_samples"],"type":"object","properties":{"id":{"title":"スタイルID","type":"integer"},"icon":{"title":"当該スタイルのアイコンをbase64エンコードしたもの","type":"string"},"voice_samples":{"title":"voice_sampleのwavファイルをbase64エンコードしたもの","type":"array","items":{"type":"string"}}},"description":"スタイルの追加情報"},"SupportedDevicesInfo":{"title":"SupportedDevicesInfo","required":["cpu","cuda"],"type":"object","properties":{"cpu":{"title":"CPUに対応しているか","type":"boolean"},"cuda":{"title":"CUDA(GPU)に対応しているか","type":"boolean"}},"description":"対応しているデバイスの情報"},"UserDictWord":{"title":"UserDictWord","required":["surface","cost","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"],"type":"object","properties":{"surface":{"title":"表層形","type":"string"},"cost":{"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"},"inflectional_type":{"title":"活用型","type":"string"},"inflectional_form":{"title":"活用形","type":"string"},"stem":{"title":"原形","type":"string"},"yomi":{"title":"読み","type":"string"},"pronunciation":{"title":"発音","type":"string"},"accent_type":{"title":"アクセント型","type":"integer"},"mora_count":{"title":"モーラ数","type":"integer"},"accent_associative_rule":{"title":"アクセント結合規則","type":"string"}},"description":"辞書のコンパイルに使われる情報"},"ValidationError":{"title":"ValidationError","required":["loc","msg","type"],"type":"object","properties":{"loc":{"title":"Location","type":"array","items":{"type":"string"}},"msg":{"title":"Message","type":"string"},"type":{"title":"Error Type","type":"string"}}}}}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b49528f7cd..b0d43e0308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "voicevox", - "version": "0.9.4", + "version": "999.999.999", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -46,6 +46,14 @@ "json5": "^2.1.2", "semver": "^6.3.0", "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/generator": { @@ -88,6 +96,14 @@ "@babel/helper-validator-option": "^7.12.17", "browserslist": "^4.14.5", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/helper-create-class-features-plugin": { @@ -128,6 +144,14 @@ "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/helper-explode-assignable-expression": { @@ -901,6 +925,14 @@ "babel-plugin-polyfill-corejs3": "^0.2.0", "babel-plugin-polyfill-regenerator": "^0.2.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -1047,6 +1079,14 @@ "babel-plugin-polyfill-regenerator": "^0.2.0", "core-js-compat": "^3.9.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/preset-modules": { @@ -1282,6 +1322,12 @@ "jsonfile": "^4.0.0", "universalify": "^0.1.0" } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -2251,7 +2297,8 @@ "@types/semver": { "version": "7.3.9", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz", - "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==" + "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", + "dev": true }, "@types/serve-static": { "version": "1.13.9", @@ -2734,6 +2781,14 @@ "core-js": "^3.6.5", "core-js-compat": "^3.6.5", "semver": "^6.1.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@vue/babel-preset-jsx": { @@ -3326,6 +3381,14 @@ "request": "^2.88.2", "semver": "^6.1.0", "strip-ansi": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@vue/compiler-core": { @@ -4466,6 +4529,14 @@ "@babel/compat-data": "^7.13.11", "@babel/helper-define-polyfill-provider": "^0.2.0", "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "babel-plugin-polyfill-corejs3": { @@ -6791,6 +6862,12 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -8762,6 +8839,12 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -12451,6 +12534,14 @@ "dev": true, "requires": { "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "map-cache": { @@ -14157,6 +14248,14 @@ "registry-auth-token": "^4.0.0", "registry-url": "^5.0.0", "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "pako": { @@ -16087,6 +16186,14 @@ "neo-async": "^2.6.1", "schema-utils": "^2.6.1", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "sax": { @@ -16150,10 +16257,27 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } }, "semver-compare": { "version": "1.0.0", @@ -16169,6 +16293,14 @@ "dev": true, "requires": { "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "send": { @@ -17738,6 +17870,12 @@ "picomatch": "^2.2.3" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -19521,6 +19659,12 @@ "ajv-keywords": "^3.1.0" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", diff --git a/package.json b/package.json index bea5acdf6a..150e887ed6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "voicevox", - "version": "0.0.0", + "version": "999.999.999", "author": "Hiroshiba Kazuyuki", "private": true, "engines": { @@ -40,6 +40,7 @@ "lodash.debounce": "4.0.8", "markdown-it": "12.0.4", "quasar": "2.3.4", + "semver": "7.3.5", "source-map-support": "0.5.19", "systeminformation": "5.8.0", "tree-kill": "1.2.2", @@ -58,6 +59,7 @@ "@types/markdown-it": "12.2.0", "@types/mocha": "5.2.7", "@types/mousetrap": "1.6.8", + "@types/semver": "7.3.9", "@types/uuid": "8.3.0", "@typescript-eslint/eslint-plugin": "4.24.0", "@typescript-eslint/parser": "4.24.0", diff --git a/public/howtouse.md b/public/howtouse.md index 6f15adb9ba..b955360cd6 100644 --- a/public/howtouse.md +++ b/public/howtouse.md @@ -61,6 +61,12 @@ +キャラクターの表示順序は「キャラクター並び替え」で変更できます。 + +## テキスト欄の並び替え + +テキスト欄周辺をドラッグすることで、テキスト欄の順番を並び替えられます。 + ## 単語の接続変更 意図しない箇所で単語が分離していた場合や、意図しない形で結合してしまっている場合は、アクセント項目で文字の間をクリックすることで修正できます。 @@ -197,11 +203,30 @@ -## デフォルトスタイル・試聴 +## キャラクターの並び替え・試聴 -「設定」の「デフォルトスタイル」で、キャラクターごとのデフォルトのスタイルを変更することができます。 +「設定」の「キャラクターの並び替え」で、キャラクターの表示順序を変更することができます。 また、キャラクターごとのサンプルボイスを試聴することもできます。 +## デフォルトスタイル + +「設定」の「デフォルトスタイル」で、キャラクターごとのデフォルトのスタイルを変更することができます。 + +## 読み方&アクセント辞書 + +難しい単語や新しい単語は正しい読みにならないことがありますが、辞書機能を使って読み方を登録しておくことができます。 +辞書機能は「設定」の「読み方&アクセント辞書」で利用できます。 + +読み方&アクセント辞書画面を開くと、左に登録した単語のリストが表示されます。 +「追加」ボタンで新規に単語を登録できます。 + + + +「単語」に登録したいテキストを、「読み」にそのテキストの読み方をひらがなかカタカナで入力してください。 +「アクセント調整」で自然になるアクセントを登録できます。 + + + ## オプション 「設定」の「オプション」でいろいろな設定を変更することができます。 @@ -271,10 +296,6 @@ GPU モードを利用するには、3GB 以上のメモリがある NVIDIA 製 疑問文のときに自動的に語尾の音を上げて、疑問文っぽい音声を生成するようになります。 -#### テキスト欄の並び替え - -テキスト欄左のキャラクターアイコンをドラッグすることで、テキスト欄の順番を並び替えられるようになります。 - ### 「データ収集」項目 #### ソフトウェア利用状況のデータ収集を許可する diff --git a/public/qAndA.md b/public/qAndA.md index ce5de7140d..d37b7ca8c3 100644 --- a/public/qAndA.md +++ b/public/qAndA.md @@ -6,6 +6,10 @@ Windows/Mac/Linux 搭載の PC に対応しています。 +※Windows:Windows 10・Windows 11 +※Mac:macOS Catalina以降 +※Linux:Ubuntu 18.04・Ubuntu 20.04 + #### GPU 版 Windows/Linux と Nvidia 製 GPU 搭載の PC に対応しています。 @@ -47,6 +51,10 @@ Windows/Linux と Nvidia 製 GPU 搭載の PC に対応しています。 `/Applications/VOICEVOX` もしくは `/Users/(ユーザー名)/Applications/VOICEVOX` +### Q. アップデート方法を教えてください。 + +ホームページから最新版を再インストールすることでアップデートできます。 + ## 使い方に関する質問 ### Q. 使い方がわかりません。 diff --git a/public/res/dict01.png b/public/res/dict01.png new file mode 100644 index 0000000000..6c7b7dd182 Binary files /dev/null and b/public/res/dict01.png differ diff --git a/public/res/dict02.png b/public/res/dict02.png new file mode 100644 index 0000000000..c0dea82fb7 Binary files /dev/null and b/public/res/dict02.png differ diff --git a/public/updateInfos.json b/public/updateInfos.json index 7c0c5b7dd5..c07ec92f84 100644 --- a/public/updateInfos.json +++ b/public/updateInfos.json @@ -1,4 +1,54 @@ [ + { + "version": "0.11.4", + "descriptions": [ + "キャラクター「九州そら」を追加", + "途中に疑問形があるとおかしくなる問題を修正" + ], + "contributors": ["Hiroshiba", "aoirint", "y-chan"] + }, + { + "version": "0.11.3", + "descriptions": [ + "再生位置を指定できない問題を修正", + "テキストを範囲選択できない問題を修正", + "辞書UIのバグを修正", + "辞書登録できない問題を修正" + ], + "contributors": ["Hiroshiba", "madosuki", "y-chan"] + }, + { + "version": "0.11.2", + "descriptions": ["エンジンが起動しない問題を修正", "スタイルの順番を修正"], + "contributors": ["aoirint"] + }, + { + "version": "0.11.0", + "descriptions": [ + "キャラクター「玄野武宏」「白上虎太郎」「青山龍星」「冥鳴ひまり」を追加", + "読み方&アクセント辞書機能の追加", + "キャラクター並び替え機能の追加", + "ヘルプからアップデートの有無を確認可能に", + "利用規約の同意を促すダイアログを追加", + "UXの向上", + "開発環境の向上", + "バグ修正" + ], + "contributors": [ + "aoirint", + "gobosan", + "Hiroshiba", + "HyodaKazuaki", + "Lapis256", + "Oyaki122", + "PickledChair", + "Segu-g", + "takana-v", + "xuzijian629", + "y-chan", + "Yosshi999" + ] + }, { "version": "0.10.4", "descriptions": ["途中に疑問形があるとおかしくなる問題を修正"], diff --git a/src/App.vue b/src/App.vue index cfbf273b2f..9c3441312a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,7 +20,6 @@ export default defineComponent({ setup() { const store = useStore(); store.dispatch("INIT_VUEX"); - store.dispatch("START_WAITING_ENGINE"); // Google Tag Manager const gtm = useGtm(); diff --git a/src/background.ts b/src/background.ts index e4015c1813..12bdd6c6bb 100644 --- a/src/background.ts +++ b/src/background.ts @@ -34,6 +34,7 @@ import { AcceptTermsStatus, ToolbarSetting, ActivePointScrollMode, + EngineInfo, } from "./type/preload"; import { autoUpdater } from "electron-updater"; @@ -77,15 +78,27 @@ process.on("unhandledRejection", (reason) => { log.error(reason); }); -// 設定 +// .envから設定をprocess.envに読み込み const appDirPath = path.dirname(app.getPath("exe")); const envPath = path.join(appDirPath, ".env"); dotenv.config({ path: envPath }); + protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); const isMac = process.platform === "darwin"; + +const engineInfos: EngineInfo[] = (() => { + const defaultEngineInfosEnv = process.env.DEFAULT_ENGINE_INFOS; + + if (defaultEngineInfosEnv) { + return JSON.parse(defaultEngineInfosEnv) as EngineInfo[]; + } + + return []; +})(); + const defaultHotkeySettings: HotkeySetting[] = [ { action: "音声書き出し", @@ -183,6 +196,7 @@ const store = new Store<{ presets: PresetConfig; hotkeySettings: HotkeySetting[]; toolbarSetting: ToolbarSetting; + userCharacterOrder: string[]; defaultStyleIds: DefaultStyleId[]; currentTheme: string; experimentalSetting: ExperimentalSetting; @@ -255,6 +269,13 @@ const store = new Store<{ }, default: defaultToolbarButtonSetting, }, + userCharacterOrder: { + type: "array", + items: { + type: "string", + }, + default: [], + }, defaultStyleIds: { type: "array", items: { @@ -315,15 +336,10 @@ const store = new Store<{ type: "boolean", default: false, }, - enableReorderCell: { - type: "boolean", - default: false, - }, }, default: { enablePreset: false, enableInterrogativeUpspeak: false, - enableReorderCell: false, }, }, acceptRetrieveTelemetry: { @@ -341,10 +357,51 @@ const store = new Store<{ }); // engine -let willQuitEngine = false; -let engineProcess: ChildProcess; -async function runEngine() { - willQuitEngine = false; +type EngineProcessContainer = { + willQuitEngine: boolean; + engineProcess?: ChildProcess; +}; + +const engineProcessContainers: Record = {}; + +async function runEngineAll() { + log.info(`Starting ${engineInfos.length} engine/s...`); + + for (const engineInfo of engineInfos) { + log.info(`ENGINE ${engineInfo.key}: Start launching`); + await runEngine(engineInfo.key); + } +} + +async function runEngine(engineKey: string) { + const engineInfo = engineInfos.find( + (engineInfo) => engineInfo.key === engineKey + ); + if (!engineInfo) + throw new Error(`No such engineInfo registered: key == ${engineKey}`); + + if (!engineInfo.executionEnabled) { + log.info(`ENGINE ${engineKey}: Skipped engineInfo execution: disabled`); + return; + } + + if (!engineInfo.executionFilePath) { + log.info( + `ENGINE ${engineKey}: Skipped engineInfo execution: empty executionFilePath` + ); + return; + } + + log.info(`ENGINE ${engineKey}: Starting process`); + + if (!(engineKey in engineProcessContainers)) { + engineProcessContainers[engineKey] = { + willQuitEngine: false, + }; + } + + const engineProcessContainer = engineProcessContainers[engineKey]; + engineProcessContainer.willQuitEngine = false; // 最初のエンジンモード if (!store.has("useGpu")) { @@ -366,36 +423,38 @@ async function runEngine() { } const useGpu = store.get("useGpu"); - log.info(`Starting ENGINE`); - log.info(`ENGINE mode: ${useGpu ? "GPU" : "CPU"}`); + log.info(`ENGINE ${engineKey} mode: ${useGpu ? "GPU" : "CPU"}`); // エンジンプロセスの起動 const enginePath = path.resolve( appDirPath, - process.env.ENGINE_PATH ?? "run.exe" + engineInfo.executionFilePath ?? "run.exe" ); const args = useGpu ? ["--use_gpu"] : []; - log.info(`ENGINE path: ${enginePath}`); - log.info(`ENGINE args: ${JSON.stringify(args)}`); + log.info(`ENGINE ${engineKey} path: ${enginePath}`); + log.info(`ENGINE ${engineKey} args: ${JSON.stringify(args)}`); - engineProcess = spawn(enginePath, args, { + const engineProcess = spawn(enginePath, args, { cwd: path.dirname(enginePath), }); + engineProcessContainer.engineProcess = engineProcess; engineProcess.stdout?.on("data", (data) => { - log.info(`ENGINE: ${data.toString("utf-8")}`); + log.info(`ENGINE ${engineKey} STDOUT: ${data.toString("utf-8")}`); }); engineProcess.stderr?.on("data", (data) => { - log.error(`ENGINE: ${data.toString("utf-8")}`); + log.error(`ENGINE ${engineKey} STDERR: ${data.toString("utf-8")}`); }); engineProcess.on("close", (code, signal) => { - log.info(`ENGINE: process terminated due to receipt of signal ${signal}`); - log.info(`ENGINE: process exited with code ${code}`); + log.info( + `ENGINE ${engineKey}: Process terminated due to receipt of signal ${signal}` + ); + log.info(`ENGINE ${engineKey}: Process exited with code ${code}`); - if (!willQuitEngine) { + if (!engineProcessContainer.willQuitEngine) { ipcMainSend(win, "DETECTED_ENGINE_ERROR"); dialog.showErrorBox( "音声合成エンジンエラー", @@ -405,45 +464,168 @@ async function runEngine() { }); } -async function restartEngine() { +function killEngineAll({ + onFirstKillStart, + onAllKilled, + onError, +}: { + onFirstKillStart?: VoidFunction; + onAllKilled?: VoidFunction; + onError?: (engineKey: string, message: unknown) => void; +}) { + let anyKillStart = false; + + const numEngineProcess = Object.keys(engineProcessContainers).length; + let numEngineProcessKilled = 0; + + for (const [engineKey] of Object.entries(engineProcessContainers)) { + killEngine({ + engineKey, + onKillStart: () => { + if (!anyKillStart) { + anyKillStart = true; + onFirstKillStart?.(); + } + }, + onKilled: () => { + numEngineProcessKilled++; + log.info( + `ENGINE ${numEngineProcessKilled} / ${numEngineProcess} processes killed` + ); + + if (numEngineProcessKilled === numEngineProcess) { + onAllKilled?.(); + } + }, + onError: (message) => { + onError?.(engineKey, message); + + // エディタを終了するため、エラーが起きてもエンジンプロセスをキルできたとみなして次のエンジンプロセスをキルする + numEngineProcessKilled++; + log.info( + `ENGINE ${engineKey}: process kill errored, but assume to have been killed` + ); + log.info( + `ENGINE ${numEngineProcessKilled} / ${numEngineProcess} processes killed` + ); + + if (numEngineProcessKilled === numEngineProcess) { + onAllKilled?.(); + } + }, + }); + } +} + +function killEngine({ + engineKey, + onKillStart, + onKilled, + onError, +}: { + engineKey: string; + onKillStart?: VoidFunction; + onKilled?: VoidFunction; + onError?: (error: unknown) => void; +}) { + // この関数では、呼び出し元に結果を通知するためonKilledまたはonErrorを同期または非同期で必ず呼び出さなければならない + + const engineProcessContainer = engineProcessContainers[engineKey]; + if (!engineProcessContainer) { + onError?.(`No such engineProcessContainer: key == ${engineKey}`); + return; + } + + const engineProcess = engineProcessContainer.engineProcess; + if (engineProcess == undefined) { + // nop if no process started (already killed or not started yet) + log.info(`ENGINE ${engineKey}: Process not started`); + onKilled?.(); + return; + } + + // considering the case that ENGINE process killed after checking process status + engineProcess.once("close", () => { + log.info(`ENGINE ${engineKey}: Process closed`); + onKilled?.(); + }); + + log.info( + `ENGINE ${engineKey}: last exit code: ${engineProcess.exitCode}, signal: ${engineProcess.signalCode}` + ); + + const engineNotExited = engineProcess.exitCode === null; + const engineNotKilled = engineProcess.signalCode === null; + + if (engineNotExited && engineNotKilled) { + log.info(`ENGINE ${engineKey}: Killing process (PID=${engineProcess.pid})`); + onKillStart?.(); + + engineProcessContainer.willQuitEngine = true; + try { + engineProcess.pid != undefined && treeKill(engineProcess.pid); + } catch (error: unknown) { + log.error(`ENGINE ${engineKey}: Error during killing process`); + onError?.(error); + } + } else { + log.info(`ENGINE ${engineKey}: Process already closed`); + onKilled?.(); + } +} + +async function restartEngineAll() { + for (const engineInfo of engineInfos) { + await restartEngine(engineInfo.key); + } +} + +async function restartEngine(engineKey: string) { await new Promise((resolve, reject) => { + const engineProcessContainer: EngineProcessContainer | undefined = + engineProcessContainers[engineKey]; + const engineProcess = engineProcessContainer?.engineProcess; + log.info( - `Restarting ENGINE (last exit code: ${engineProcess.exitCode}, signal: ${engineProcess.signalCode})` + `ENGINE ${engineKey}: Restarting process (last exit code: ${engineProcess?.exitCode}, signal: ${engineProcess?.signalCode})` ); // エンジンのプロセスがすでに終了している、またはkillされている場合 - const engineExited = engineProcess.exitCode !== null; - const engineKilled = engineProcess.signalCode !== null; + const engineExited = engineProcess?.exitCode !== null; + const engineKilled = engineProcess?.signalCode !== null; + // engineProcess === undefinedの場合true if (engineExited || engineKilled) { log.info( - "ENGINE process is not started yet or already killed. Starting ENGINE..." + `ENGINE ${engineKey}: Process is not started yet or already killed. Starting process...` ); - runEngine(); + runEngine(engineKey); resolve(); return; } // エンジンエラー時のエラーウィンドウ抑制用。 - willQuitEngine = true; + engineProcessContainer.willQuitEngine = true; // 「killに使用するコマンドが終了するタイミング」と「OSがプロセスをkillするタイミング」が違うので単純にtreeKillのコールバック関数でrunEngine()を実行すると失敗します。 // closeイベントはexitイベントよりも後に発火します。 const restartEngineOnProcessClosedCallback = () => { - log.info("ENGINE process killed. Restarting ENGINE..."); + log.info(`ENGINE ${engineKey}: Process killed. Restarting process...`); - runEngine(); + runEngine(engineKey); resolve(); }; engineProcess.once("close", restartEngineOnProcessClosedCallback); // treeKillのコールバック関数はコマンドが終了した時に呼ばれます。 - log.info(`Killing current ENGINE process (PID=${engineProcess.pid})...`); + log.info( + `ENGINE ${engineKey}: Killing current process (PID=${engineProcess.pid})...` + ); treeKill(engineProcess.pid, (error) => { // error変数の値がundefined以外であればkillコマンドが失敗したことを意味します。 if (error != null) { - log.error("Failed to kill ENGINE"); + log.error(`ENGINE ${engineKey}: Failed to kill process`); log.error(error); // killに失敗したとき、closeイベントが発生せず、once listenerが消費されない @@ -626,8 +808,29 @@ async function createWindow() { }); } -if (!isDevelopment) { - Menu.setApplicationMenu(null); +const menuTemplateForMac: Electron.MenuItemConstructorOptions[] = [ + { + label: "VOICEVOX", + submenu: [{ role: "quit" }], + }, + { + label: "Edit", + submenu: [ + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "selectAll" }, + ], + }, +]; + +// For macOS, set the native menu to enable shortcut keys such as 'Cmd + V'. +if (isMac) { + Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplateForMac)); +} else { + if (!isDevelopment) { + Menu.setApplicationMenu(null); + } } // プロセス間通信 @@ -834,12 +1037,21 @@ ipcMainHandle("LOG_INFO", (_, ...params) => { log.info(...params); }); +ipcMainHandle("ENGINE_INFOS", () => { + // エンジン情報を設定ファイルに保存しないためにstoreではなくグローバル変数を使用する + return engineInfos; +}); + /** * エンジンを再起動する。 * エンジンの起動が開始したらresolve、起動が失敗したらreject。 */ -ipcMainHandle("RESTART_ENGINE", async () => { - await restartEngine(); +ipcMainHandle("RESTART_ENGINE_ALL", async () => { + await restartEngineAll(); +}); + +ipcMainHandle("RESTART_ENGINE", async (_, { engineKey }) => { + await restartEngine(engineKey); }); ipcMainHandle("SAVING_SETTING", (_, { newData }) => { @@ -908,6 +1120,14 @@ ipcMainHandle("SAVING_PRESETS", (_, { newPresets }) => { return store.get("presets"); }); +ipcMainHandle("GET_USER_CHARACTER_ORDER", () => { + return store.get("userCharacterOrder"); +}); + +ipcMainHandle("SET_USER_CHARACTER_ORDER", (_, userCharacterOrder) => { + store.set("userCharacterOrder", userCharacterOrder); +}); + ipcMainHandle("IS_UNSET_DEFAULT_STYLE_ID", (_, speakerUuid) => { const defaultStyleIds = store.get("defaultStyleIds"); return !defaultStyleIds.find((style) => style.speakerUuid === speakerUuid); @@ -978,32 +1198,31 @@ app.on("before-quit", (event) => { return; } - // considering the case that ENGINE process killed after checking process status - engineProcess.once("close", () => { - log.info("ENGINE killed. Quitting app"); - app.quit(); // attempt to quit app again - }); - - log.info( - `Quitting app (ENGINE last exit code: ${engineProcess.exitCode}, signal: ${engineProcess.signalCode})` - ); - - const engineNotExited = engineProcess.exitCode === null; - const engineNotKilled = engineProcess.signalCode === null; + let anyKillStart = false; - if (engineNotExited && engineNotKilled) { - log.info("Killing ENGINE before app quit"); - event.preventDefault(); + log.info("Checking ENGINE status before app quit"); + killEngineAll({ + onFirstKillStart: () => { + anyKillStart = true; - log.info(`Killing ENGINE (PID=${engineProcess.pid})...`); - willQuitEngine = true; - try { - engineProcess.pid != undefined && treeKill(engineProcess.pid); - } catch (error: unknown) { - log.error("engine kill error"); - log.error(error); - } - } + // executed synchronously to cancel before-quit event + log.info("Interrupt app quit to kill ENGINE processes"); + event.preventDefault(); + }, + onAllKilled: () => { + // executed asynchronously + if (anyKillStart) { + log.info("All ENGINE process killed. Quitting app"); + app.quit(); // attempt to quit app again + } + // else: before-quit event is not cancelled + }, + onError: (engineKey, message) => { + log.error( + `ENGINE ${engineKey}: Error during killing process: ${message}` + ); + }, + }); }); app.on("activate", () => { @@ -1029,7 +1248,7 @@ app.on("ready", async () => { } } - createWindow().then(() => runEngine()); + createWindow().then(() => runEngineAll()); const isAutoUpdateCheck = store.get("isAutoUpdateCheck"); if (isAutoUpdateCheck) { await autoUpdater.checkForUpdatesAndNotify(); diff --git a/src/components/AudioAccent.vue b/src/components/AudioAccent.vue index 7630134227..2c0ca4bce5 100644 --- a/src/components/AudioAccent.vue +++ b/src/components/AudioAccent.vue @@ -21,6 +21,9 @@ @update:model-value=" previewAccentSlider.qSliderProps['onUpdate:modelValue'] " + @click.stop=" + undefined; // クリックでアクセント句が選択されないように + " @change="previewAccentSlider.qSliderProps.onChange" @wheel="previewAccentSlider.qSliderProps.onWheel" @pan="previewAccentSlider.qSliderProps.onPan" diff --git a/src/components/AudioCell.vue b/src/components/AudioCell.vue index a02839395c..3862443ba8 100644 --- a/src/components/AudioCell.vue +++ b/src/components/AudioCell.vue @@ -7,11 +7,7 @@ size="sm" class="absolute active-arrow" /> - + @@ -176,7 +172,9 @@ export default defineComponent({ setup(props, { emit }) { const store = useStore(); - const characterInfos = computed(() => store.state.characterInfos); + const userOrderedCharacterInfos = computed( + () => store.getters.USER_ORDERED_CHARACTER_INFOS + ); const audioItem = computed(() => store.state.audioItems[props.audioKey]); const nowPlaying = computed( () => store.state.audioStates[props.audioKey].nowPlaying @@ -188,9 +186,9 @@ export default defineComponent({ const uiLocked = computed(() => store.getters.UI_LOCKED); const selectedCharacterInfo = computed(() => - store.state.characterInfos !== undefined && + userOrderedCharacterInfos.value !== undefined && audioItem.value.styleId !== undefined - ? store.state.characterInfos.find((info) => + ? userOrderedCharacterInfos.value.find((info) => info.metas.styles.find( (style) => style.styleId === audioItem.value.styleId ) @@ -204,12 +202,14 @@ export default defineComponent({ ); const subMenuOpenFlags = ref( - [...Array(characterInfos.value?.length)].map(() => false) + [...Array(userOrderedCharacterInfos.value?.length)].map(() => false) ); const reassignSubMenuOpen = debounce((idx: number) => { if (subMenuOpenFlags.value[idx]) return; - const arr = [...Array(characterInfos.value?.length)].map(() => false); + const arr = [...Array(userOrderedCharacterInfos.value?.length)].map( + () => false + ); arr[idx] = true; subMenuOpenFlags.value = arr; }, 100); @@ -252,7 +252,7 @@ export default defineComponent({ }); }; const getDefaultStyle = (speakerUuid: string) => { - const characterInfo = characterInfos.value?.find( + const characterInfo = userOrderedCharacterInfos.value?.find( (info) => info.metas.speakerUuid === speakerUuid ); const defaultStyleId = store.state.defaultStyleIds.find( @@ -389,11 +389,8 @@ export default defineComponent({ textfield.value.focus(); }; - // キャラクター選択 - const isOpenedCharacterList = ref(false); - return { - characterInfos, + userOrderedCharacterInfos, audioItem, deleteButtonEnable, uiLocked, @@ -424,7 +421,6 @@ export default defineComponent({ textfield, focusTextField, blurCell, - isOpenedCharacterList, }; }, }); @@ -435,7 +431,14 @@ export default defineComponent({ .audio-cell { display: flex; - margin: 1rem 1rem; + padding: 0.4rem 0.5rem; + margin: 0.2rem 0.5rem; + &:first-child { + margin-top: 0.6rem; + } + &:last-child { + margin-bottom: 0.6rem; + } gap: 0px 1rem; .active-arrow { left: -5px; diff --git a/src/components/AudioDetail.vue b/src/components/AudioDetail.vue index 992004db22..ab798193a6 100644 --- a/src/components/AudioDetail.vue +++ b/src/components/AudioDetail.vue @@ -148,7 +148,10 @@ }" @mouseover="handleHoverText(true, accentPhraseIndex, moraIndex)" @mouseleave="handleHoverText(false, accentPhraseIndex, moraIndex)" - @click="handleChangeVoicing(mora, accentPhraseIndex, moraIndex)" + @click.stop=" + uiLocked || + handleChangeVoicing(mora, accentPhraseIndex, moraIndex) + " > {{ getHoveredText(mora, accentPhraseIndex, moraIndex) }} {{ accentPhrase.pauseMora.text }} -1 - ) { - let data = 0; - if (mora.pitch == 0) { - if (lastPitches.value[accentPhraseIndex][moraIndex] == 0) { - // 元々無声だった場合、適当な値を代入 - data = 5.5; - } else { - data = lastPitches.value[accentPhraseIndex][moraIndex]; - } + if ( + selectedDetail.value == "pitch" && + unvoicableVowels.indexOf(mora.vowel) > -1 + ) { + let data = 0; + if (mora.pitch == 0) { + if (lastPitches.value[accentPhraseIndex][moraIndex] == 0) { + // 元々無声だった場合、適当な値を代入 + data = 5.5; + } else { + data = lastPitches.value[accentPhraseIndex][moraIndex]; } - changeMoraData(accentPhraseIndex, moraIndex, data, "voicing"); } + changeMoraData(accentPhraseIndex, moraIndex, data, "voicing"); } }; @@ -785,10 +786,10 @@ $pitch-label-height: 24px; .accent-phrase-table { flex-grow: 1; align-self: stretch; - margin-left: 5px; - margin-right: 5px; - margin-bottom: 5px; - padding-left: 5px; + margin-left: 4px; + margin-right: 4px; + margin-bottom: 4px; + padding-left: 4px; display: flex; overflow-x: scroll; diff --git a/src/components/AudioParameter.vue b/src/components/AudioParameter.vue index 4d6e9fbd91..54a481a13f 100644 --- a/src/components/AudioParameter.vue +++ b/src/components/AudioParameter.vue @@ -25,6 +25,9 @@ :disable="previewSlider.qSliderProps.disable.value" :model-value="previewSlider.qSliderProps.modelValue.value" @update:model-value="previewSlider.qSliderProps['onUpdate:modelValue']" + @click.stop=" + undefined; // クリックでアクセント句が選択されないように + " @change="previewSlider.qSliderProps.onChange" @wheel="previewSlider.qSliderProps.onWheel" @pan="previewSlider.qSliderProps.onPan" diff --git a/src/components/CharacterOrderDialog.vue b/src/components/CharacterOrderDialog.vue new file mode 100644 index 0000000000..03b6ecb419 --- /dev/null +++ b/src/components/CharacterOrderDialog.vue @@ -0,0 +1,539 @@ + + + + + + + {{ + hasNewCharacter + ? "追加キャラクターの紹介" + : "設定 / キャラクター並び替え・試聴" + }} + + + + + + + + + + + + + + + + + + + + サンプルボイス一覧 + + + + + {{ + characterInfosMap[speakerUuid].metas.speakerName + }} + + + {{ + selectedStyles[speakerUuid].styleName || "ノーマル" + }} + + + + + + + NEW! + + + + + + + + + キャラクター並び替え + + + + + {{ element.metas.speakerName }} + + + + + + + + + + + + + diff --git a/src/components/DefaultStyleSelectDialog.vue b/src/components/DefaultStyleSelectDialog.vue index 4efe678e4a..9b1c20b64b 100644 --- a/src/components/DefaultStyleSelectDialog.vue +++ b/src/components/DefaultStyleSelectDialog.vue @@ -54,22 +54,22 @@ @@ -131,7 +131,10 @@ round outline :icon=" - style.styleId === playing?.styleId && + playing != undefined && + characterInfo.metas.speakerUuid === + playing.speakerUuid && + style.styleId === playing.styleId && voiceSampleIndex === playing.index ? 'stop' : 'play_arrow' @@ -141,7 +144,10 @@ @mouseenter="isHoverableStyleItem = false" @mouseleave="isHoverableStyleItem = true" @click.stop=" - style.styleId === playing?.styleId && + playing != undefined && + characterInfo.metas.speakerUuid === + playing.speakerUuid && + style.styleId === playing.styleId && voiceSampleIndex === playing.index ? stop() : play(style, voiceSampleIndex) @@ -199,10 +205,17 @@ export default defineComponent({ set: (val) => emit("update:modelValue", val), }); - // アップデートで増えたキャラ・スタイルがあれば、それらに対して起動時にデフォルトスタイル選択・試聴を問うための変数 + // 複数スタイルあるキャラクター + const multiStyleCharacterInfos = computed(() => { + return props.characterInfos.filter( + (characterInfo) => characterInfo.metas.styles.length > 1 + ); + }); + + // アップデートで増えたスタイルがあれば、それらに対して起動時にデフォルトスタイル選択を問うための変数 // その他の場合は、characterInfosと同じになる // FIXME: 現状はスタイルが増えてもデフォルトスタイルを問えないので、そこを改修しなければならない - const showCharacterInfos = ref(props.characterInfos); + const showCharacterInfos = ref(multiStyleCharacterInfos.value); const isFirstTime = ref(false); const selectedStyleIndexes = ref<(number | undefined)[]>([]); @@ -214,7 +227,7 @@ export default defineComponent({ if (!oldValue && newValue) { showCharacterInfos.value = []; selectedStyleIndexes.value = await Promise.all( - props.characterInfos.map(async (info) => { + multiStyleCharacterInfos.value.map(async (info) => { const styles = info.metas.styles; const isUnsetDefaultStyleId = await store.dispatch( "IS_UNSET_DEFAULT_STYLE_ID", @@ -237,7 +250,7 @@ export default defineComponent({ }) ); if (!isFirstTime.value) { - showCharacterInfos.value = props.characterInfos; + showCharacterInfos.value = multiStyleCharacterInfos.value; } else { selectedStyleIndexes.value = showCharacterInfos.value.map( (info) => { @@ -256,16 +269,17 @@ export default defineComponent({ const selectStyleIndex = (characterIndex: number, styleIndex: number) => { selectedStyleIndexes.value[characterIndex] = styleIndex; - // 音声を再生する。同じstyleIndexだったら停止する。 - const selectedStyleInfo = - showCharacterInfos.value[characterIndex].metas.styles[styleIndex]; + // 音声を再生する。同じ話者/styleIndexだったら停止する。 + const selectedCharacter = showCharacterInfos.value[characterIndex]; + const selectedStyleInfo = selectedCharacter.metas.styles[styleIndex]; if ( playing.value !== undefined && + playing.value.speakerUuid === selectedCharacter.metas.speakerUuid && playing.value.styleId === selectedStyleInfo.styleId ) { stop(); } else { - play(selectedStyleInfo, 0); + play(selectedCharacter.metas.speakerUuid, selectedStyleInfo, 0); } }; @@ -273,7 +287,8 @@ export default defineComponent({ const isHoverableStyleItem = ref(true); - const playing = ref<{ styleId: number; index: number }>(); + const playing = + ref<{ speakerUuid: string; styleId: number; index: number }>(); const audio = new Audio(); audio.volume = 0.5; @@ -284,12 +299,16 @@ export default defineComponent({ return selectedStyleIndex !== undefined; }); - const play = ({ styleId, voiceSamplePaths }: StyleInfo, index: number) => { + const play = ( + speakerUuid: string, + { styleId, voiceSamplePaths }: StyleInfo, + index: number + ) => { if (audio.src !== "") stop(); audio.src = voiceSamplePaths[index]; audio.play(); - playing.value = { styleId, index }; + playing.value = { speakerUuid, styleId, index }; }; const stop = () => { if (audio.src === "") return; diff --git a/src/components/DictionaryManageDialog.vue b/src/components/DictionaryManageDialog.vue new file mode 100644 index 0000000000..fa8d5bd689 --- /dev/null +++ b/src/components/DictionaryManageDialog.vue @@ -0,0 +1,820 @@ + + + + + + + 読み方&アクセント辞書 + + + + + + + + + + 読み込み中・・・ + + + + + + 単語一覧 + + 削除 + + + 編集 + + + 追加 + + + + + + {{ + value.surface + }} + {{ value.yomi }} + + + + + + + + + 単語 + + + + 読み + + + 読みに使える文字はひらがなとカタカナのみです。 + + + + アクセント調整 + + 語尾のアクセントを考慮するため、「が」が自動で挿入されます。 + + + + + + + + + + + + {{ mora.text }} + + + + + + + + + リセット + 保存 + キャンセル + + + + + + + + + + + diff --git a/src/components/HowToUse.vue b/src/components/HowToUse.vue index 33930f83f5..4effdb2397 100644 --- a/src/components/HowToUse.vue +++ b/src/components/HowToUse.vue @@ -34,5 +34,6 @@ export default defineComponent({ .markdown :deep(img) { border: 1px solid #333; vertical-align: middle; + margin-bottom: 1rem; } diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue index 6bae86c64e..9e1b696c55 100644 --- a/src/components/MenuBar.vue +++ b/src/components/MenuBar.vue @@ -22,7 +22,8 @@ (isEdited ? "*" : "") + (projectName !== undefined ? projectName + " - " : "") + "VOICEVOX" + - (currentVersion ? " - Ver. " + currentVersion : "") + (currentVersion ? " - Ver. " + currentVersion + " - " : "") + + (useGpu ? "GPU" : "CPU") }} @@ -91,6 +92,7 @@ export default defineComponent({ const uiLocked = computed(() => store.getters.UI_LOCKED); const menubarLocked = computed(() => store.getters.MENUBAR_LOCKED); const projectName = computed(() => store.getters.PROJECT_NAME); + const useGpu = store.state.useGpu; const isEdited = computed(() => store.getters.IS_EDITED); const isFullscreen = computed(() => store.getters.IS_FULLSCREEN); @@ -181,6 +183,9 @@ export default defineComponent({ store.dispatch("IS_TOOLBAR_SETTING_DIALOG_OPEN", { isToolbarSettingDialogOpen: false, }); + store.dispatch("IS_CHARACTER_ORDER_DIALOG_OPEN", { + isCharacterOrderDialogOpen: false, + }); store.dispatch("IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN", { isDefaultStyleSelectDialogOpen: false, }); @@ -300,13 +305,31 @@ export default defineComponent({ }, { type: "button", - label: "デフォルトスタイル・試聴", + label: "キャラクター並び替え・試聴", + onClick() { + store.dispatch("IS_CHARACTER_ORDER_DIALOG_OPEN", { + isCharacterOrderDialogOpen: true, + }); + }, + }, + { + type: "button", + label: "デフォルトスタイル", onClick() { store.dispatch("IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN", { isDefaultStyleSelectDialogOpen: true, }); }, }, + { + type: "button", + label: "読み方&アクセント辞書", + onClick() { + store.dispatch("IS_DICTIONARY_MANAGE_DIALOG_OPEN", { + isDictionaryManageDialogOpen: true, + }); + }, + }, { type: "separator" }, { type: "button", @@ -387,6 +410,7 @@ export default defineComponent({ subMenuOpenFlags, reassignSubMenuOpen, menudata, + useGpu, }; }, }); diff --git a/src/components/SettingDialog.vue b/src/components/SettingDialog.vue index db7a595698..dc90fb9985 100644 --- a/src/components/SettingDialog.vue +++ b/src/components/SettingDialog.vue @@ -433,26 +433,6 @@ - - テキスト欄の並び替え - - - - テキスト欄をドラッグ&ドロップで並び替える. - - - diff --git a/src/components/UpdateInfo.vue b/src/components/UpdateInfo.vue index 25ab63ed30..ec9df51043 100644 --- a/src/components/UpdateInfo.vue +++ b/src/components/UpdateInfo.vue @@ -13,19 +13,74 @@ import { useStore } from "@/store"; import { computed, defineComponent, ref } from "@vue/runtime-core"; import { UpdateInfo } from "../type/preload"; +import semver from "semver"; export default defineComponent({ setup() { const store = useStore(); - const infos = ref(); - store.dispatch("GET_UPDATE_INFOS").then((obj) => (infos.value = obj)); + const updateInfos = ref(); + store.dispatch("GET_UPDATE_INFOS").then((obj) => (updateInfos.value = obj)); + + let isCheckingFinished = ref(false); + + // 最新版があるか調べる + const currentVersion = ref(""); + const latestVersion = ref(""); + window.electron + .getAppInfos() + .then((obj) => { + currentVersion.value = obj.version; + }) + .then(() => { + fetch("https://api.github.com/repos/VOICEVOX/voicevox/releases", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + }) + .then((response) => { + if (!response.ok) throw new Error("Network response was not ok."); + return response.json(); + }) + .then((json) => { + const obj = json.find( + (item: { prerelease: boolean; tag_name: string }) => { + return ( + !item.prerelease && + semver.valid(currentVersion.value) && + semver.valid(item.tag_name) && + semver.lt(currentVersion.value, item.tag_name) + ); + } + ); + obj ? (latestVersion.value = obj.tag_name) : undefined; + isCheckingFinished.value = true; + }) + .catch((err) => { + throw new Error(err); + }); + }); + + const isUpdateAvailable = computed(() => { + return isCheckingFinished.value && latestVersion.value !== ""; + }); const html = computed(() => { - if (!infos.value) return ""; + if (!updateInfos.value) return ""; let html = ""; - for (const info of infos.value) { + + if (isUpdateAvailable.value) { + html += `最新バージョン ${latestVersion.value} が見つかりました`; + html += `ダウンロードページ`; + } + + html += ``; + html += `アップデート履歴`; + + for (const info of updateInfos.value) { const version: string = info.version; const descriptions: string[] = info.descriptions; const contributors: string[] = info.contributors; diff --git a/src/electron/preload.ts b/src/electron/preload.ts index 1d82ac481d..a0774a7283 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -192,6 +192,10 @@ const api: Sandbox = { return ipcRenderer.invoke("LOG_INFO", ...params); }, + engineInfos: () => { + return ipcRendererInvoke("ENGINE_INFOS"); + }, + restartEngine: () => { return ipcRendererInvoke("RESTART_ENGINE"); }, @@ -220,6 +224,14 @@ const api: Sandbox = { return ipcRenderer.invoke("TOOLBAR_SETTING", { newData }); }, + getUserCharacterOrder: async () => { + return await ipcRendererInvoke("GET_USER_CHARACTER_ORDER"); + }, + + setUserCharacterOrder: async (userCharacterOrder) => { + await ipcRendererInvoke("SET_USER_CHARACTER_ORDER", userCharacterOrder); + }, + isUnsetDefaultStyleId: async (speakerUuid: string) => { return await ipcRendererInvoke("IS_UNSET_DEFAULT_STYLE_ID", speakerUuid); }, diff --git a/src/openapi/.openapi-generator/FILES b/src/openapi/.openapi-generator/FILES index c2795e12b5..9a07f61461 100644 --- a/src/openapi/.openapi-generator/FILES +++ b/src/openapi/.openapi-generator/FILES @@ -11,6 +11,8 @@ models/Speaker.ts models/SpeakerInfo.ts models/SpeakerStyle.ts models/StyleInfo.ts +models/SupportedDevicesInfo.ts +models/UserDictWord.ts models/ValidationError.ts models/index.ts runtime.ts diff --git a/src/openapi/apis/DefaultApi.ts b/src/openapi/apis/DefaultApi.ts index 362d3dc035..9ade6cfab0 100644 --- a/src/openapi/apis/DefaultApi.ts +++ b/src/openapi/apis/DefaultApi.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). @@ -36,22 +36,37 @@ import { SpeakerInfo, SpeakerInfoFromJSON, SpeakerInfoToJSON, + SupportedDevicesInfo, + SupportedDevicesInfoFromJSON, + SupportedDevicesInfoToJSON, + UserDictWord, + UserDictWordFromJSON, + UserDictWordToJSON, } from '../models'; export interface AccentPhrasesAccentPhrasesPostRequest { text: string; speaker: number; isKana?: boolean; + coreVersion?: string; +} + +export interface AddUserDictWordUserDictWordPostRequest { + surface: string; + pronunciation: string; + accentType: number; } export interface AudioQueryAudioQueryPostRequest { text: string; speaker: number; + coreVersion?: string; } export interface AudioQueryFromPresetAudioQueryFromPresetPostRequest { text: string; presetId: number; + coreVersion?: string; } export interface CancellableSynthesisCancellableSynthesisPostRequest { @@ -63,28 +78,52 @@ export interface ConnectWavesConnectWavesPostRequest { requestBody: Array; } +export interface DeleteUserDictWordUserDictWordWordUuidDeleteRequest { + wordUuid: string; +} + export interface MoraDataMoraDataPostRequest { speaker: number; accentPhrase: Array; + coreVersion?: string; } export interface MoraLengthMoraLengthPostRequest { speaker: number; accentPhrase: Array; + coreVersion?: string; } export interface MoraPitchMoraPitchPostRequest { speaker: number; accentPhrase: Array; + coreVersion?: string; } export interface MultiSynthesisMultiSynthesisPostRequest { speaker: number; audioQuery: Array; + coreVersion?: string; +} + +export interface RewriteUserDictWordUserDictWordWordUuidPutRequest { + wordUuid: string; + surface: string; + pronunciation: string; + accentType: number; } export interface SpeakerInfoSpeakerInfoGetRequest { speakerUuid: string; + coreVersion?: string; +} + +export interface SpeakersSpeakersGetRequest { + coreVersion?: string; +} + +export interface SupportedDevicesSupportedDevicesGetRequest { + coreVersion?: string; } export interface SynthesisMorphingSynthesisMorphingPostRequest { @@ -92,12 +131,14 @@ export interface SynthesisMorphingSynthesisMorphingPostRequest { targetSpeaker: number; morphRate: number; audioQuery: AudioQuery; + coreVersion?: string; } export interface SynthesisSynthesisPostRequest { speaker: number; audioQuery: AudioQuery; enableInterrogativeUpspeak?: boolean; + coreVersion?: string; } /** @@ -113,6 +154,7 @@ export interface DefaultApiInterface { * @param {string} text * @param {number} speaker * @param {boolean} [isKana] + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -125,11 +167,30 @@ export interface DefaultApiInterface { */ accentPhrasesAccentPhrasesPost(requestParameters: AccentPhrasesAccentPhrasesPostRequest, initOverrides?: RequestInit): Promise>; + /** + * ユーザ辞書に言葉を追加します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) + * @summary Add User Dict Word + * @param {string} surface + * @param {string} pronunciation + * @param {number} accentType + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + addUserDictWordUserDictWordPostRaw(requestParameters: AddUserDictWordUserDictWordPostRequest, initOverrides?: RequestInit): Promise>; + + /** + * ユーザ辞書に言葉を追加します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) + * Add User Dict Word + */ + addUserDictWordUserDictWordPost(requestParameters: AddUserDictWordUserDictWordPostRequest, initOverrides?: RequestInit): Promise; + /** * クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 * @summary 音声合成用のクエリを作成する * @param {string} text * @param {number} speaker + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -147,6 +208,7 @@ export interface DefaultApiInterface { * @summary 音声合成用のクエリをプリセットを用いて作成する * @param {string} text * @param {number} presetId + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -191,6 +253,36 @@ export interface DefaultApiInterface { */ connectWavesConnectWavesPost(requestParameters: ConnectWavesConnectWavesPostRequest, initOverrides?: RequestInit): Promise; + /** + * + * @summary Core Versions + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + coreVersionsCoreVersionsGetRaw(initOverrides?: RequestInit): Promise>>; + + /** + * Core Versions + */ + coreVersionsCoreVersionsGet(initOverrides?: RequestInit): Promise>; + + /** + * ユーザ辞書に登録されている言葉を削除します。 Parameters ---------- word_uuid: str 削除する言葉のUUID + * @summary Delete User Dict Word + * @param {string} wordUuid + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + deleteUserDictWordUserDictWordWordUuidDeleteRaw(requestParameters: DeleteUserDictWordUserDictWordWordUuidDeleteRequest, initOverrides?: RequestInit): Promise>; + + /** + * ユーザ辞書に登録されている言葉を削除します。 Parameters ---------- word_uuid: str 削除する言葉のUUID + * Delete User Dict Word + */ + deleteUserDictWordUserDictWordWordUuidDelete(requestParameters: DeleteUserDictWordUserDictWordWordUuidDeleteRequest, initOverrides?: RequestInit): Promise; + /** * エンジンが保持しているプリセットの設定を返します Returns ------- presets: List[Preset] プリセットのリスト * @summary Get Presets @@ -206,11 +298,27 @@ export interface DefaultApiInterface { */ getPresetsPresetsGet(initOverrides?: RequestInit): Promise>; + /** + * ユーザ辞書に登録されている単語の一覧を返します。 単語の表層形(surface)は正規化済みの物を返します。 Returns ------- Dict[str, UserDictWord] 単語のUUIDとその詳細 + * @summary Get User Dict Words + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + getUserDictWordsUserDictGetRaw(initOverrides?: RequestInit): Promise>; + + /** + * ユーザ辞書に登録されている単語の一覧を返します。 単語の表層形(surface)は正規化済みの物を返します。 Returns ------- Dict[str, UserDictWord] 単語のUUIDとその詳細 + * Get User Dict Words + */ + getUserDictWordsUserDictGet(initOverrides?: RequestInit): Promise<{ [key: string]: UserDictWord; }>; + /** * * @summary アクセント句から音高・音素長を得る * @param {number} speaker * @param {Array} accentPhrase + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -227,6 +335,7 @@ export interface DefaultApiInterface { * @summary アクセント句から音素長を得る * @param {number} speaker * @param {Array} accentPhrase + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -243,6 +352,7 @@ export interface DefaultApiInterface { * @summary アクセント句から音高を得る * @param {number} speaker * @param {Array} accentPhrase + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -259,6 +369,7 @@ export interface DefaultApiInterface { * @summary 複数まとめて音声合成する * @param {number} speaker * @param {Array} audioQuery + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -270,10 +381,30 @@ export interface DefaultApiInterface { */ multiSynthesisMultiSynthesisPost(requestParameters: MultiSynthesisMultiSynthesisPostRequest, initOverrides?: RequestInit): Promise; + /** + * ユーザ辞書に登録されている言葉を更新します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) word_uuid: str 更新する言葉のUUID + * @summary Rewrite User Dict Word + * @param {string} wordUuid + * @param {string} surface + * @param {string} pronunciation + * @param {number} accentType + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + rewriteUserDictWordUserDictWordWordUuidPutRaw(requestParameters: RewriteUserDictWordUserDictWordWordUuidPutRequest, initOverrides?: RequestInit): Promise>; + + /** + * ユーザ辞書に登録されている言葉を更新します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) word_uuid: str 更新する言葉のUUID + * Rewrite User Dict Word + */ + rewriteUserDictWordUserDictWordWordUuidPut(requestParameters: RewriteUserDictWordUserDictWordWordUuidPutRequest, initOverrides?: RequestInit): Promise; + /** * 指定されたspeaker_uuidに関する情報をjson形式で返します。 画像や音声はbase64エンコードされたものが返されます。 Returns ------- ret_data: SpeakerInfo * @summary Speaker Info * @param {string} speakerUuid + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -289,16 +420,32 @@ export interface DefaultApiInterface { /** * * @summary Speakers + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface */ - speakersSpeakersGetRaw(initOverrides?: RequestInit): Promise>>; + speakersSpeakersGetRaw(requestParameters: SpeakersSpeakersGetRequest, initOverrides?: RequestInit): Promise>>; /** * Speakers */ - speakersSpeakersGet(initOverrides?: RequestInit): Promise>; + speakersSpeakersGet(requestParameters: SpeakersSpeakersGetRequest, initOverrides?: RequestInit): Promise>; + + /** + * + * @summary Supported Devices + * @param {string} [coreVersion] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + supportedDevicesSupportedDevicesGetRaw(requestParameters: SupportedDevicesSupportedDevicesGetRequest, initOverrides?: RequestInit): Promise>; + + /** + * Supported Devices + */ + supportedDevicesSupportedDevicesGet(requestParameters: SupportedDevicesSupportedDevicesGetRequest, initOverrides?: RequestInit): Promise; /** * 指定された2人の話者で音声を合成、指定した割合でモーフィングした音声を得ます。 モーフィングの割合は`morph_rate`で指定でき、0.0でベースの話者、1.0でターゲットの話者に近づきます。 @@ -307,6 +454,7 @@ export interface DefaultApiInterface { * @param {number} targetSpeaker * @param {number} morphRate * @param {AudioQuery} audioQuery + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -325,6 +473,7 @@ export interface DefaultApiInterface { * @param {number} speaker * @param {AudioQuery} audioQuery * @param {boolean} [enableInterrogativeUpspeak] 疑問系のテキストが与えられたら語尾を自動調整する + * @param {string} [coreVersion] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApiInterface @@ -384,6 +533,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['is_kana'] = requestParameters.isKana; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ @@ -405,6 +558,58 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { return await response.value(); } + /** + * ユーザ辞書に言葉を追加します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) + * Add User Dict Word + */ + async addUserDictWordUserDictWordPostRaw(requestParameters: AddUserDictWordUserDictWordPostRequest, initOverrides?: RequestInit): Promise> { + if (requestParameters.surface === null || requestParameters.surface === undefined) { + throw new runtime.RequiredError('surface','Required parameter requestParameters.surface was null or undefined when calling addUserDictWordUserDictWordPost.'); + } + + if (requestParameters.pronunciation === null || requestParameters.pronunciation === undefined) { + throw new runtime.RequiredError('pronunciation','Required parameter requestParameters.pronunciation was null or undefined when calling addUserDictWordUserDictWordPost.'); + } + + if (requestParameters.accentType === null || requestParameters.accentType === undefined) { + throw new runtime.RequiredError('accentType','Required parameter requestParameters.accentType was null or undefined when calling addUserDictWordUserDictWordPost.'); + } + + const queryParameters: any = {}; + + if (requestParameters.surface !== undefined) { + queryParameters['surface'] = requestParameters.surface; + } + + if (requestParameters.pronunciation !== undefined) { + queryParameters['pronunciation'] = requestParameters.pronunciation; + } + + if (requestParameters.accentType !== undefined) { + queryParameters['accent_type'] = requestParameters.accentType; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/user_dict_word`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.TextApiResponse(response) as any; + } + + /** + * ユーザ辞書に言葉を追加します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) + * Add User Dict Word + */ + async addUserDictWordUserDictWordPost(requestParameters: AddUserDictWordUserDictWordPostRequest, initOverrides?: RequestInit): Promise { + const response = await this.addUserDictWordUserDictWordPostRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * クエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。 * 音声合成用のクエリを作成する @@ -428,6 +633,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker'] = requestParameters.speaker; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ @@ -472,6 +681,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['preset_id'] = requestParameters.presetId; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ @@ -569,6 +782,63 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { return await response.value(); } + /** + * Core Versions + */ + async coreVersionsCoreVersionsGetRaw(initOverrides?: RequestInit): Promise>> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/core_versions`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Core Versions + */ + async coreVersionsCoreVersionsGet(initOverrides?: RequestInit): Promise> { + const response = await this.coreVersionsCoreVersionsGetRaw(initOverrides); + return await response.value(); + } + + /** + * ユーザ辞書に登録されている言葉を削除します。 Parameters ---------- word_uuid: str 削除する言葉のUUID + * Delete User Dict Word + */ + async deleteUserDictWordUserDictWordWordUuidDeleteRaw(requestParameters: DeleteUserDictWordUserDictWordWordUuidDeleteRequest, initOverrides?: RequestInit): Promise> { + if (requestParameters.wordUuid === null || requestParameters.wordUuid === undefined) { + throw new runtime.RequiredError('wordUuid','Required parameter requestParameters.wordUuid was null or undefined when calling deleteUserDictWordUserDictWordWordUuidDelete.'); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/user_dict_word/{word_uuid}`.replace(`{${"word_uuid"}}`, encodeURIComponent(String(requestParameters.wordUuid))), + method: 'DELETE', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * ユーザ辞書に登録されている言葉を削除します。 Parameters ---------- word_uuid: str 削除する言葉のUUID + * Delete User Dict Word + */ + async deleteUserDictWordUserDictWordWordUuidDelete(requestParameters: DeleteUserDictWordUserDictWordWordUuidDeleteRequest, initOverrides?: RequestInit): Promise { + await this.deleteUserDictWordUserDictWordWordUuidDeleteRaw(requestParameters, initOverrides); + } + /** * エンジンが保持しているプリセットの設定を返します Returns ------- presets: List[Preset] プリセットのリスト * Get Presets @@ -597,6 +867,34 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { return await response.value(); } + /** + * ユーザ辞書に登録されている単語の一覧を返します。 単語の表層形(surface)は正規化済みの物を返します。 Returns ------- Dict[str, UserDictWord] 単語のUUIDとその詳細 + * Get User Dict Words + */ + async getUserDictWordsUserDictGetRaw(initOverrides?: RequestInit): Promise> { + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/user_dict`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => runtime.mapValues(jsonValue, UserDictWordFromJSON)); + } + + /** + * ユーザ辞書に登録されている単語の一覧を返します。 単語の表層形(surface)は正規化済みの物を返します。 Returns ------- Dict[str, UserDictWord] 単語のUUIDとその詳細 + * Get User Dict Words + */ + async getUserDictWordsUserDictGet(initOverrides?: RequestInit): Promise<{ [key: string]: UserDictWord; }> { + const response = await this.getUserDictWordsUserDictGetRaw(initOverrides); + return await response.value(); + } + /** * アクセント句から音高・音素長を得る */ @@ -615,6 +913,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker'] = requestParameters.speaker; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; @@ -656,6 +958,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker'] = requestParameters.speaker; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; @@ -697,6 +1003,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker'] = requestParameters.speaker; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; @@ -738,6 +1048,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker'] = requestParameters.speaker; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; @@ -761,6 +1075,61 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { return await response.value(); } + /** + * ユーザ辞書に登録されている言葉を更新します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) word_uuid: str 更新する言葉のUUID + * Rewrite User Dict Word + */ + async rewriteUserDictWordUserDictWordWordUuidPutRaw(requestParameters: RewriteUserDictWordUserDictWordWordUuidPutRequest, initOverrides?: RequestInit): Promise> { + if (requestParameters.wordUuid === null || requestParameters.wordUuid === undefined) { + throw new runtime.RequiredError('wordUuid','Required parameter requestParameters.wordUuid was null or undefined when calling rewriteUserDictWordUserDictWordWordUuidPut.'); + } + + if (requestParameters.surface === null || requestParameters.surface === undefined) { + throw new runtime.RequiredError('surface','Required parameter requestParameters.surface was null or undefined when calling rewriteUserDictWordUserDictWordWordUuidPut.'); + } + + if (requestParameters.pronunciation === null || requestParameters.pronunciation === undefined) { + throw new runtime.RequiredError('pronunciation','Required parameter requestParameters.pronunciation was null or undefined when calling rewriteUserDictWordUserDictWordWordUuidPut.'); + } + + if (requestParameters.accentType === null || requestParameters.accentType === undefined) { + throw new runtime.RequiredError('accentType','Required parameter requestParameters.accentType was null or undefined when calling rewriteUserDictWordUserDictWordWordUuidPut.'); + } + + const queryParameters: any = {}; + + if (requestParameters.surface !== undefined) { + queryParameters['surface'] = requestParameters.surface; + } + + if (requestParameters.pronunciation !== undefined) { + queryParameters['pronunciation'] = requestParameters.pronunciation; + } + + if (requestParameters.accentType !== undefined) { + queryParameters['accent_type'] = requestParameters.accentType; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/user_dict_word/{word_uuid}`.replace(`{${"word_uuid"}}`, encodeURIComponent(String(requestParameters.wordUuid))), + method: 'PUT', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * ユーザ辞書に登録されている言葉を更新します。 Parameters ---------- surface : str 言葉の表層形 pronunciation: str 言葉の発音(カタカナ) accent_type: int アクセント型(音が下がる場所を指す) word_uuid: str 更新する言葉のUUID + * Rewrite User Dict Word + */ + async rewriteUserDictWordUserDictWordWordUuidPut(requestParameters: RewriteUserDictWordUserDictWordWordUuidPutRequest, initOverrides?: RequestInit): Promise { + await this.rewriteUserDictWordUserDictWordWordUuidPutRaw(requestParameters, initOverrides); + } + /** * 指定されたspeaker_uuidに関する情報をjson形式で返します。 画像や音声はbase64エンコードされたものが返されます。 Returns ------- ret_data: SpeakerInfo * Speaker Info @@ -776,6 +1145,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['speaker_uuid'] = requestParameters.speakerUuid; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ @@ -800,9 +1173,13 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { /** * Speakers */ - async speakersSpeakersGetRaw(initOverrides?: RequestInit): Promise>> { + async speakersSpeakersGetRaw(requestParameters: SpeakersSpeakersGetRequest, initOverrides?: RequestInit): Promise>> { const queryParameters: any = {}; + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; const response = await this.request({ @@ -818,8 +1195,38 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { /** * Speakers */ - async speakersSpeakersGet(initOverrides?: RequestInit): Promise> { - const response = await this.speakersSpeakersGetRaw(initOverrides); + async speakersSpeakersGet(requestParameters: SpeakersSpeakersGetRequest, initOverrides?: RequestInit): Promise> { + const response = await this.speakersSpeakersGetRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Supported Devices + */ + async supportedDevicesSupportedDevicesGetRaw(requestParameters: SupportedDevicesSupportedDevicesGetRequest, initOverrides?: RequestInit): Promise> { + const queryParameters: any = {}; + + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + const response = await this.request({ + path: `/supported_devices`, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => SupportedDevicesInfoFromJSON(jsonValue)); + } + + /** + * Supported Devices + */ + async supportedDevicesSupportedDevicesGet(requestParameters: SupportedDevicesSupportedDevicesGetRequest, initOverrides?: RequestInit): Promise { + const response = await this.supportedDevicesSupportedDevicesGetRaw(requestParameters, initOverrides); return await response.value(); } @@ -858,6 +1265,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['morph_rate'] = requestParameters.morphRate; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; @@ -904,6 +1315,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { queryParameters['enable_interrogative_upspeak'] = requestParameters.enableInterrogativeUpspeak; } + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + const headerParameters: runtime.HTTPHeaders = {}; headerParameters['Content-Type'] = 'application/json'; diff --git a/src/openapi/models/AccentPhrase.ts b/src/openapi/models/AccentPhrase.ts index 4fee061836..1f84864ff5 100644 --- a/src/openapi/models/AccentPhrase.ts +++ b/src/openapi/models/AccentPhrase.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/AudioQuery.ts b/src/openapi/models/AudioQuery.ts index 9ca6010153..fdff626db7 100644 --- a/src/openapi/models/AudioQuery.ts +++ b/src/openapi/models/AudioQuery.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/HTTPValidationError.ts b/src/openapi/models/HTTPValidationError.ts index 4155858c26..68bcd071a5 100644 --- a/src/openapi/models/HTTPValidationError.ts +++ b/src/openapi/models/HTTPValidationError.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/Mora.ts b/src/openapi/models/Mora.ts index 8c516a54da..ee36c7cb3f 100644 --- a/src/openapi/models/Mora.ts +++ b/src/openapi/models/Mora.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/ParseKanaBadRequest.ts b/src/openapi/models/ParseKanaBadRequest.ts index 0499d7f58e..03f4b81cc6 100644 --- a/src/openapi/models/ParseKanaBadRequest.ts +++ b/src/openapi/models/ParseKanaBadRequest.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/Preset.ts b/src/openapi/models/Preset.ts index 68010a5963..95aa5f0d24 100644 --- a/src/openapi/models/Preset.ts +++ b/src/openapi/models/Preset.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/Speaker.ts b/src/openapi/models/Speaker.ts index dd29616783..938f31f5ec 100644 --- a/src/openapi/models/Speaker.ts +++ b/src/openapi/models/Speaker.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/SpeakerInfo.ts b/src/openapi/models/SpeakerInfo.ts index 8a81a7b215..534d9e91fc 100644 --- a/src/openapi/models/SpeakerInfo.ts +++ b/src/openapi/models/SpeakerInfo.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/SpeakerStyle.ts b/src/openapi/models/SpeakerStyle.ts index 144f80576d..796f08c24d 100644 --- a/src/openapi/models/SpeakerStyle.ts +++ b/src/openapi/models/SpeakerStyle.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/StyleInfo.ts b/src/openapi/models/StyleInfo.ts index 4b38a63c45..b1a3a39fda 100644 --- a/src/openapi/models/StyleInfo.ts +++ b/src/openapi/models/StyleInfo.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/SupportedDevicesInfo.ts b/src/openapi/models/SupportedDevicesInfo.ts new file mode 100644 index 0000000000..31028a224f --- /dev/null +++ b/src/openapi/models/SupportedDevicesInfo.ts @@ -0,0 +1,64 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * VOICEVOX ENGINE + * VOICEVOXの音声合成エンジンです。 + * + * The version of the OpenAPI document: 0.10.4 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * 対応しているデバイスの情報 + * @export + * @interface SupportedDevicesInfo + */ +export interface SupportedDevicesInfo { + /** + * + * @type {boolean} + * @memberof SupportedDevicesInfo + */ + cpu: boolean; + /** + * + * @type {boolean} + * @memberof SupportedDevicesInfo + */ + cuda: boolean; +} + +export function SupportedDevicesInfoFromJSON(json: any): SupportedDevicesInfo { + return SupportedDevicesInfoFromJSONTyped(json, false); +} + +export function SupportedDevicesInfoFromJSONTyped(json: any, ignoreDiscriminator: boolean): SupportedDevicesInfo { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'cpu': json['cpu'], + 'cuda': json['cuda'], + }; +} + +export function SupportedDevicesInfoToJSON(value?: SupportedDevicesInfo | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'cpu': value.cpu, + 'cuda': value.cuda, + }; +} + diff --git a/src/openapi/models/UserDictWord.ts b/src/openapi/models/UserDictWord.ts new file mode 100644 index 0000000000..d18407c386 --- /dev/null +++ b/src/openapi/models/UserDictWord.ts @@ -0,0 +1,160 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * VOICEVOX ENGINE + * VOICEVOXの音声合成エンジンです。 + * + * The version of the OpenAPI document: 0.10.4 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from '../runtime'; +/** + * 辞書のコンパイルに使われる情報 + * @export + * @interface UserDictWord + */ +export interface UserDictWord { + /** + * + * @type {string} + * @memberof UserDictWord + */ + surface: string; + /** + * + * @type {number} + * @memberof UserDictWord + */ + cost: number; + /** + * + * @type {string} + * @memberof UserDictWord + */ + partOfSpeech: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + partOfSpeechDetail1: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + partOfSpeechDetail2: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + partOfSpeechDetail3: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + inflectionalType: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + inflectionalForm: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + stem: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + yomi: string; + /** + * + * @type {string} + * @memberof UserDictWord + */ + pronunciation: string; + /** + * + * @type {number} + * @memberof UserDictWord + */ + accentType: number; + /** + * + * @type {number} + * @memberof UserDictWord + */ + moraCount?: number; + /** + * + * @type {string} + * @memberof UserDictWord + */ + accentAssociativeRule: string; +} + +export function UserDictWordFromJSON(json: any): UserDictWord { + return UserDictWordFromJSONTyped(json, false); +} + +export function UserDictWordFromJSONTyped(json: any, ignoreDiscriminator: boolean): UserDictWord { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'surface': json['surface'], + 'cost': json['cost'], + 'partOfSpeech': json['part_of_speech'], + 'partOfSpeechDetail1': json['part_of_speech_detail_1'], + 'partOfSpeechDetail2': json['part_of_speech_detail_2'], + 'partOfSpeechDetail3': json['part_of_speech_detail_3'], + 'inflectionalType': json['inflectional_type'], + 'inflectionalForm': json['inflectional_form'], + 'stem': json['stem'], + 'yomi': json['yomi'], + 'pronunciation': json['pronunciation'], + 'accentType': json['accent_type'], + 'moraCount': !exists(json, 'mora_count') ? undefined : json['mora_count'], + 'accentAssociativeRule': json['accent_associative_rule'], + }; +} + +export function UserDictWordToJSON(value?: UserDictWord | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'surface': value.surface, + 'cost': value.cost, + 'part_of_speech': value.partOfSpeech, + 'part_of_speech_detail_1': value.partOfSpeechDetail1, + 'part_of_speech_detail_2': value.partOfSpeechDetail2, + 'part_of_speech_detail_3': value.partOfSpeechDetail3, + 'inflectional_type': value.inflectionalType, + 'inflectional_form': value.inflectionalForm, + 'stem': value.stem, + 'yomi': value.yomi, + 'pronunciation': value.pronunciation, + 'accent_type': value.accentType, + 'mora_count': value.moraCount, + 'accent_associative_rule': value.accentAssociativeRule, + }; +} + diff --git a/src/openapi/models/ValidationError.ts b/src/openapi/models/ValidationError.ts index 99ee89df50..75f0a6197f 100644 --- a/src/openapi/models/ValidationError.ts +++ b/src/openapi/models/ValidationError.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/openapi/models/index.ts b/src/openapi/models/index.ts index cf6b9162c5..1915bba089 100644 --- a/src/openapi/models/index.ts +++ b/src/openapi/models/index.ts @@ -10,4 +10,6 @@ export * from './Speaker'; export * from './SpeakerInfo'; export * from './SpeakerStyle'; export * from './StyleInfo'; +export * from './SupportedDevicesInfo'; +export * from './UserDictWord'; export * from './ValidationError'; diff --git a/src/openapi/runtime.ts b/src/openapi/runtime.ts index 5a6eb75ddd..73ad1ea9b3 100644 --- a/src/openapi/runtime.ts +++ b/src/openapi/runtime.ts @@ -4,7 +4,7 @@ * VOICEVOX ENGINE * VOICEVOXの音声合成エンジンです。 * - * The version of the OpenAPI document: 0.10.2 + * The version of the OpenAPI document: 0.10.4 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/src/store/audio.ts b/src/store/audio.ts index 1118e260d6..4d49f88c7a 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -28,7 +28,13 @@ import { } from "@/type/preload"; import Encoding from "encoding-japanese"; import { PromiseType } from "./vuex"; -import { buildProjectFileName, sanitizeFileName } from "./utility"; +import { + buildProjectFileName, + convertHiraToKana, + convertLongVowel, + createKanaRegex, + sanitizeFileName, +} from "./utility"; async function generateUniqueIdAndQuery( state: State, @@ -59,17 +65,17 @@ async function generateUniqueIdAndQuery( function parseTextFile( body: string, defaultStyleIds: DefaultStyleId[], - characterInfos?: CharacterInfo[] + userOrderedCharacterInfos: CharacterInfo[] ): AudioItem[] { const characters = new Map(); { const uuid2StyleIds = new Map(); - for (const defaultStyleId of defaultStyleIds || []) { + for (const defaultStyleId of defaultStyleIds) { const speakerUuid = defaultStyleId.speakerUuid; const styleId = defaultStyleId.defaultStyleId; uuid2StyleIds.set(speakerUuid, styleId); } - for (const characterInfo of characterInfos || []) { + for (const characterInfo of userOrderedCharacterInfos) { const uuid = characterInfo.metas.speakerUuid; const styleId = uuid2StyleIds.get(uuid) ?? characterInfo.metas.styles[0].styleId; @@ -81,7 +87,7 @@ function parseTextFile( const audioItems: AudioItem[] = []; const seps = [",", "\r\n", "\n"]; - let lastStyleId = 0; + let lastStyleId = userOrderedCharacterInfos[0].metas.styles[0].styleId; for (const splittedText of body.split(new RegExp(`${seps.join("|")}`, "g"))) { const styleId = characters.get(splittedText); if (styleId !== undefined) { @@ -169,6 +175,14 @@ export const audioStore: VoiceVoxStoreOptions< ? audioElements[state._activeAudioKey]?.currentTime : undefined; }, + USER_ORDERED_CHARACTER_INFOS: (state) => { + const characterInfos = state.characterInfos?.slice(); + return characterInfos?.sort( + (a, b) => + state.userCharacterOrder.indexOf(a.metas.speakerUuid) - + state.userCharacterOrder.indexOf(b.metas.speakerUuid) + ); + }, }, mutations: { @@ -456,6 +470,10 @@ export const audioStore: VoiceVoxStoreOptions< actions: { START_WAITING_ENGINE: createUILockAction( async ({ state, commit, dispatch }) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + let engineState = state.engineState; for (let i = 0; i < 100; i++) { engineState = state.engineState; @@ -465,12 +483,14 @@ export const audioStore: VoiceVoxStoreOptions< try { await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "versionVersionGet", payload: [], }).then(toDispatchResponse("versionVersionGet")); } catch { await new Promise((resolve) => setTimeout(resolve, 1000)); - window.electron.logInfo("waiting engine..."); + + window.electron.logInfo(`Waiting engine ${engineInfo.key}`); continue; } engineState = "READY"; @@ -483,10 +503,16 @@ export const audioStore: VoiceVoxStoreOptions< } } ), - LOAD_CHARACTER: createUILockAction(async ({ commit, dispatch }) => { + LOAD_CHARACTER: createUILockAction(async ({ state, commit, dispatch }) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + const speakers = await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "speakersSpeakersGet", - payload: [], + // 連想配列が第一引数になければ失敗する + payload: [{}], }) .then(toDispatchResponse("speakersSpeakersGet")) .catch((error) => { @@ -521,7 +547,12 @@ export const audioStore: VoiceVoxStoreOptions< return styles; }; const getSpeakerInfo = async function (speaker: Speaker) { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + const speakerInfo = await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "speakerInfoSpeakerInfoGet", payload: [{ speakerUuid: speaker.speakerUuid }], }) @@ -574,16 +605,17 @@ export const audioStore: VoiceVoxStoreOptions< //baseAudioItemのうち、textとstyleIdは別途与えられるので引き継がない if (state.defaultStyleIds == undefined) throw new Error("state.defaultStyleIds == undefined"); - if (state.characterInfos == undefined) + if (getters.USER_ORDERED_CHARACTER_INFOS == undefined) throw new Error("state.characterInfos == undefined"); - const characterInfos = state.characterInfos; + const userOrderedCharacterInfos = getters.USER_ORDERED_CHARACTER_INFOS; const text = payload.text ?? ""; const styleId = payload.styleId ?? state.defaultStyleIds[ state.defaultStyleIds.findIndex( - (x) => x.speakerUuid === characterInfos[0].metas.speakerUuid // FIXME: defaultStyleIds内にspeakerUuidがない場合がある + (x) => + x.speakerUuid === userOrderedCharacterInfos[0].metas.speakerUuid // FIXME: defaultStyleIds内にspeakerUuidがない場合がある ) ].defaultStyleId; const baseAudioItem = payload.baseAudioItem; @@ -645,8 +677,17 @@ export const audioStore: VoiceVoxStoreOptions< ) { commit("SET_AUDIO_PLAY_START_POINT", { startPoint }); }, - async GET_AUDIO_CACHE({ state }, { audioKey }: { audioKey: string }) { + async GET_AUDIO_CACHE( + { state, dispatch }, + { audioKey }: { audioKey: string } + ) { const audioItem = state.audioItems[audioKey]; + return dispatch("GET_AUDIO_CACHE_FROM_AUDIO_ITEM", { audioItem }); + }, + async GET_AUDIO_CACHE_FROM_AUDIO_ITEM( + { state }, + { audioItem }: { audioItem: AudioItem } + ) { const [id] = await generateUniqueIdAndQuery(state, audioItem); if (Object.prototype.hasOwnProperty.call(audioBlobCache, id)) { @@ -662,7 +703,7 @@ export const audioStore: VoiceVoxStoreOptions< commit("SET_AUDIO_QUERY", payload); }, FETCH_ACCENT_PHRASES( - { dispatch }, + { state, dispatch }, { text, styleId, @@ -673,7 +714,12 @@ export const audioStore: VoiceVoxStoreOptions< isKana?: boolean; } ) { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + return dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "accentPhrasesAccentPhrasesPost", payload: [ { @@ -693,13 +739,18 @@ export const audioStore: VoiceVoxStoreOptions< }); }, FETCH_MORA_DATA( - { dispatch }, + { dispatch, state }, { accentPhrases, styleId, }: { accentPhrases: AccentPhrase[]; styleId: number } ) { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + return dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "moraDataMoraDataPost", payload: [{ accentPhrase: accentPhrases, speaker: styleId }], }) @@ -739,10 +790,15 @@ export const audioStore: VoiceVoxStoreOptions< return accentPhrases; }, FETCH_AUDIO_QUERY( - { dispatch }, + { dispatch, state }, { text, styleId }: { text: string; styleId: number } ) { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + return dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "audioQueryAudioQueryPost", payload: [ { @@ -845,8 +901,16 @@ export const audioStore: VoiceVoxStoreOptions< return offsets; }, CONNECT_AUDIO: createUILockAction( - async ({ dispatch }, { encodedBlobs }: { encodedBlobs: string[] }) => { + async ( + { dispatch, state }, + { encodedBlobs }: { encodedBlobs: string[] } + ) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + return dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "connectWavesConnectWavesPost", payload: [ { @@ -864,11 +928,20 @@ export const audioStore: VoiceVoxStoreOptions< }); } ), - GENERATE_AUDIO: createUILockAction( - async ({ dispatch, state }, { audioKey }: { audioKey: string }) => { - const audioItem: AudioItem = JSON.parse( - JSON.stringify(state.audioItems[audioKey]) - ); + async GENERATE_AUDIO( + { dispatch, state }, + { audioKey }: { audioKey: string } + ) { + const audioItem: AudioItem = JSON.parse( + JSON.stringify(state.audioItems[audioKey]) + ); + return dispatch("GENERATE_AUDIO_FROM_AUDIO_ITEM", { audioItem }); + }, + GENERATE_AUDIO_FROM_AUDIO_ITEM: createUILockAction( + async ({ dispatch, state }, { audioItem }: { audioItem: AudioItem }) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); const [id, audioQuery] = await generateUniqueIdAndQuery( state, @@ -880,6 +953,7 @@ export const audioStore: VoiceVoxStoreOptions< } return dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, action: "synthesisSynthesisPost", payload: [ { @@ -1192,13 +1266,8 @@ export const audioStore: VoiceVoxStoreOptions< } ), PLAY_AUDIO: createUILockAction( - async ( - { state, commit, dispatch }, - { audioKey }: { audioKey: string } - ) => { - const audioElem = audioElements[audioKey] as HTMLAudioElement & { - setSinkId(deviceID: string): Promise; // setSinkIdを認識してくれないため - }; + async ({ commit, dispatch }, { audioKey }: { audioKey: string }) => { + const audioElem = audioElements[audioKey]; audioElem.pause(); // 音声用意 @@ -1217,18 +1286,39 @@ export const audioStore: VoiceVoxStoreOptions< throw new Error(); } } - audioElem.src = URL.createObjectURL(blob); - const accentPhraseOffsets = await dispatch("GET_AUDIO_PLAY_OFFSETS", { + + return dispatch("PLAY_AUDIO_BLOB", { + audioBlob: blob, + audioElem, audioKey, }); - if (accentPhraseOffsets.length === 0) { - audioElem.currentTime = 0; - } else { - const startTime = accentPhraseOffsets[state.audioPlayStartPoint ?? 0]; - if (startTime === undefined) throw Error("startTime === undefined"); - // 小さい値が切り捨てられることでフォーカスされるアクセントフレーズが一瞬元に戻るので、 - // 再生に影響のない程度かつ切り捨てられない値を加算する - audioElem.currentTime = startTime + 10e-6; + } + ), + PLAY_AUDIO_BLOB: createUILockAction( + async ( + { state, commit, dispatch }, + { + audioBlob, + audioElem, + audioKey, + }: { audioBlob: Blob; audioElem: HTMLAudioElement; audioKey?: string } + ) => { + audioElem.src = URL.createObjectURL(audioBlob); + // 途中再生用の処理 + if (audioKey) { + const accentPhraseOffsets = await dispatch("GET_AUDIO_PLAY_OFFSETS", { + audioKey, + }); + if (accentPhraseOffsets.length === 0) { + audioElem.currentTime = 0; + } else { + const startTime = + accentPhraseOffsets[state.audioPlayStartPoint ?? 0]; + if (startTime === undefined) throw Error("startTime === undefined"); + // 小さい値が切り捨てられることでフォーカスされるアクセントフレーズが一瞬元に戻るので、 + // 再生に影響のない程度かつ切り捨てられない値を加算する + audioElem.currentTime = startTime + 10e-6; + } } audioElem @@ -1248,7 +1338,9 @@ export const audioStore: VoiceVoxStoreOptions< // 再生終了時にresolveされるPromiseを返す const played = async () => { - commit("SET_AUDIO_NOW_PLAYING", { audioKey, nowPlaying: true }); + if (audioKey) { + commit("SET_AUDIO_NOW_PLAYING", { audioKey, nowPlaying: true }); + } }; audioElem.addEventListener("play", played); @@ -1261,7 +1353,9 @@ export const audioStore: VoiceVoxStoreOptions< }).finally(async () => { audioElem.removeEventListener("play", played); audioElem.removeEventListener("pause", paused); - commit("SET_AUDIO_NOW_PLAYING", { audioKey, nowPlaying: false }); + if (audioKey) { + commit("SET_AUDIO_NOW_PLAYING", { audioKey, nowPlaying: false }); + } }); audioElem.play(); @@ -1632,28 +1726,24 @@ export const audioCommandStore: VoiceVoxStoreOptions< let newAccentPhrasesSegment: AccentPhrase[] | undefined = undefined; - // ひらがな(U+3041~U+3094)とカタカナ(U+30A1~U+30F4)と全角長音(U+30FC)のみで構成される場合、 - // 「読み仮名」としてこれを処理する - const kanaRegex = /^[\u3041-\u3094\u30A1-\u30F4\u30FC]+$/; + const kanaRegex = createKanaRegex(true); if (kanaRegex.test(newPronunciation)) { // ひらがなが混ざっている場合はカタカナに変換 - const katakana = newPronunciation.replace(/[\u3041-\u3094]/g, (s) => { - return String.fromCharCode(s.charCodeAt(0) + 0x60); - }); + const katakana = convertHiraToKana(newPronunciation); // 長音を適切な音に変換 - const pureKatakana = katakana - .replace(/(?<=[アカサタナハマヤラワャァガザダバパ]ー*)ー/g, "ア") - .replace(/(?<=[イキシチニヒミリィギジヂビピ]ー*)ー/g, "イ") - .replace(/(?<=[ウクスツヌフムユルュゥヴグズヅブプ]ー*)ー/g, "ウ") - .replace(/(?<=[エケセテネヘメレェゲゼデベペ]ー*)ー/g, "エ") - .replace(/(?<=[オコソトノホモヨロヲョォゴゾドボポ]ー*)ー/g, "オ") - .replace(/(?<=[ン]ー*)ー/g, "ン") - .replace(/(?<=[ッ]ー*)ー/g, "ッ"); - - // アクセントを末尾につけaccent phraseの生成をリクエスト + const pureKatakana = convertLongVowel(katakana); + + // アクセントを各句の末尾につける + // 文中に「?、」「、」がある場合は、そこで句切りとみなす + const pureKatakanaWithAccent = pureKatakana.replace( + /(?、|、|(?<=[^?、])$|?$)/g, + "'$1" + ); + + // accent phraseの生成をリクエスト // 判別できない読み仮名が混じっていた場合400エラーが帰るのでfallback newAccentPhrasesSegment = await dispatch("FETCH_ACCENT_PHRASES", { - text: pureKatakana + "'", + text: pureKatakanaWithAccent, styleId, isKana: true, }).catch( @@ -1801,7 +1891,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< }, COMMAND_IMPORT_FROM_FILE: createUILockAction( async ( - { state, commit, dispatch }, + { state, commit, dispatch, getters }, { filePath }: { filePath?: string } ) => { if (!filePath) { @@ -1826,10 +1916,12 @@ export const audioCommandStore: VoiceVoxStoreOptions< : undefined; } + if (!getters.USER_ORDERED_CHARACTER_INFOS) + throw new Error("USER_ORDERED_CHARACTER_INFOS == undefined"); for (const { text, styleId } of parseTextFile( body, state.defaultStyleIds, - state.characterInfos + getters.USER_ORDERED_CHARACTER_INFOS )) { //パラメータ引き継ぎがONの場合は話速等のパラメータを引き継いでテキスト欄を作成する //パラメータ引き継ぎがOFFの場合、baseAudioItemがundefinedになっているのでパラメータ引き継ぎは行われない @@ -2210,7 +2302,7 @@ export const audioCommandStore: VoiceVoxStoreOptions< }; // FIXME: ProxyStoreのactionとVuexの組み合わせでReturnValueの型付けが中途半端になり、Promiseになってしまっている -const toDispatchResponse = +export const toDispatchResponse = (_: T) => ( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/store/dictionary.ts b/src/store/dictionary.ts new file mode 100644 index 0000000000..8629ed44a4 --- /dev/null +++ b/src/store/dictionary.ts @@ -0,0 +1,107 @@ +import { UserDictWord } from "@/openapi"; +import { + DictionaryGetters, + DictionaryActions, + DictionaryMutations, + DictionaryStoreState, + VoiceVoxStoreOptions, +} from "@/store/type"; +import { toDispatchResponse } from "./audio"; + +export const dictionaryStoreState: DictionaryStoreState = {}; + +export const dictionaryStore: VoiceVoxStoreOptions< + DictionaryGetters, + DictionaryActions, + DictionaryMutations +> = { + getters: {}, + mutations: {}, + actions: { + LOAD_USER_DICT: async ({ state, dispatch }) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + const engineDict = await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, + action: "getUserDictWordsUserDictGet", + payload: [], + }).then(toDispatchResponse("getUserDictWordsUserDictGet")); + + // 50音順にソートするために、一旦arrayにする + const dictArray = Object.keys(engineDict).map((k) => { + return { key: k, ...engineDict[k] }; + }); + dictArray.sort((a, b) => { + if (a.yomi > b.yomi) { + return 1; + } else { + return -1; + } + }); + const dictEntries: [string, UserDictWord][] = dictArray.map((v) => { + const { key, ...newV } = v; + return [key, newV]; + }); + return Object.fromEntries(dictEntries); + }, + + ADD_WORD: async ( + { state, dispatch }, + { surface, pronunciation, accentType } + ) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, + action: "addUserDictWordUserDictWordPost", + payload: [ + { + surface, + pronunciation, + accentType, + }, + ], + }).then(toDispatchResponse("addUserDictWordUserDictWordPost")); + }, + + REWRITE_WORD: async ( + { state, dispatch }, + { wordUuid, surface, pronunciation, accentType } + ) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, + action: "rewriteUserDictWordUserDictWordWordUuidPut", + payload: [ + { + wordUuid, + surface, + pronunciation, + accentType, + }, + ], + }).then(toDispatchResponse("rewriteUserDictWordUserDictWordWordUuidPut")); + }, + + DELETE_WORD: async ({ state, dispatch }, { wordUuid }) => { + const engineInfo = state.engineInfos[0]; // TODO: 複数エンジン対応 + if (!engineInfo) + throw new Error(`No such engineInfo registered: index == 0`); + await dispatch("INVOKE_ENGINE_CONNECTOR", { + engineKey: engineInfo.key, + action: "deleteUserDictWordUserDictWordWordUuidDelete", + payload: [ + { + wordUuid, + }, + ], + }).then( + toDispatchResponse("deleteUserDictWordUserDictWordWordUuidDelete") + ); + }, + }, +}; diff --git a/src/store/index.ts b/src/store/index.ts index 93f3c38b54..dd25070060 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -14,11 +14,17 @@ import { VoiceVoxStoreOptions, } from "./type"; import { commandStoreState, commandStore } from "./command"; -import { audioStoreState, audioStore, audioCommandStore } from "./audio"; +import { + audioStoreState, + audioStore, + audioCommandStore, + audioCommandStoreState, +} from "./audio"; import { projectStoreState, projectStore } from "./project"; import { uiStoreState, uiStore } from "./ui"; import { settingStoreState, settingStore } from "./setting"; import { presetStoreState, presetStore } from "./preset"; +import { dictionaryStoreState, dictionaryStore } from "./dictionary"; import { proxyStore, proxyStoreState } from "./proxy"; import { DefaultStyleId } from "@/type/preload"; @@ -30,6 +36,7 @@ export const storeKey: InjectionKey< export const indexStoreState: IndexStoreState = { defaultStyleIds: [], + userCharacterOrder: [], }; export const indexStore: VoiceVoxStoreOptions< @@ -65,6 +72,9 @@ export const indexStore: VoiceVoxStoreOptions< } } }, + SET_USER_CHARACTER_ORDER(state, { userCharacterOrder }) { + state.userCharacterOrder = userCharacterOrder; + }, }, actions: { async GET_HOW_TO_USE_TEXT() { @@ -103,25 +113,51 @@ export const indexStore: VoiceVoxStoreOptions< LOG_INFO(_, ...params: unknown[]) { window.electron.logInfo(...params); }, + async LOAD_USER_CHARACTER_ORDER({ commit }) { + const userCharacterOrder = await window.electron.getUserCharacterOrder(); + commit("SET_USER_CHARACTER_ORDER", { userCharacterOrder }); + }, + async SET_USER_CHARACTER_ORDER({ commit }, userCharacterOrder) { + commit("SET_USER_CHARACTER_ORDER", { userCharacterOrder }); + await window.electron.setUserCharacterOrder(userCharacterOrder); + }, + GET_NEW_CHARACTERS({ state }) { + if (!state.characterInfos) throw new Error("characterInfos is undefined"); + + // キャラクター表示順序に含まれていなければ新規キャラとみなす + const allSpeakerUuid = state.characterInfos.map( + (characterInfo) => characterInfo.metas.speakerUuid + ); + const newSpeakerUuid = allSpeakerUuid.filter( + (speakerUuid) => !state.userCharacterOrder.includes(speakerUuid) + ); + return newSpeakerUuid; + }, async IS_UNSET_DEFAULT_STYLE_ID(_, { speakerUuid }) { return await window.electron.isUnsetDefaultStyleId(speakerUuid); }, async LOAD_DEFAULT_STYLE_IDS({ commit, state }) { - const storeDefaultStyleIds = await window.electron.getDefaultStyleIds(); - if (storeDefaultStyleIds.length === 0) { - const characterInfos = await state.characterInfos; - if (characterInfos == undefined) - throw new Error("state.characterInfos == undefined"); - const defaultStyleIds = characterInfos.map((info) => ({ + let defaultStyleIds = await window.electron.getDefaultStyleIds(); + + if (!state.characterInfos) throw new Error("characterInfos is undefined"); + + // デフォルトスタイルが設定されていない場合は0をセットする + // FIXME: 保存しているものとstateのものが異なってしまうので良くない。デフォルトスタイルが未設定の場合はAudioCellsを表示しないようにすべき + const unsetCharacterInfos = state.characterInfos.filter( + (characterInfo) => + !defaultStyleIds.some( + (styleId) => styleId.speakerUuid == characterInfo.metas.speakerUuid + ) + ); + defaultStyleIds = [ + ...defaultStyleIds, + ...unsetCharacterInfos.map((info) => ({ speakerUuid: info.metas.speakerUuid, defaultStyleId: info.metas.styles[0].styleId, - })); - commit("SET_DEFAULT_STYLE_IDS", { defaultStyleIds }); - } else { - commit("SET_DEFAULT_STYLE_IDS", { - defaultStyleIds: storeDefaultStyleIds, - }); - } + })), + ]; + + commit("SET_DEFAULT_STYLE_IDS", { defaultStyleIds }); }, async SET_DEFAULT_STYLE_IDS({ commit }, defaultStyleIds) { commit("SET_DEFAULT_STYLE_IDS", { defaultStyleIds }); @@ -142,7 +178,7 @@ export const indexStore: VoiceVoxStoreOptions< promises.push(dispatch("GET_ACCEPT_TERMS")); promises.push(dispatch("GET_EXPERIMENTAL_SETTING")); - Promise.all(promises).then(() => { + await Promise.all(promises).then(() => { dispatch("ON_VUEX_READY"); }); }, @@ -156,9 +192,10 @@ export const store = createStore({ ...commandStoreState, ...projectStoreState, ...settingStoreState, - ...audioCommandStore, + ...audioCommandStoreState, ...indexStoreState, ...presetStoreState, + ...dictionaryStoreState, ...proxyStoreState, }, @@ -169,6 +206,7 @@ export const store = createStore({ ...projectStore.getters, ...settingStore.getters, ...presetStore.getters, + ...dictionaryStore.getters, ...audioCommandStore.getters, ...indexStore.getters, ...proxyStore.getters, @@ -182,6 +220,7 @@ export const store = createStore({ ...settingStore.mutations, ...audioCommandStore.mutations, ...presetStore.mutations, + ...dictionaryStore.mutations, ...indexStore.mutations, ...proxyStore.mutations, }, @@ -194,6 +233,7 @@ export const store = createStore({ ...settingStore.actions, ...audioCommandStore.actions, ...presetStore.actions, + ...dictionaryStore.actions, ...indexStore.actions, ...proxyStore.actions, }, diff --git a/src/store/project.ts b/src/store/project.ts index fe7d3e55b6..b8f9cfabb8 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -7,6 +7,7 @@ import { ProjectMutations, VoiceVoxStoreOptions, } from "@/store/type"; +import semver from "semver"; import { buildProjectFileName } from "./utility"; import Ajv, { JTDDataType } from "ajv/dist/jtd"; @@ -108,18 +109,22 @@ export const projectStore: VoiceVoxStoreOptions< " The appVersion of the project file should be string" ); } - const appVersionList = versionTextParse(obj.appVersion); - const nowAppInfo = await window.electron.getAppInfos(); - const nowAppVersionList = versionTextParse(nowAppInfo.version); - if (appVersionList == null || nowAppVersionList == null) { + const projectAppVersion: string = obj.appVersion; + if (!semver.valid(projectAppVersion)) { throw new Error( projectFileErrorMsg + - ' An invalid appVersion format. The appVersion should be in the format "%d.%d.%d' + ` The app version of the project file "${projectAppVersion}" is invalid. The app version should be a string in semver format.` ); } + const semverSatisfiesOptions: semver.Options = { + includePrerelease: true, + }; + // Migration - if (baseVersionIsLow(appVersionList, [0, 4, 0])) { + if ( + semver.satisfies(projectAppVersion, "<0.4", semverSatisfiesOptions) + ) { for (const audioItemsKey in obj.audioItems) { if ("charactorIndex" in obj.audioItems[audioItemsKey]) { obj.audioItems[audioItemsKey].characterIndex = @@ -138,7 +143,9 @@ export const projectStore: VoiceVoxStoreOptions< } } - if (baseVersionIsLow(appVersionList, [0, 5, 0])) { + if ( + semver.satisfies(projectAppVersion, "<0.5", semverSatisfiesOptions) + ) { for (const audioItemsKey in obj.audioItems) { const audioItem = obj.audioItems[audioItemsKey]; if (audioItem.query != null) { @@ -184,7 +191,9 @@ export const projectStore: VoiceVoxStoreOptions< } } - if (baseVersionIsLow(appVersionList, [0, 7, 0])) { + if ( + semver.satisfies(projectAppVersion, "<0.7", semverSatisfiesOptions) + ) { for (const audioItemsKey in obj.audioItems) { const audioItem = obj.audioItems[audioItemsKey]; if (audioItem.characterIndex != null) { @@ -201,7 +210,9 @@ export const projectStore: VoiceVoxStoreOptions< } } - if (baseVersionIsLow(appVersionList, [0, 8, 0])) { + if ( + semver.satisfies(projectAppVersion, "<0.8", semverSatisfiesOptions) + ) { for (const audioItemsKey in obj.audioItems) { const audioItem = obj.audioItems[audioItemsKey]; if (audioItem.speaker !== null) { @@ -401,26 +412,3 @@ interface ProjectType { audioKeys: string[]; audioItems: Record; } - -export type VersionType = [number, number, number]; - -const versionTextParse = (appVersionText: string): VersionType | undefined => { - const textArray = appVersionText.split("."); - if (textArray.length !== 3) return undefined; - const appVersion = textArray.map(Number) as VersionType; - if (!appVersion.every((item) => Number.isInteger(item))) return undefined; - return appVersion; -}; - -const baseVersionIsLow = (base: VersionType, target: VersionType): boolean => { - let result = false; - for (let i = 0; i < 3; i++) { - if (base[i] > target[i]) { - break; - } else if (base[i] < target[i]) { - result = true; - break; - } - } - return result; -}; diff --git a/src/store/proxy.ts b/src/store/proxy.ts index 28ccfdfe53..c03f693a9c 100644 --- a/src/store/proxy.ts +++ b/src/store/proxy.ts @@ -23,8 +23,17 @@ const proxyStoreCreator = ( getters: {}, mutations: {}, actions: { - INVOKE_ENGINE_CONNECTOR({ rootState }, payload) { - const instance = _engineFactory.instance(rootState.engineHost); + INVOKE_ENGINE_CONNECTOR({ state }, payload) { + const engineKey = payload.engineKey; + const engineInfo = state.engineInfos.find( + (engineInfo) => engineInfo.key === engineKey + ); + if (!engineInfo) + throw new Error( + `No such engineInfo registered: engineKey == ${engineKey}` + ); + + const instance = _engineFactory.instance(engineInfo.host); const action = payload.action; const args = payload.payload; // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/src/store/setting.ts b/src/store/setting.ts index 97a8bfb535..b6d6e289e5 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -35,7 +35,7 @@ export const settingStoreState: SettingStoreState = { }, hotkeySettings: [], toolbarSetting: [], - engineHost: process.env.VUE_APP_ENGINE_URL as unknown as string, + engineInfos: [], themeSetting: { currentTheme: "Default", availableThemes: [], @@ -44,7 +44,6 @@ export const settingStoreState: SettingStoreState = { experimentalSetting: { enablePreset: false, enableInterrogativeUpspeak: false, - enableReorderCell: false, }, }; diff --git a/src/store/type.ts b/src/store/type.ts index ada12cdd0f..377f5b2758 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -6,7 +6,7 @@ import { StoreOptions, } from "./vuex"; import { Patch } from "immer"; -import { AccentPhrase, AudioQuery } from "@/openapi"; +import { AccentPhrase, AudioQuery, UserDictWord } from "@/openapi"; import { createCommandMutationTree, PayloadRecipeTree } from "./command"; import { CharacterInfo, @@ -24,6 +24,7 @@ import { UpdateInfo, Preset, ActivePointScrollMode, + EngineInfo, } from "@/type/preload"; import { IEngineConnectorFactory } from "@/infrastructures/EngineConnector"; import { QVueGlobals } from "quasar"; @@ -126,6 +127,10 @@ type AudioStoreTypes = { mutation: { characterInfos: CharacterInfo[] }; }; + USER_ORDERED_CHARACTER_INFOS: { + getter: CharacterInfo[] | undefined; + }; + GENERATE_AUDIO_KEY: { action(): string; }; @@ -199,6 +204,10 @@ type AudioStoreTypes = { action(payload: { audioKey: string }): Promise; }; + GET_AUDIO_CACHE_FROM_AUDIO_ITEM: { + action(payload: { audioItem: AudioItem }): Promise; + }; + SET_AUDIO_TEXT: { mutation: { audioKey: string; text: string }; }; @@ -298,7 +307,11 @@ type AudioStoreTypes = { }; GENERATE_AUDIO: { - action(payload: { audioKey: string }): Blob | null; + action(payload: { audioKey: string }): Promise; + }; + + GENERATE_AUDIO_FROM_AUDIO_ITEM: { + action(payload: { audioItem: AudioItem }): Blob | null; }; CONNECT_AUDIO: { @@ -331,6 +344,14 @@ type AudioStoreTypes = { action(payload: { audioKey: string }): boolean; }; + PLAY_AUDIO_BLOB: { + action(payload: { + audioBlob: Blob; + audioElem: HTMLAudioElement; + audioKey?: string; + }): boolean; + }; + STOP_AUDIO: { action(payload: { audioKey: string }): void; }; @@ -597,6 +618,7 @@ export type CommandActions = StoreType; export type IndexStoreState = { defaultStyleIds: DefaultStyleId[]; + userCharacterOrder: string[]; }; type IndexStoreTypes = { @@ -645,6 +667,19 @@ type IndexStoreTypes = { action(payload: DefaultStyleId[]): void; }; + LOAD_USER_CHARACTER_ORDER: { + action(): Promise; + }; + + SET_USER_CHARACTER_ORDER: { + mutation: { userCharacterOrder: string[] }; + action(payload: string[]): void; + }; + + GET_NEW_CHARACTERS: { + action(): string[]; + }; + SHOW_WARNING_DIALOG: { action(payload: { title: string; @@ -720,7 +755,7 @@ export type SettingStoreState = { savingSetting: SavingSetting; hotkeySettings: HotkeySetting[]; toolbarSetting: ToolbarSetting; - engineHost: string; + engineInfos: EngineInfo[]; themeSetting: ThemeSetting; acceptRetrieveTelemetry: AcceptRetrieveTelemetryStatus; experimentalSetting: ExperimentalSetting; @@ -810,12 +845,14 @@ export type UiStoreState = { activePointScrollMode: ActivePointScrollMode; isHelpDialogOpen: boolean; isSettingDialogOpen: boolean; + isCharacterOrderDialogOpen: boolean; isUpdateCheckDialogOpen: boolean; isDefaultStyleSelectDialogOpen: boolean; isHotkeySettingDialogOpen: boolean; isToolbarSettingDialogOpen: boolean; isAcceptRetrieveTelemetryDialogOpen: boolean; isAcceptTermsDialogOpen: boolean; + isDictionaryManageDialogOpen: boolean; isMaximized: boolean; isPinned: boolean; isAutoUpdateCheck: boolean; @@ -894,10 +931,20 @@ type UiStoreTypes = { action(payload: { isAcceptTermsDialogOpen: boolean }): void; }; + IS_DICTIONARY_MANAGE_DIALOG_OPEN: { + mutation: { isDictionaryManageDialogOpen: boolean }; + action(payload: { isDictionaryManageDialogOpen: boolean }): void; + }; + ON_VUEX_READY: { action(): void; }; + IS_CHARACTER_ORDER_DIALOG_OPEN: { + mutation: { isCharacterOrderDialogOpen: boolean }; + action(payload: { isCharacterOrderDialogOpen: boolean }): void; + }; + IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN: { mutation: { isDefaultStyleSelectDialogOpen: boolean }; action(payload: { isDefaultStyleSelectDialogOpen: boolean }): void; @@ -912,6 +959,12 @@ type UiStoreTypes = { action(payload: { useGpu: boolean }): void; }; + GET_ENGINE_INFOS: { + action(): void; + }; + + SET_ENGINE_INFOS: { mutation: { engineInfos: EngineInfo[] } }; + GET_INHERIT_AUDIOINFO: { action(): void; }; @@ -1022,6 +1075,40 @@ export type PresetGetters = StoreType; export type PresetMutations = StoreType; export type PresetActions = StoreType; +/* + * Dictionary Store Types + */ + +export type DictionaryStoreState = Record; + +type DictionaryStoreTypes = { + LOAD_USER_DICT: { + action(): Promise>; + }; + ADD_WORD: { + action(payload: { + surface: string; + pronunciation: string; + accentType: number; + }): Promise; + }; + REWRITE_WORD: { + action(payload: { + wordUuid: string; + surface: string; + pronunciation: string; + accentType: number; + }): Promise; + }; + DELETE_WORD: { + action(payload: { wordUuid: string }): Promise; + }; +}; + +export type DictionaryGetters = StoreType; +export type DictionaryMutations = StoreType; +export type DictionaryActions = StoreType; + /* * Setting Store Types */ @@ -1035,6 +1122,7 @@ export type IEngineConnectorFactoryActions = ReturnType< type IEngineConnectorFactoryActionsMapper = K extends keyof IEngineConnectorFactoryActions ? (payload: { + engineKey: string; action: K; payload: Parameters; }) => ReturnType @@ -1066,6 +1154,7 @@ export type State = AudioStoreState & SettingStoreState & UiStoreState & PresetStoreState & + DictionaryStoreState & ProxyStoreState; type AllStoreTypes = AudioStoreTypes & @@ -1076,6 +1165,7 @@ type AllStoreTypes = AudioStoreTypes & SettingStoreTypes & UiStoreTypes & PresetStoreTypes & + DictionaryStoreTypes & ProxyStoreTypes; export type AllGetters = StoreType; diff --git a/src/store/ui.ts b/src/store/ui.ts index 448c305999..0b139494e1 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -9,7 +9,7 @@ import { UiStoreState, VoiceVoxStoreOptions, } from "./type"; -import { ActivePointScrollMode } from "@/type/preload"; +import { ActivePointScrollMode, EngineInfo } from "@/type/preload"; export function createUILockAction( action: ( @@ -38,9 +38,11 @@ export const uiStoreState: UiStoreState = { isUpdateCheckDialogOpen: false, isHotkeySettingDialogOpen: false, isToolbarSettingDialogOpen: false, + isCharacterOrderDialogOpen: false, isDefaultStyleSelectDialogOpen: false, isAcceptRetrieveTelemetryDialogOpen: false, isAcceptTermsDialogOpen: false, + isDictionaryManageDialogOpen: false, isMaximized: false, isPinned: false, isFullscreen: false, @@ -104,6 +106,12 @@ export const uiStore: VoiceVoxStoreOptions = ) { state.isToolbarSettingDialogOpen = isToolbarSettingDialogOpen; }, + IS_CHARACTER_ORDER_DIALOG_OPEN( + state, + { isCharacterOrderDialogOpen }: { isCharacterOrderDialogOpen: boolean } + ) { + state.isCharacterOrderDialogOpen = isCharacterOrderDialogOpen; + }, IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN( state, { @@ -112,6 +120,14 @@ export const uiStore: VoiceVoxStoreOptions = ) { state.isDefaultStyleSelectDialogOpen = isDefaultStyleSelectDialogOpen; }, + IS_DICTIONARY_MANAGE_DIALOG_OPEN( + state, + { + isDictionaryManageDialogOpen, + }: { isDictionaryManageDialogOpen: boolean } + ) { + state.isDictionaryManageDialogOpen = isDictionaryManageDialogOpen; + }, IS_ACCEPT_RETRIEVE_TELEMETRY_DIALOG_OPEN( state, { isAcceptRetrieveTelemetryDialogOpen } @@ -125,6 +141,9 @@ export const uiStore: VoiceVoxStoreOptions = SET_USE_GPU(state, { useGpu }: { useGpu: boolean }) { state.useGpu = useGpu; }, + SET_ENGINE_INFOS(state, { engineInfos }: { engineInfos: EngineInfo[] }) { + state.engineInfos = engineInfos; + }, SET_INHERIT_AUDIOINFO( state, { inheritAudioInfo }: { inheritAudioInfo: boolean } @@ -277,6 +296,25 @@ export const uiStore: VoiceVoxStoreOptions = ON_VUEX_READY() { window.electron.vuexReady(); }, + async IS_CHARACTER_ORDER_DIALOG_OPEN( + { state, commit }, + { isCharacterOrderDialogOpen } + ) { + if (state.isCharacterOrderDialogOpen === isCharacterOrderDialogOpen) + return; + + if (isCharacterOrderDialogOpen) { + commit("LOCK_UI"); + commit("LOCK_MENUBAR"); + } else { + commit("UNLOCK_UI"); + commit("UNLOCK_MENUBAR"); + } + + commit("IS_CHARACTER_ORDER_DIALOG_OPEN", { + isCharacterOrderDialogOpen, + }); + }, async IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN( { state, commit }, { isDefaultStyleSelectDialogOpen } @@ -299,6 +337,25 @@ export const uiStore: VoiceVoxStoreOptions = isDefaultStyleSelectDialogOpen, }); }, + async IS_DICTIONARY_MANAGE_DIALOG_OPEN( + { state, commit }, + { isDictionaryManageDialogOpen } + ) { + if (state.isDictionaryManageDialogOpen === isDictionaryManageDialogOpen) + return; + + if (isDictionaryManageDialogOpen) { + commit("LOCK_UI"); + commit("LOCK_MENUBAR"); + } else { + commit("UNLOCK_UI"); + commit("UNLOCK_MENUBAR"); + } + + commit("IS_DICTIONARY_MANAGE_DIALOG_OPEN", { + isDictionaryManageDialogOpen, + }); + }, async IS_ACCEPT_RETRIEVE_TELEMETRY_DIALOG_OPEN( { state, commit }, { isAcceptRetrieveTelemetryDialogOpen } @@ -339,6 +396,11 @@ export const uiStore: VoiceVoxStoreOptions = useGpu: await window.electron.useGpu(useGpu), }); }, + async GET_ENGINE_INFOS({ commit }) { + commit("SET_ENGINE_INFOS", { + engineInfos: await window.electron.engineInfos(), + }); + }, async GET_INHERIT_AUDIOINFO({ commit }) { commit("SET_INHERIT_AUDIOINFO", { inheritAudioInfo: await window.electron.inheritAudioInfo(), diff --git a/src/store/utility.ts b/src/store/utility.ts index 56a0d80763..c9aad029b2 100644 --- a/src/store/utility.ts +++ b/src/store/utility.ts @@ -60,3 +60,32 @@ export const getToolbarButtonName = (tag: ToolbarButtonTagType): string => { }; return tag2NameObj[tag]; }; + +export const createKanaRegex = (includeSeparation?: boolean): RegExp => { + // 以下の文字のみで構成される場合、「読み仮名」としてこれを処理する + // includeSeparationがtrueの時は、読点(U+3001)とクエスチョン(U+FF1F)も含む + // * ひらがな(U+3041~U+3094) + // * カタカナ(U+30A1~U+30F4) + // * 全角長音(U+30FC) + if (includeSeparation) { + return /^[\u3041-\u3094\u30A1-\u30F4\u30FC\u3001\uFF1F]+$/; + } + return /^[\u3041-\u3094\u30A1-\u30F4\u30FC]+$/; +}; + +export const convertHiraToKana = (text: string): string => { + return text.replace(/[\u3041-\u3094]/g, (s) => { + return String.fromCharCode(s.charCodeAt(0) + 0x60); + }); +}; + +export const convertLongVowel = (text: string): string => { + return text + .replace(/(?<=[アカサタナハマヤラワャァガザダバパ]ー*)ー/g, "ア") + .replace(/(?<=[イキシチニヒミリィギジヂビピ]ー*)ー/g, "イ") + .replace(/(?<=[ウクスツヌフムユルュゥヴグズヅブプ]ー*)ー/g, "ウ") + .replace(/(?<=[エケセテネヘメレェゲゼデベペ]ー*)ー/g, "エ") + .replace(/(?<=[オコソトノホモヨロヲョォゴゾドボポ]ー*)ー/g, "オ") + .replace(/(?<=[ン]ー*)ー/g, "ン") + .replace(/(?<=[ッ]ー*)ー/g, "ッ"); +}; diff --git a/src/styles/_index.scss b/src/styles/_index.scss index 67ce15755e..415fc9df1a 100644 --- a/src/styles/_index.scss +++ b/src/styles/_index.scss @@ -16,16 +16,16 @@ img { // スクロールバーのデザイン ::-webkit-scrollbar { - width: 15px; - height: 15px; + width: 12px; + height: 12px; background-color: rgba(colors.$primary-light-rgb, 0.2); border-radius: 5px; } ::-webkit-scrollbar-thumb { - background-color: rgba(colors.$primary-light-rgb, 0.5); + background-color: rgba(colors.$primary-light-rgb, 0.6); border-radius: 5px; &:hover { - background-color: rgba(colors.$primary-light-rgb, 0.6); + background-color: rgba(colors.$primary-light-rgb, 0.7); } &:active { background-color: rgba(colors.$primary-light-rgb, 0.8); @@ -68,6 +68,13 @@ img { background-color: colors.$background; } +// 無効時の色 +.q-btn { + &.disabled { + opacity: 0.6 !important; + } +} + // ダイアログ .q-dialog, #q-loading { diff --git a/src/type/globals.d.ts b/src/type/globals.d.ts index 99f2ca085e..300dba2983 100644 --- a/src/type/globals.d.ts +++ b/src/type/globals.d.ts @@ -1,2 +1,8 @@ // Include global variables to build immer source code export * from "immer/src/types/globals"; + +declare global { + interface HTMLAudioElement { + setSinkId(deviceID: string): Promise; // setSinkIdを認識してくれないため + } +} diff --git a/src/type/ipc.d.ts b/src/type/ipc.d.ts index 545245b353..ba93c1f661 100644 --- a/src/type/ipc.d.ts +++ b/src/type/ipc.d.ts @@ -1,3 +1,5 @@ +import { EngineInfo } from "@/type/preload"; + /** * invoke, handle */ @@ -159,6 +161,11 @@ type IpcIHData = { return: void; }; + ENGINE_INFOS: { + args: []; + return: EngineInfo[]; + }; + RESTART_ENGINE: { args: []; return: void; @@ -201,6 +208,16 @@ type IpcIHData = { return: import("@/type/preload").ToolbarSetting; }; + GET_USER_CHARACTER_ORDER: { + args: []; + return: string[]; + }; + + SET_USER_CHARACTER_ORDER: { + args: [string[]]; + return: void; + }; + IS_UNSET_DEFAULT_STYLE_ID: { args: [speakerUuid: string]; return: boolean; diff --git a/src/type/preload.d.ts b/src/type/preload.d.ts index a8a43ff289..94d8bd31c0 100644 --- a/src/type/preload.d.ts +++ b/src/type/preload.d.ts @@ -58,6 +58,7 @@ export interface Sandbox { maximizeWindow(): void; logError(...params: unknown[]): void; logInfo(...params: unknown[]): void; + engineInfos(): Promise; restartEngine(): Promise; savingSetting(newData?: SavingSetting): Promise; hotkeySettings(newData?: HotkeySetting): Promise; @@ -68,6 +69,8 @@ export interface Sandbox { presetItems: Record; presetKeys: string[]; }): Promise; + getUserCharacterOrder(): Promise; + setUserCharacterOrder(userCharacterOrder: string[]): Promise; isUnsetDefaultStyleId(speakerUuid: string): Promise; getDefaultStyleIds(): Promise; setDefaultStyleIds( @@ -154,6 +157,13 @@ export type HotkeySetting = { combination: HotkeyCombo; }; +export type EngineInfo = { + key: string; + host: string; + executionEnabled: boolean; + executionFilePath: string; +}; + export type Preset = { name: string; speedScale: number; @@ -251,5 +261,4 @@ export type ThemeSetting = { export type ExperimentalSetting = { enablePreset: boolean; enableInterrogativeUpspeak: boolean; - enableReorderCell: boolean; }; diff --git a/src/views/Home.vue b/src/views/Home.vue index 9456ab3bfe..ae10f12190 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -69,31 +69,23 @@ " > - - - + + @@ -168,8 +166,10 @@ import HotkeySettingDialog from "@/components/HotkeySettingDialog.vue"; import HeaderBarCustomDialog from "@/components/HeaderBarCustomDialog.vue"; import CharacterPortrait from "@/components/CharacterPortrait.vue"; import DefaultStyleSelectDialog from "@/components/DefaultStyleSelectDialog.vue"; +import CharacterOrderDialog from "@/components/CharacterOrderDialog.vue"; import AcceptRetrieveTelemetryDialog from "@/components/AcceptRetrieveTelemetryDialog.vue"; import AcceptTermsDialog from "@/components/AcceptTermsDialog.vue"; +import DictionaryManageDialog from "@/components/DictionaryManageDialog.vue"; import { AudioItem } from "@/store/type"; import { QResizeObserver } from "quasar"; import path from "path"; @@ -192,8 +192,10 @@ export default defineComponent({ HeaderBarCustomDialog, CharacterPortrait, DefaultStyleSelectDialog, + CharacterOrderDialog, AcceptRetrieveTelemetryDialog, AcceptTermsDialog, + DictionaryManageDialog, }, setup() { @@ -315,10 +317,6 @@ export default defineComponent({ const resizeObserverRef = ref(); // DaD - const enableReorderCell = computed( - () => store.state.experimentalSetting.enableReorderCell - ); - const updateAudioKeys = (audioKeys: string[]) => store.dispatch("COMMAND_SET_AUDIO_KEYS", { audioKeys }); const itemKey = (key: string) => key; @@ -404,17 +402,26 @@ export default defineComponent({ // ソフトウェアを初期化 const isCompletedInitialStartup = ref(false); onMounted(async () => { + await store.dispatch("GET_ENGINE_INFOS"); + await store.dispatch("START_WAITING_ENGINE"); await store.dispatch("LOAD_CHARACTER"); + await store.dispatch("LOAD_USER_CHARACTER_ORDER"); await store.dispatch("LOAD_DEFAULT_STYLE_IDS"); + // 新キャラが追加されている場合はキャラ並び替えダイアログを表示 + const newCharacters = await store.dispatch("GET_NEW_CHARACTERS"); + isCharacterOrderDialogOpenComputed.value = newCharacters.length > 0; + + // スタイルが複数あって未選択なキャラがいる場合はデフォルトスタイル選択ダイアログを表示 let isUnsetDefaultStyleIds = false; if (characterInfos.value == undefined) throw new Error(); for (const info of characterInfos.value) { - isUnsetDefaultStyleIds ||= await store.dispatch( - "IS_UNSET_DEFAULT_STYLE_ID", - { speakerUuid: info.metas.speakerUuid } - ); + isUnsetDefaultStyleIds ||= + info.metas.styles.length > 1 && + (await store.dispatch("IS_UNSET_DEFAULT_STYLE_ID", { + speakerUuid: info.metas.speakerUuid, + })); } isDefaultStyleSelectDialogOpenComputed.value = isUnsetDefaultStyleIds; @@ -487,11 +494,23 @@ export default defineComponent({ }), }); - // デフォルトスタイル選択 + // キャラクター並び替え const characterInfos = computed(() => store.state.characterInfos); + const isCharacterOrderDialogOpenComputed = computed({ + get: () => + !store.state.isAcceptTermsDialogOpen && + store.state.isCharacterOrderDialogOpen, + set: (val) => + store.dispatch("IS_CHARACTER_ORDER_DIALOG_OPEN", { + isCharacterOrderDialogOpen: val, + }), + }); + + // デフォルトスタイル選択 const isDefaultStyleSelectDialogOpenComputed = computed({ get: () => !store.state.isAcceptTermsDialogOpen && + !store.state.isCharacterOrderDialogOpen && store.state.isDefaultStyleSelectDialogOpen, set: (val) => store.dispatch("IS_DEFAULT_STYLE_SELECT_DIALOG_OPEN", { @@ -499,9 +518,19 @@ export default defineComponent({ }), }); + // 読み方&アクセント辞書 + const isDictionaryManageDialogOpenComputed = computed({ + get: () => store.state.isDictionaryManageDialogOpen, + set: (val) => + store.dispatch("IS_DICTIONARY_MANAGE_DIALOG_OPEN", { + isDictionaryManageDialogOpen: val, + }), + }); + const isAcceptRetrieveTelemetryDialogOpenComputed = computed({ get: () => !store.state.isAcceptTermsDialogOpen && + !store.state.isCharacterOrderDialogOpen && !store.state.isDefaultStyleSelectDialogOpen && store.state.isAcceptRetrieveTelemetryDialogOpen, set: (val) => @@ -537,7 +566,6 @@ export default defineComponent({ uiLocked, addAudioCellRef, activeAudioKey, - enableReorderCell, itemKey, updateAudioKeys, addAudioItem, @@ -561,7 +589,9 @@ export default defineComponent({ isHotkeySettingDialogOpenComputed, isToolbarSettingDialogOpenComputed, characterInfos, + isCharacterOrderDialogOpenComputed, isDefaultStyleSelectDialogOpenComputed, + isDictionaryManageDialogOpenComputed, isAcceptRetrieveTelemetryDialogOpenComputed, isAcceptTermsDialogOpenComputed, dragEventCounter, @@ -640,6 +670,11 @@ export default defineComponent({ padding-bottom: 70px; } + + .draggable-cursor { + cursor: grab; + } + .add-button-wrapper { position: absolute; right: 0px; diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index d72b9a6f80..24cba36e34 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -10,6 +10,7 @@ import { settingStore } from "@/store/setting"; import { presetStore } from "@/store/preset"; import { assert } from "chai"; import { proxyStore } from "@/store/proxy"; +import { dictionaryStore } from "@/store/dictionary"; const isDevelopment = process.env.NODE_ENV == "development"; // TODO: Swap external files to Mock @@ -19,6 +20,7 @@ describe("store/vuex.js test", () => { state: { engineState: "STARTING", defaultStyleIds: [], + userCharacterOrder: [], audioItems: {}, audioKeys: [], audioStates: {}, @@ -36,7 +38,9 @@ describe("store/vuex.js test", () => { isUpdateCheckDialogOpen: false, isHotkeySettingDialogOpen: false, isToolbarSettingDialogOpen: false, + isCharacterOrderDialogOpen: false, isDefaultStyleSelectDialogOpen: false, + isDictionaryManageDialogOpen: false, isAcceptRetrieveTelemetryDialogOpen: false, isAcceptTermsDialogOpen: false, isMaximized: false, @@ -65,11 +69,17 @@ describe("store/vuex.js test", () => { toolbarSetting: [], acceptRetrieveTelemetry: "Unconfirmed", acceptTerms: "Unconfirmed", - engineHost: "http://127.0.0.1", + engineInfos: [ + { + key: "88022f86-c823-436e-85a3-500c629749c4", + executionEnabled: false, + executionFilePath: "", + host: "http://127.0.0.1", + }, + ], experimentalSetting: { enablePreset: false, enableInterrogativeUpspeak: false, - enableReorderCell: false, }, }, getters: { @@ -82,6 +92,7 @@ describe("store/vuex.js test", () => { ...indexStore.getters, ...presetStore.getters, ...proxyStore.getters, + ...dictionaryStore.getters, }, mutations: { ...uiStore.mutations, @@ -93,6 +104,7 @@ describe("store/vuex.js test", () => { ...indexStore.mutations, ...presetStore.mutations, ...proxyStore.mutations, + ...dictionaryStore.mutations, }, actions: { ...uiStore.actions, @@ -104,6 +116,7 @@ describe("store/vuex.js test", () => { ...indexStore.actions, ...presetStore.actions, ...proxyStore.actions, + ...dictionaryStore.actions, }, plugins: isDevelopment ? [createLogger()] : undefined, strict: process.env.NODE_ENV !== "production", @@ -134,7 +147,9 @@ describe("store/vuex.js test", () => { assert.equal(store.state.isSettingDialogOpen, false); assert.equal(store.state.isUpdateCheckDialogOpen, false); assert.equal(store.state.isHotkeySettingDialogOpen, false); + assert.equal(store.state.isCharacterOrderDialogOpen, false); assert.equal(store.state.isDefaultStyleSelectDialogOpen, false); + assert.equal(store.state.isDictionaryManageDialogOpen, false); assert.equal(store.state.isAcceptRetrieveTelemetryDialogOpen, false); assert.equal(store.state.isAcceptTermsDialogOpen, false); assert.equal(store.state.isMaximized, false); @@ -161,6 +176,5 @@ describe("store/vuex.js test", () => { store.state.experimentalSetting.enableInterrogativeUpspeak, false ); - assert.equal(store.state.experimentalSetting.enableReorderCell, false); }); });