diff --git a/.env.test b/.env.test index ab77dc1307..014c32203d 100644 --- a/.env.test +++ b/.env.test @@ -1,12 +1,14 @@ +# テスト用の.envファイル。モックを使う。 + VITE_APP_NAME=voicevox VITE_DEFAULT_ENGINE_INFOS=`[ { - "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", - "name": "VOICEVOX Engine", - "executionEnabled": true, - "executionFilePath": "../voicevox_engine/run.exe", + "name": "Mock Engine", + "uuid": "00000000-0000-0000-0000-000000000000", + "executionEnabled": false, + "executionFilePath": "dummy/path", "executionArgs": [], - "host": "http://127.0.0.1:50021" + "host": "mock://mock" } ]` VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ diff --git a/.github/actions/download-engine/action.yml b/.github/actions/download-engine/action.yml index 081dcc917b..fc4af5eaba 100644 --- a/.github/actions/download-engine/action.yml +++ b/.github/actions/download-engine/action.yml @@ -85,6 +85,13 @@ runs: mkdir -p $DEST mv $TEMPDIR/tmp-extract/$TARGET/* $DEST + # 実行ファイルのパーミッションを変更 + if [ "${{ runner.os }}" = "Windows" ]; then + chmod +x $DEST/run.exe + else + chmod +x $DEST/run + fi + echo "::group::ll $DEST" ls -al $DEST echo "::endgroup::" diff --git a/.github/workflows/merge_gatekeeper.yml b/.github/workflows/merge_gatekeeper.yml new file mode 100644 index 0000000000..d497e0d210 --- /dev/null +++ b/.github/workflows/merge_gatekeeper.yml @@ -0,0 +1,30 @@ +name: "Merge Gatekeeper" + +# auto mergeとmerge queue用のチェッカー。 +# Approve数が足りているか、すべてのテストが通っているかを確認します。 +# 詳細: https://github.com/VOICEVOX/merge-gatekeeper + +on: + pull_request_target: + types: [auto_merge_enabled] + merge_group: + types: [checks_requested] + +jobs: + merge_gatekeeper: + runs-on: ubuntu-latest + steps: + - uses: voicevox/merge-gatekeeper@main + with: + token: ${{ secrets.GATEKEEPER_TOKEN }} + required_score: 2 + score_rules: | + #maintainer: 2 + #reviewer: 1 + - uses: upsidr/merge-gatekeeper@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + self: merge_gatekeeper + # https://github.com/upsidr/merge-gatekeeper/issues/71#issuecomment-1660607977 + ref: ${{ github.event.pull_request && github.event.pull_request.head.sha || github.ref }} + timeout: 18000 # 5 hours diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de66c30720..5251fb0178 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,20 +98,6 @@ jobs: dest: ${{ github.workspace }}/voicevox_engine target: ${{ matrix.voicevox_engine_asset_name }} - - name: Setup - run: | - # run.exe - chmod +x ${{ steps.download-engine.outputs.run_path }} - - # .env - sed -i -e 's|"074fc39e-678b-4c13-8916-ffca8d505d1d"|"208cf94d-43d2-4cf5-abc0-9783cac36d29"|' .env.test - sed -i -e 's|"../voicevox_engine/run.exe"|"${{ steps.download-engine.outputs.run_path }}"|' .env.test - # GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする - PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})") - sed -i -e 's|"host": "http://127.0.0.1:50021"|"host": "http://127.0.0.1:'$PORT'"|' .env.test - sed -i -e 's|"executionArgs": \[\],|"executionArgs": ["--port='$PORT'"],|' .env.test - cp .env.test .env - - name: Run npm run test:browser-e2e run: | if [ -n "${{ runner.debug }}" ]; then @@ -125,6 +111,14 @@ jobs: - name: Run npm run test:electron-e2e run: | + # .env + cp tests/env/.env.test-electron .env + sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env + # GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする + PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})") + sed -i -e 's|random_port|'$PORT'|' .env + cat .env # ログ用 + if [ -n "${{ runner.debug }}" ]; then export DEBUG="pw:browser*" fi @@ -134,6 +128,8 @@ jobs: npm run test:electron-e2e fi + rm .env + - name: Run npm run test:storybook-vrt run: | if [ -n "${{ runner.debug }}" ]; then diff --git a/openapi.json b/openapi.json index b91b38e738..950712c050 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"VOICEVOX Engine","description":"VOICEVOX の音声合成エンジンです。","version":"latest"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"preset_id","in":"query","required":true,"schema":{"type":"integer","title":"Preset Id"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"is_kana","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Is Kana"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Accent Phrases Accent Phrases Post"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Data Mora Data Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Length Mora Length Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Pitch Mora Pitch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"enable_interrogative_upspeak","in":"query","required":false,"schema":{"type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true,"title":"Enable Interrogative Upspeak"},"description":"疑問系のテキストが与えられたら語尾を自動調整する"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AudioQuery"},"title":"Queries"}}}},"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"}}}}}}},"/sing_frame_audio_query":{"post":{"tags":["クエリ作成"],"summary":"歌唱音声合成用のクエリを作成する","description":"歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"sing_frame_audio_query_sing_frame_audio_query_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Score"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_volume":{"post":{"tags":["クエリ編集"],"summary":"スコア・歌唱音声合成用のクエリからフレームごとの音量を得る","operationId":"sing_frame_volume_sing_frame_volume_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_volume_sing_frame_volume_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame Volume Sing Frame Volume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/frame_synthesis":{"post":{"tags":["音声合成"],"summary":"Frame Synthesis","description":"歌唱音声合成を行います。","operationId":"frame_synthesis_frame_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"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":{"items":{"type":"string"},"type":"array","title":"Waves"}}},"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"}}}}}}},"/validate_kana":{"post":{"tags":["その他"],"summary":"テキストがAquesTalk 風記法に従っているか判定する","description":"テキストがAquesTalk 風記法に従っているかどうかを判定します。\n従っていない場合はエラーが返ります。","operationId":"validate_kana_validate_kana_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","description":"判定する対象の文字列","title":"Text"},"description":"判定する対象の文字列"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Validate Kana Validate Kana Post"}}}},"400":{"description":"テキストが不正です","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/initialize_speaker":{"post":{"tags":["その他"],"summary":"Initialize Speaker","description":"指定されたスタイルを初期化します。\n実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。","operationId":"initialize_speaker_initialize_speaker_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"skip_reinit","in":"query","required":false,"schema":{"type":"boolean","description":"既に初期化済みのスタイルの再初期化をスキップするかどうか","default":false,"title":"Skip Reinit"},"description":"既に初期化済みのスタイルの再初期化をスキップするかどうか"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/is_initialized_speaker":{"get":{"tags":["その他"],"summary":"Is Initialized Speaker","description":"指定されたスタイルが初期化されているかどうかを返します。","operationId":"is_initialized_speaker_is_initialized_speaker_get","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Is Initialized Speaker Is Initialized Speaker Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/supported_devices":{"get":{"tags":["その他"],"summary":"Supported Devices","description":"対応デバイスの一覧を取得します。","operationId":"supported_devices_supported_devices_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/morphable_targets":{"post":{"tags":["音声合成"],"summary":"指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する","description":"指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。\nモーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。\nプロパティが存在しない場合は、モーフィングが許可されているとみなします。\n返り値のスタイルIDはstring型なので注意。","operationId":"morphable_targets_morphable_targets_post","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"integer"},"title":"Base Style Ids"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MorphableTargetInfo"}},"title":"Response Morphable Targets Morphable Targets Post"}}}},"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":[{"name":"base_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Base Speaker"}},{"name":"target_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Target Speaker"}},{"name":"morph_rate","in":"query","required":true,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Morph Rate"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":"エンジンが保持しているプリセットの設定を返します","operationId":"get_presets_presets_get","responses":{"200":{"description":"プリセットのリスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Preset"},"type":"array","title":"Response Get Presets Presets Get"}}}}}}},"/add_preset":{"post":{"tags":["その他"],"summary":"Add Preset","description":"新しいプリセットを追加します","operationId":"add_preset_add_preset_post","requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Preset"}],"title":"Preset","description":"新しいプリセット。プリセットIDが既存のものと重複している場合は、新規のプリセットIDが採番されます。"}}},"required":true},"responses":{"200":{"description":"追加したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Add Preset Add Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/update_preset":{"post":{"tags":["その他"],"summary":"Update Preset","description":"既存のプリセットを更新します","operationId":"update_preset_update_preset_post","requestBody":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Preset"}],"title":"Preset","description":"更新するプリセット。プリセットIDが更新対象と一致している必要があります。"}}},"required":true},"responses":{"200":{"description":"更新したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Update Preset Update Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/delete_preset":{"post":{"tags":["その他"],"summary":"Delete Preset","description":"既存のプリセットを削除します","operationId":"delete_preset_delete_preset_post","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"削除するプリセットのプリセットID","title":"Id"},"description":"削除するプリセットのプリセットID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","description":"喋れるキャラクターの情報の一覧を返します。","operationId":"speakers_speakers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Speakers Speakers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"UUID で指定された喋れるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"speaker_info_speaker_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/singers":{"get":{"tags":["その他"],"summary":"Singers","description":"歌えるキャラクターの情報の一覧を返します。","operationId":"singers_singers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Singers Singers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singer_info":{"get":{"tags":["その他"],"summary":"Singer Info","description":"UUID で指定された歌えるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"singer_info_singer_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/downloadable_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Downloadable Libraries","description":"ダウンロード可能な音声ライブラリの情報を返します。","operationId":"downloadable_libraries_downloadable_libraries_get","responses":{"200":{"description":"ダウンロード可能な音声ライブラリの情報リスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DownloadableLibraryInfo"},"type":"array","title":"Response Downloadable Libraries Downloadable Libraries Get"}}}}}}},"/installed_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Installed Libraries","description":"インストールした音声ライブラリの情報を返します。","operationId":"installed_libraries_installed_libraries_get","responses":{"200":{"description":"インストールした音声ライブラリの情報","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/InstalledLibraryInfo"},"type":"object","title":"Response Installed Libraries Installed Libraries Get"}}}}}}},"/install_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Install Library","description":"音声ライブラリをインストールします。\n音声ライブラリのZIPファイルをリクエストボディとして送信してください。","operationId":"install_library_install_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/uninstall_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Uninstall Library","description":"音声ライブラリをアンインストールします。","operationId":"uninstall_library_uninstall_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict":{"get":{"tags":["ユーザー辞書"],"summary":"Get User Dict Words","description":"ユーザー辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。","operationId":"get_user_dict_words_user_dict_get","responses":{"200":{"description":"単語のUUIDとその詳細","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"type":"object","title":"Response Get User Dict Words User Dict Get"}}}}}}},"/user_dict_word":{"post":{"tags":["ユーザー辞書"],"summary":"Add User Dict Word","description":"ユーザー辞書に言葉を追加します。","operationId":"add_user_dict_word_user_dict_word_post","parameters":[{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/WordTypes"}],"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか","title":"Word Type"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Add User Dict Word User Dict Word Post"}}}},"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":"ユーザー辞書に登録されている言葉を更新します。","operationId":"rewrite_user_dict_word_user_dict_word__word_uuid__put","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"更新する言葉のUUID","title":"Word Uuid"},"description":"更新する言葉のUUID"},{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/WordTypes"}],"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか","title":"Word Type"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。"}],"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":"ユーザー辞書に登録されている言葉を削除します。","operationId":"delete_user_dict_word_user_dict_word__word_uuid__delete","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"削除する言葉のUUID","title":"Word Uuid"},"description":"削除する言葉のUUID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/import_user_dict":{"post":{"tags":["ユーザー辞書"],"summary":"Import User Dict Words","description":"他のユーザー辞書をインポートします。","operationId":"import_user_dict_words_import_user_dict_post","parameters":[{"name":"override","in":"query","required":true,"schema":{"type":"boolean","description":"重複したエントリがあった場合、上書きするかどうか","title":"Override"},"description":"重複したエントリがあった場合、上書きするかどうか"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"description":"インポートするユーザー辞書のデータ","title":"Import Dict Data"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","description":"エンジンのバージョンを取得します。","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Version Version Get"}}}}}}},"/core_versions":{"get":{"tags":["その他"],"summary":"Core Versions","description":"利用可能なコアのバージョン一覧を取得します。","operationId":"core_versions_core_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response Core Versions Core Versions Get"}}}}}}},"/engine_manifest":{"get":{"tags":["その他"],"summary":"Engine Manifest","description":"エンジンマニフェストを取得します。","operationId":"engine_manifest_engine_manifest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineManifest"}}}}}}},"/setting":{"get":{"tags":["設定"],"summary":"Setting Get","description":"設定ページを返します。","operationId":"setting_get_setting_get","responses":{"200":{"description":"Successful Response"}}},"post":{"tags":["設定"],"summary":"Setting Post","description":"設定を更新します。","operationId":"setting_post_setting_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_setting_post_setting_post"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["その他"],"summary":"Get Portal Page","description":"ポータルページを返します。","operationId":"get_portal_page__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"properties":{"moras":{"items":{"$ref":"#/components/schemas/Mora"},"type":"array","title":"Moras","description":"モーラのリスト"},"accent":{"type":"integer","title":"Accent","description":"アクセント箇所"},"pause_mora":{"allOf":[{"$ref":"#/components/schemas/Mora"}],"title":"Pause Mora","description":"後ろに無音を付けるかどうか"},"is_interrogative":{"type":"boolean","title":"Is Interrogative","description":"疑問系かどうか","default":false}},"type":"object","required":["moras","accent"],"title":"AccentPhrase","description":"アクセント句ごとの情報"},"AudioQuery":{"properties":{"accent_phrases":{"items":{"$ref":"#/components/schemas/AccentPhrase"},"type":"array","title":"Accent Phrases","description":"アクセント句のリスト"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pauselength","description":"句読点などの無音時間。nullのときは無視される。デフォルト値はnull"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)。デフォルト値は1","default":1},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"},"kana":{"type":"string","title":"Kana","description":"[読み取り専用]AquesTalk 風記法によるテキスト。音声合成用のクエリとしては無視される"}},"type":"object","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"title":"AudioQuery","description":"音声合成用のクエリ"},"Body_setting_post_setting_post":{"properties":{"cors_policy_mode":{"$ref":"#/components/schemas/CorsPolicyMode"},"allow_origin":{"type":"string","title":"Allow Origin"}},"type":"object","required":["cors_policy_mode"],"title":"Body_setting_post_setting_post"},"Body_sing_frame_volume_sing_frame_volume_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_volume_sing_frame_volume_post"},"CorsPolicyMode":{"type":"string","enum":["all","localapps"],"title":"CorsPolicyMode","description":"CORSの許可モード"},"DownloadableLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers"],"title":"DownloadableLibraryInfo","description":"ダウンロード可能な音声ライブラリの情報"},"EngineManifest":{"properties":{"manifest_version":{"type":"string","title":"Manifest Version","description":"マニフェストのバージョン"},"name":{"type":"string","title":"Name","description":"エンジン名"},"brand_name":{"type":"string","title":"Brand Name","description":"ブランド名"},"uuid":{"type":"string","title":"Uuid","description":"エンジンのUUID"},"url":{"type":"string","title":"Url","description":"エンジンのURL"},"icon":{"type":"string","title":"Icon","description":"エンジンのアイコンをBASE64エンコードしたもの"},"default_sampling_rate":{"type":"integer","title":"Default Sampling Rate","description":"デフォルトのサンプリング周波数"},"frame_rate":{"type":"number","title":"Frame Rate","description":"エンジンのフレームレート"},"terms_of_service":{"type":"string","title":"Terms Of Service","description":"エンジンの利用規約"},"update_infos":{"items":{"$ref":"#/components/schemas/UpdateInfo"},"type":"array","title":"Update Infos","description":"エンジンのアップデート情報"},"dependency_licenses":{"items":{"$ref":"#/components/schemas/LicenseInfo"},"type":"array","title":"Dependency Licenses","description":"依存関係のライセンス情報"},"supported_vvlib_manifest_version":{"type":"string","title":"Supported Vvlib Manifest Version","description":"エンジンが対応するvvlibのバージョン"},"supported_features":{"allOf":[{"$ref":"#/components/schemas/SupportedFeatures"}],"description":"エンジンが持つ機能"}},"type":"object","required":["manifest_version","name","brand_name","uuid","url","icon","default_sampling_rate","frame_rate","terms_of_service","update_infos","dependency_licenses","supported_features"],"title":"EngineManifest","description":"エンジン自体に関する情報"},"FrameAudioQuery":{"properties":{"f0":{"items":{"type":"number"},"type":"array","title":"F0","description":"フレームごとの基本周波数"},"volume":{"items":{"type":"number"},"type":"array","title":"Volume","description":"フレームごとの音量"},"phonemes":{"items":{"$ref":"#/components/schemas/FramePhoneme"},"type":"array","title":"Phonemes","description":"音素のリスト"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"}},"type":"object","required":["f0","volume","phonemes","volumeScale","outputSamplingRate","outputStereo"],"title":"FrameAudioQuery","description":"フレームごとの音声合成用のクエリ"},"FramePhoneme":{"properties":{"phoneme":{"type":"string","title":"Phoneme","description":"音素"},"frame_length":{"type":"integer","title":"Frame Length","description":"音素のフレーム長"},"note_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Id","description":"音符のID"}},"type":"object","required":["phoneme","frame_length"],"title":"FramePhoneme","description":"音素の情報"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstalledLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"},"uninstallable":{"type":"boolean","title":"Uninstallable","description":"アンインストール可能かどうか"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers","uninstallable"],"title":"InstalledLibraryInfo","description":"インストール済み音声ライブラリの情報"},"LibrarySpeaker":{"properties":{"speaker":{"$ref":"#/components/schemas/Speaker"},"speaker_info":{"$ref":"#/components/schemas/SpeakerInfo"}},"type":"object","required":["speaker","speaker_info"],"title":"LibrarySpeaker","description":"音声ライブラリに含まれるキャラクターの情報"},"LicenseInfo":{"properties":{"name":{"type":"string","title":"Name","description":"依存ライブラリ名"},"version":{"type":"string","title":"Version","description":"依存ライブラリのバージョン"},"license":{"type":"string","title":"License","description":"依存ライブラリのライセンス名"},"text":{"type":"string","title":"Text","description":"依存ライブラリのライセンス本文"}},"type":"object","required":["name","text"],"title":"LicenseInfo","description":"依存ライブラリのライセンス情報"},"Mora":{"properties":{"text":{"type":"string","title":"Text","description":"文字"},"consonant":{"type":"string","title":"Consonant","description":"子音の音素"},"consonant_length":{"type":"number","title":"Consonant Length","description":"子音の音長"},"vowel":{"type":"string","title":"Vowel","description":"母音の音素"},"vowel_length":{"type":"number","title":"Vowel Length","description":"母音の音長"},"pitch":{"type":"number","title":"Pitch","description":"音高"}},"type":"object","required":["text","vowel","vowel_length","pitch"],"title":"Mora","description":"モーラ(子音+母音)ごとの情報"},"MorphableTargetInfo":{"properties":{"is_morphable":{"type":"boolean","title":"Is Morphable","description":"指定したキャラクターに対してモーフィングの可否"}},"type":"object","required":["is_morphable"],"title":"MorphableTargetInfo"},"Note":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"ID"},"key":{"type":"integer","title":"Key","description":"音階"},"frame_length":{"type":"integer","title":"Frame Length","description":"音符のフレーム長"},"lyric":{"type":"string","title":"Lyric","description":"音符の歌詞"}},"type":"object","required":["frame_length","lyric"],"title":"Note","description":"音符ごとの情報"},"ParseKanaBadRequest":{"properties":{"text":{"type":"string","title":"Text","description":"エラーメッセージ"},"error_name":{"type":"string","title":"Error Name","description":"エラー名\n\n|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":{"additionalProperties":{"type":"string"},"type":"object","title":"Error Args","description":"エラーを起こした箇所"}},"type":"object","required":["text","error_name","error_args"],"title":"ParseKanaBadRequest"},"Preset":{"properties":{"id":{"type":"integer","title":"Id","description":"プリセットID"},"name":{"type":"string","title":"Name","description":"プリセット名"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"style_id":{"type":"integer","title":"Style Id","description":"スタイルID"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"type":"number","title":"Pauselength","description":"句読点などの無音時間"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)","default":1}},"type":"object","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"title":"Preset","description":"プリセット情報"},"Score":{"properties":{"notes":{"items":{"$ref":"#/components/schemas/Note"},"type":"array","title":"Notes","description":"音符のリスト"}},"type":"object","required":["notes"],"title":"Score","description":"楽譜情報"},"Speaker":{"properties":{"name":{"type":"string","title":"Name","description":"名前"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"styles":{"items":{"$ref":"#/components/schemas/SpeakerStyle"},"type":"array","title":"Styles","description":"スタイルの一覧"},"version":{"type":"string","title":"Version","description":"キャラクターのバージョン"},"supported_features":{"allOf":[{"$ref":"#/components/schemas/SpeakerSupportedFeatures"}],"description":"キャラクターの対応機能"}},"type":"object","required":["name","speaker_uuid","styles","version"],"title":"Speaker","description":"キャラクター情報"},"SpeakerInfo":{"properties":{"policy":{"type":"string","title":"Policy","description":"policy.md"},"portrait":{"type":"string","title":"Portrait","description":"立ち絵画像をbase64エンコードしたもの、あるいはURL"},"style_infos":{"items":{"$ref":"#/components/schemas/StyleInfo"},"type":"array","title":"Style Infos","description":"スタイルの追加情報"}},"type":"object","required":["policy","portrait","style_infos"],"title":"SpeakerInfo","description":"キャラクターの追加情報"},"SpeakerStyle":{"properties":{"name":{"type":"string","title":"Name","description":"スタイル名"},"id":{"type":"integer","title":"Id","description":"スタイルID"},"type":{"type":"string","enum":["talk","singing_teacher","frame_decode","sing"],"title":"Type","description":"スタイルの種類。talk:音声合成クエリの作成と音声合成が可能。singing_teacher:歌唱音声合成用のクエリの作成が可能。frame_decode:歌唱音声合成が可能。sing:歌唱音声合成用のクエリの作成と歌唱音声合成が可能。","default":"talk"}},"type":"object","required":["name","id"],"title":"SpeakerStyle","description":"キャラクターのスタイル情報"},"SpeakerSupportedFeatures":{"properties":{"permitted_synthesis_morphing":{"type":"string","enum":["ALL","SELF_ONLY","NOTHING"],"title":"Permitted Synthesis Morphing","description":"モーフィング機能への対応。'ALL' は「全て許可」、'SELF_ONLY' は「同じキャラクター内でのみ許可」、'NOTHING' は「全て禁止」","default":"ALL"}},"type":"object","title":"SpeakerSupportedFeatures","description":"キャラクターの対応機能の情報"},"StyleInfo":{"properties":{"id":{"type":"integer","title":"Id","description":"スタイルID"},"icon":{"type":"string","title":"Icon","description":"このスタイルのアイコンをbase64エンコードしたもの、あるいはURL"},"portrait":{"type":"string","title":"Portrait","description":"このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL"},"voice_samples":{"items":{"type":"string"},"type":"array","title":"Voice Samples","description":"サンプル音声をbase64エンコードしたもの、あるいはURL"}},"type":"object","required":["id","icon","voice_samples"],"title":"StyleInfo","description":"スタイルの追加情報"},"SupportedDevicesInfo":{"properties":{"cpu":{"type":"boolean","title":"Cpu","description":"CPUに対応しているか"},"cuda":{"type":"boolean","title":"Cuda","description":"CUDA(Nvidia GPU)に対応しているか"},"dml":{"type":"boolean","title":"Dml","description":"DirectML(Nvidia GPU/Radeon GPU等)に対応しているか"}},"type":"object","required":["cpu","cuda","dml"],"title":"SupportedDevicesInfo","description":"対応しているデバイスの情報"},"SupportedFeatures":{"properties":{"adjust_mora_pitch":{"type":"boolean","title":"Adjust Mora Pitch","description":"モーラごとの音高の調整"},"adjust_phoneme_length":{"type":"boolean","title":"Adjust Phoneme Length","description":"音素ごとの長さの調整"},"adjust_speed_scale":{"type":"boolean","title":"Adjust Speed Scale","description":"全体の話速の調整"},"adjust_pitch_scale":{"type":"boolean","title":"Adjust Pitch Scale","description":"全体の音高の調整"},"adjust_intonation_scale":{"type":"boolean","title":"Adjust Intonation Scale","description":"全体の抑揚の調整"},"adjust_volume_scale":{"type":"boolean","title":"Adjust Volume Scale","description":"全体の音量の調整"},"adjust_pause_length":{"type":"boolean","title":"Adjust Pause Length","description":"句読点などの無音時間の調整"},"interrogative_upspeak":{"type":"boolean","title":"Interrogative Upspeak","description":"疑問文の自動調整"},"synthesis_morphing":{"type":"boolean","title":"Synthesis Morphing","description":"2種類のスタイルでモーフィングした音声を合成"},"sing":{"type":"boolean","title":"Sing","description":"歌唱音声合成"},"manage_library":{"type":"boolean","title":"Manage Library","description":"音声ライブラリのインストール・アンインストール"},"return_resource_url":{"type":"boolean","title":"Return Resource Url","description":"キャラクター情報のリソースをURLで返送"}},"type":"object","required":["adjust_mora_pitch","adjust_phoneme_length","adjust_speed_scale","adjust_pitch_scale","adjust_intonation_scale","adjust_volume_scale","interrogative_upspeak","synthesis_morphing"],"title":"SupportedFeatures","description":"エンジンが持つ機能の一覧"},"UpdateInfo":{"properties":{"version":{"type":"string","title":"Version","description":"エンジンのバージョン名"},"descriptions":{"items":{"type":"string"},"type":"array","title":"Descriptions","description":"アップデートの詳細についての説明"},"contributors":{"items":{"type":"string"},"type":"array","title":"Contributors","description":"貢献者名"}},"type":"object","required":["version","descriptions"],"title":"UpdateInfo","description":"エンジンのアップデート情報"},"UserDictWord":{"properties":{"surface":{"type":"string","title":"Surface","description":"表層形"},"priority":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Priority","description":"優先度"},"context_id":{"type":"integer","title":"Context Id","description":"文脈ID","default":1348},"part_of_speech":{"type":"string","title":"Part Of Speech","description":"品詞"},"part_of_speech_detail_1":{"type":"string","title":"Part Of Speech Detail 1","description":"品詞細分類1"},"part_of_speech_detail_2":{"type":"string","title":"Part Of Speech Detail 2","description":"品詞細分類2"},"part_of_speech_detail_3":{"type":"string","title":"Part Of Speech Detail 3","description":"品詞細分類3"},"inflectional_type":{"type":"string","title":"Inflectional Type","description":"活用型"},"inflectional_form":{"type":"string","title":"Inflectional Form","description":"活用形"},"stem":{"type":"string","title":"Stem","description":"原形"},"yomi":{"type":"string","title":"Yomi","description":"読み"},"pronunciation":{"type":"string","title":"Pronunciation","description":"発音"},"accent_type":{"type":"integer","title":"Accent Type","description":"アクセント型"},"mora_count":{"type":"integer","title":"Mora Count","description":"モーラ数"},"accent_associative_rule":{"type":"string","title":"Accent Associative Rule","description":"アクセント結合規則"}},"type":"object","required":["surface","priority","part_of_speech","part_of_speech_detail_1","part_of_speech_detail_2","part_of_speech_detail_3","inflectional_type","inflectional_form","stem","yomi","pronunciation","accent_type","accent_associative_rule"],"title":"UserDictWord","description":"辞書のコンパイルに使われる情報"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordTypes":{"type":"string","enum":["PROPER_NOUN","COMMON_NOUN","VERB","ADJECTIVE","SUFFIX"],"title":"WordTypes","description":"品詞"},"BaseLibraryInfo":{"description":"音声ライブラリの情報","properties":{"name":{"description":"音声ライブラリの名前","title":"Name","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"version":{"description":"音声ライブラリのバージョン","title":"Version","type":"string"},"download_url":{"description":"音声ライブラリのダウンロードURL","title":"Download Url","type":"string"},"bytes":{"description":"音声ライブラリのバイト数","title":"Bytes","type":"integer"},"speakers":{"description":"音声ライブラリに含まれるキャラクターのリスト","items":{"$ref":"#/components/schemas/LibrarySpeaker"},"title":"Speakers","type":"array"}},"required":["name","uuid","version","download_url","bytes","speakers"],"title":"BaseLibraryInfo","type":"object"},"VvlibManifest":{"description":"vvlib(VOICEVOX Library)に関する情報","properties":{"manifest_version":{"description":"マニフェストバージョン","title":"Manifest Version","type":"string"},"name":{"description":"音声ライブラリ名","title":"Name","type":"string"},"version":{"description":"音声ライブラリバージョン","title":"Version","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"brand_name":{"description":"エンジンのブランド名","title":"Brand Name","type":"string"},"engine_name":{"description":"エンジン名","title":"Engine Name","type":"string"},"engine_uuid":{"description":"エンジンのUUID","title":"Engine Uuid","type":"string"}},"required":["manifest_version","name","version","uuid","brand_name","engine_name","engine_uuid"],"title":"VvlibManifest","type":"object"}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"DUMMY Engine","description":"DUMMY の音声合成エンジンです。","version":"latest"},"paths":{"/audio_query":{"post":{"tags":["クエリ作成"],"summary":"音声合成用のクエリを作成する","description":"音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"audio_query_audio_query_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"preset_id","in":"query","required":true,"schema":{"type":"integer","title":"Preset Id"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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":[{"name":"text","in":"query","required":true,"schema":{"type":"string","title":"Text"}},{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"is_kana","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Is Kana"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Accent Phrases Accent Phrases Post"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Data Mora Data Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_length":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音素長を得る","operationId":"mora_length_mora_length_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Length Mora Length Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/mora_pitch":{"post":{"tags":["クエリ編集"],"summary":"アクセント句から音高を得る","operationId":"mora_pitch_mora_pitch_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Accent Phrases"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccentPhrase"},"title":"Response Mora Pitch Mora Pitch Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/synthesis":{"post":{"tags":["音声合成"],"summary":"音声合成する","operationId":"synthesis_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"enable_interrogative_upspeak","in":"query","required":false,"schema":{"type":"boolean","description":"疑問系のテキストが与えられたら語尾を自動調整する","default":true,"title":"Enable Interrogative Upspeak"},"description":"疑問系のテキストが与えられたら語尾を自動調整する"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AudioQuery"},"title":"Queries"}}}},"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"}}}}}}},"/sing_frame_audio_query":{"post":{"tags":["クエリ作成"],"summary":"歌唱音声合成用のクエリを作成する","description":"歌唱音声合成用のクエリの初期値を得ます。ここで得られたクエリはそのまま歌唱音声合成に利用できます。各値の意味は`Schemas`を参照してください。","operationId":"sing_frame_audio_query_sing_frame_audio_query_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Score"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_f0":{"post":{"tags":["クエリ編集"],"summary":"楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る","operationId":"sing_frame_f0_sing_frame_f0_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_f0_sing_frame_f0_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame F0 Sing Frame F0 Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sing_frame_volume":{"post":{"tags":["クエリ編集"],"summary":"楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る","operationId":"sing_frame_volume_sing_frame_volume_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_sing_frame_volume_sing_frame_volume_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"number"},"title":"Response Sing Frame Volume Sing Frame Volume Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/frame_synthesis":{"post":{"tags":["音声合成"],"summary":"Frame Synthesis","description":"歌唱音声合成を行います。","operationId":"frame_synthesis_frame_synthesis_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FrameAudioQuery"}}}},"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":{"items":{"type":"string"},"type":"array","title":"Waves"}}},"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"}}}}}}},"/validate_kana":{"post":{"tags":["その他"],"summary":"テキストがAquesTalk 風記法に従っているか判定する","description":"テキストがAquesTalk 風記法に従っているかどうかを判定します。\n従っていない場合はエラーが返ります。","operationId":"validate_kana_validate_kana_post","parameters":[{"name":"text","in":"query","required":true,"schema":{"type":"string","description":"判定する対象の文字列","title":"Text"},"description":"判定する対象の文字列"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Validate Kana Validate Kana Post"}}}},"400":{"description":"テキストが不正です","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseKanaBadRequest"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/initialize_speaker":{"post":{"tags":["その他"],"summary":"Initialize Speaker","description":"指定されたスタイルを初期化します。\n実行しなくても他のAPIは使用できますが、初回実行時に時間がかかることがあります。","operationId":"initialize_speaker_initialize_speaker_post","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"skip_reinit","in":"query","required":false,"schema":{"type":"boolean","description":"既に初期化済みのスタイルの再初期化をスキップするかどうか","default":false,"title":"Skip Reinit"},"description":"既に初期化済みのスタイルの再初期化をスキップするかどうか"},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/is_initialized_speaker":{"get":{"tags":["その他"],"summary":"Is Initialized Speaker","description":"指定されたスタイルが初期化されているかどうかを返します。","operationId":"is_initialized_speaker_is_initialized_speaker_get","parameters":[{"name":"speaker","in":"query","required":true,"schema":{"type":"integer","title":"Speaker"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"boolean","title":"Response Is Initialized Speaker Is Initialized Speaker Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/supported_devices":{"get":{"tags":["その他"],"summary":"Supported Devices","description":"対応デバイスの一覧を取得します。","operationId":"supported_devices_supported_devices_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/morphable_targets":{"post":{"tags":["音声合成"],"summary":"指定したスタイルに対してエンジン内のキャラクターがモーフィングが可能か判定する","description":"指定されたベーススタイルに対してエンジン内の各キャラクターがモーフィング機能を利用可能か返します。\nモーフィングの許可/禁止は`/speakers`の`speaker.supported_features.synthesis_morphing`に記載されています。\nプロパティが存在しない場合は、モーフィングが許可されているとみなします。\n返り値のスタイルIDはstring型なので注意。","operationId":"morphable_targets_morphable_targets_post","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"integer"},"title":"Base Style Ids"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/MorphableTargetInfo"}},"title":"Response Morphable Targets Morphable Targets Post"}}}},"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":[{"name":"base_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Base Speaker"}},{"name":"target_speaker","in":"query","required":true,"schema":{"type":"integer","title":"Target Speaker"}},{"name":"morph_rate","in":"query","required":true,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Morph Rate"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioQuery"}}}},"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":"エンジンが保持しているプリセットの設定を返します","operationId":"get_presets_presets_get","responses":{"200":{"description":"プリセットのリスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Preset"},"type":"array","title":"Response Get Presets Presets Get"}}}}}}},"/add_preset":{"post":{"tags":["その他"],"summary":"Add Preset","description":"新しいプリセットを追加します","operationId":"add_preset_add_preset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Preset","description":"新しいプリセット。プリセットIDが既存のものと重複している場合は、新規のプリセットIDが採番されます。"}}},"required":true},"responses":{"200":{"description":"追加したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Add Preset Add Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/update_preset":{"post":{"tags":["その他"],"summary":"Update Preset","description":"既存のプリセットを更新します","operationId":"update_preset_update_preset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Preset","description":"更新するプリセット。プリセットIDが更新対象と一致している必要があります。"}}},"required":true},"responses":{"200":{"description":"更新したプリセットのプリセットID","content":{"application/json":{"schema":{"type":"integer","title":"Response Update Preset Update Preset Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/delete_preset":{"post":{"tags":["その他"],"summary":"Delete Preset","description":"既存のプリセットを削除します","operationId":"delete_preset_delete_preset_post","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","description":"削除するプリセットのプリセットID","title":"Id"},"description":"削除するプリセットのプリセットID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speakers":{"get":{"tags":["その他"],"summary":"Speakers","description":"喋れるキャラクターの情報の一覧を返します。","operationId":"speakers_speakers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Speakers Speakers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker_info":{"get":{"tags":["その他"],"summary":"Speaker Info","description":"UUID で指定された喋れるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"speaker_info_speaker_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/singers":{"get":{"tags":["その他"],"summary":"Singers","description":"歌えるキャラクターの情報の一覧を返します。","operationId":"singers_singers_get","parameters":[{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Speaker"},"title":"Response Singers Singers Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/singer_info":{"get":{"tags":["その他"],"summary":"Singer Info","description":"UUID で指定された歌えるキャラクターの情報を返します。\n画像や音声はresource_formatで指定した形式で返されます。","operationId":"singer_info_singer_info_get","parameters":[{"name":"speaker_uuid","in":"query","required":true,"schema":{"type":"string","title":"Speaker Uuid"}},{"name":"resource_format","in":"query","required":false,"schema":{"enum":["base64","url"],"type":"string","default":"base64","title":"Resource Format"}},{"name":"core_version","in":"query","required":false,"schema":{"type":"string","title":"Core Version"}}],"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"}}}}}}},"/downloadable_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Downloadable Libraries","description":"ダウンロード可能な音声ライブラリの情報を返します。","operationId":"downloadable_libraries_downloadable_libraries_get","responses":{"200":{"description":"ダウンロード可能な音声ライブラリの情報リスト","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/DownloadableLibraryInfo"},"type":"array","title":"Response Downloadable Libraries Downloadable Libraries Get"}}}}}}},"/installed_libraries":{"get":{"tags":["音声ライブラリ管理"],"summary":"Installed Libraries","description":"インストールした音声ライブラリの情報を返します。","operationId":"installed_libraries_installed_libraries_get","responses":{"200":{"description":"インストールした音声ライブラリの情報","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/InstalledLibraryInfo"},"type":"object","title":"Response Installed Libraries Installed Libraries Get"}}}}}}},"/install_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Install Library","description":"音声ライブラリをインストールします。\n音声ライブラリのZIPファイルをリクエストボディとして送信してください。","operationId":"install_library_install_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/uninstall_library/{library_uuid}":{"post":{"tags":["音声ライブラリ管理"],"summary":"Uninstall Library","description":"音声ライブラリをアンインストールします。","operationId":"uninstall_library_uninstall_library__library_uuid__post","parameters":[{"name":"library_uuid","in":"path","required":true,"schema":{"type":"string","description":"音声ライブラリのID","title":"Library Uuid"},"description":"音声ライブラリのID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user_dict":{"get":{"tags":["ユーザー辞書"],"summary":"Get User Dict Words","description":"ユーザー辞書に登録されている単語の一覧を返します。\n単語の表層形(surface)は正規化済みの物を返します。","operationId":"get_user_dict_words_user_dict_get","responses":{"200":{"description":"単語のUUIDとその詳細","content":{"application/json":{"schema":{"additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"type":"object","title":"Response Get User Dict Words User Dict Get"}}}}}}},"/user_dict_word":{"post":{"tags":["ユーザー辞書"],"summary":"Add User Dict Word","description":"ユーザー辞書に言葉を追加します。","operationId":"add_user_dict_word_user_dict_word_post","parameters":[{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/WordTypes","description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Add User Dict Word User Dict Word Post"}}}},"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":"ユーザー辞書に登録されている言葉を更新します。","operationId":"rewrite_user_dict_word_user_dict_word__word_uuid__put","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"更新する言葉のUUID","title":"Word Uuid"},"description":"更新する言葉のUUID"},{"name":"surface","in":"query","required":true,"schema":{"type":"string","description":"言葉の表層形","title":"Surface"},"description":"言葉の表層形"},{"name":"pronunciation","in":"query","required":true,"schema":{"type":"string","description":"言葉の発音(カタカナ)","title":"Pronunciation"},"description":"言葉の発音(カタカナ)"},{"name":"accent_type","in":"query","required":true,"schema":{"type":"integer","description":"アクセント型(音が下がる場所を指す)","title":"Accent Type"},"description":"アクセント型(音が下がる場所を指す)"},{"name":"word_type","in":"query","required":false,"schema":{"$ref":"#/components/schemas/WordTypes","description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},"description":"PROPER_NOUN(固有名詞)、COMMON_NOUN(普通名詞)、VERB(動詞)、ADJECTIVE(形容詞)、SUFFIX(語尾)のいずれか"},{"name":"priority","in":"query","required":false,"schema":{"type":"integer","description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。","maximum":10,"minimum":0,"title":"Priority"},"description":"単語の優先度(0から10までの整数)。数字が大きいほど優先度が高くなる。1から9までの値を指定することを推奨。"}],"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":"ユーザー辞書に登録されている言葉を削除します。","operationId":"delete_user_dict_word_user_dict_word__word_uuid__delete","parameters":[{"name":"word_uuid","in":"path","required":true,"schema":{"type":"string","description":"削除する言葉のUUID","title":"Word Uuid"},"description":"削除する言葉のUUID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/import_user_dict":{"post":{"tags":["ユーザー辞書"],"summary":"Import User Dict Words","description":"他のユーザー辞書をインポートします。","operationId":"import_user_dict_words_import_user_dict_post","parameters":[{"name":"override","in":"query","required":true,"schema":{"type":"boolean","description":"重複したエントリがあった場合、上書きするかどうか","title":"Override"},"description":"重複したエントリがあった場合、上書きするかどうか"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/UserDictWord"},"description":"インポートするユーザー辞書のデータ","title":"Import Dict Data"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/version":{"get":{"tags":["その他"],"summary":"Version","description":"エンジンのバージョンを取得します。","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Version Version Get"}}}}}}},"/core_versions":{"get":{"tags":["その他"],"summary":"Core Versions","description":"利用可能なコアのバージョン一覧を取得します。","operationId":"core_versions_core_versions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"type":"string"},"type":"array","title":"Response Core Versions Core Versions Get"}}}}}}},"/engine_manifest":{"get":{"tags":["その他"],"summary":"Engine Manifest","description":"エンジンマニフェストを取得します。","operationId":"engine_manifest_engine_manifest_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineManifest"}}}}}}},"/setting":{"get":{"tags":["設定"],"summary":"Setting Get","description":"設定ページを返します。","operationId":"setting_get_setting_get","responses":{"200":{"description":"Successful Response"}}},"post":{"tags":["設定"],"summary":"Setting Post","description":"設定を更新します。","operationId":"setting_post_setting_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_setting_post_setting_post"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["その他"],"summary":"Get Portal Page","description":"ポータルページを返します。","operationId":"get_portal_page__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"AccentPhrase":{"properties":{"moras":{"items":{"$ref":"#/components/schemas/Mora"},"type":"array","title":"Moras","description":"モーラのリスト"},"accent":{"type":"integer","title":"Accent","description":"アクセント箇所"},"pause_mora":{"$ref":"#/components/schemas/Mora","title":"Pause Mora","description":"後ろに無音を付けるかどうか"},"is_interrogative":{"type":"boolean","title":"Is Interrogative","description":"疑問系かどうか","default":false}},"type":"object","required":["moras","accent"],"title":"AccentPhrase","description":"アクセント句ごとの情報"},"AudioQuery":{"properties":{"accent_phrases":{"items":{"$ref":"#/components/schemas/AccentPhrase"},"type":"array","title":"Accent Phrases","description":"アクセント句のリスト"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pauselength","description":"句読点などの無音時間。nullのときは無視される。デフォルト値はnull"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)。デフォルト値は1","default":1},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"},"kana":{"type":"string","title":"Kana","description":"[読み取り専用]AquesTalk 風記法によるテキスト。音声合成用のクエリとしては無視される"}},"type":"object","required":["accent_phrases","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength","outputSamplingRate","outputStereo"],"title":"AudioQuery","description":"音声合成用のクエリ"},"Body_setting_post_setting_post":{"properties":{"cors_policy_mode":{"$ref":"#/components/schemas/CorsPolicyMode"},"allow_origin":{"type":"string","title":"Allow Origin"}},"type":"object","required":["cors_policy_mode"],"title":"Body_setting_post_setting_post"},"Body_sing_frame_f0_sing_frame_f0_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_f0_sing_frame_f0_post"},"Body_sing_frame_volume_sing_frame_volume_post":{"properties":{"score":{"$ref":"#/components/schemas/Score"},"frame_audio_query":{"$ref":"#/components/schemas/FrameAudioQuery"}},"type":"object","required":["score","frame_audio_query"],"title":"Body_sing_frame_volume_sing_frame_volume_post"},"CorsPolicyMode":{"type":"string","enum":["all","localapps"],"title":"CorsPolicyMode","description":"CORSの許可モード"},"DownloadableLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers"],"title":"DownloadableLibraryInfo","description":"ダウンロード可能な音声ライブラリの情報"},"EngineManifest":{"properties":{"manifest_version":{"type":"string","title":"Manifest Version","description":"マニフェストのバージョン"},"name":{"type":"string","title":"Name","description":"エンジン名"},"brand_name":{"type":"string","title":"Brand Name","description":"ブランド名"},"uuid":{"type":"string","title":"Uuid","description":"エンジンのUUID"},"url":{"type":"string","title":"Url","description":"エンジンのURL"},"icon":{"type":"string","title":"Icon","description":"エンジンのアイコンをBASE64エンコードしたもの"},"default_sampling_rate":{"type":"integer","title":"Default Sampling Rate","description":"デフォルトのサンプリング周波数"},"frame_rate":{"type":"number","title":"Frame Rate","description":"エンジンのフレームレート"},"terms_of_service":{"type":"string","title":"Terms Of Service","description":"エンジンの利用規約"},"update_infos":{"items":{"$ref":"#/components/schemas/UpdateInfo"},"type":"array","title":"Update Infos","description":"エンジンのアップデート情報"},"dependency_licenses":{"items":{"$ref":"#/components/schemas/LicenseInfo"},"type":"array","title":"Dependency Licenses","description":"依存関係のライセンス情報"},"supported_vvlib_manifest_version":{"type":"string","title":"Supported Vvlib Manifest Version","description":"エンジンが対応するvvlibのバージョン"},"supported_features":{"$ref":"#/components/schemas/SupportedFeatures","description":"エンジンが持つ機能"}},"type":"object","required":["manifest_version","name","brand_name","uuid","url","icon","default_sampling_rate","frame_rate","terms_of_service","update_infos","dependency_licenses","supported_features"],"title":"EngineManifest","description":"エンジン自体に関する情報"},"FrameAudioQuery":{"properties":{"f0":{"items":{"type":"number"},"type":"array","title":"F0","description":"フレームごとの基本周波数"},"volume":{"items":{"type":"number"},"type":"array","title":"Volume","description":"フレームごとの音量"},"phonemes":{"items":{"$ref":"#/components/schemas/FramePhoneme"},"type":"array","title":"Phonemes","description":"音素のリスト"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"outputSamplingRate":{"type":"integer","title":"Outputsamplingrate","description":"音声データの出力サンプリングレート"},"outputStereo":{"type":"boolean","title":"Outputstereo","description":"音声データをステレオ出力するか否か"}},"type":"object","required":["f0","volume","phonemes","volumeScale","outputSamplingRate","outputStereo"],"title":"FrameAudioQuery","description":"フレームごとの音声合成用のクエリ"},"FramePhoneme":{"properties":{"phoneme":{"type":"string","title":"Phoneme","description":"音素"},"frame_length":{"type":"integer","title":"Frame Length","description":"音素のフレーム長"},"note_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Id","description":"音符のID"}},"type":"object","required":["phoneme","frame_length"],"title":"FramePhoneme","description":"音素の情報"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InstalledLibraryInfo":{"properties":{"name":{"type":"string","title":"Name","description":"音声ライブラリの名前"},"uuid":{"type":"string","title":"Uuid","description":"音声ライブラリのUUID"},"version":{"type":"string","title":"Version","description":"音声ライブラリのバージョン"},"download_url":{"type":"string","title":"Download Url","description":"音声ライブラリのダウンロードURL"},"bytes":{"type":"integer","title":"Bytes","description":"音声ライブラリのバイト数"},"speakers":{"items":{"$ref":"#/components/schemas/LibrarySpeaker"},"type":"array","title":"Speakers","description":"音声ライブラリに含まれるキャラクターのリスト"},"uninstallable":{"type":"boolean","title":"Uninstallable","description":"アンインストール可能かどうか"}},"type":"object","required":["name","uuid","version","download_url","bytes","speakers","uninstallable"],"title":"InstalledLibraryInfo","description":"インストール済み音声ライブラリの情報"},"LibrarySpeaker":{"properties":{"speaker":{"$ref":"#/components/schemas/Speaker"},"speaker_info":{"$ref":"#/components/schemas/SpeakerInfo"}},"type":"object","required":["speaker","speaker_info"],"title":"LibrarySpeaker","description":"音声ライブラリに含まれるキャラクターの情報"},"LicenseInfo":{"properties":{"name":{"type":"string","title":"Name","description":"依存ライブラリ名"},"version":{"type":"string","title":"Version","description":"依存ライブラリのバージョン"},"license":{"type":"string","title":"License","description":"依存ライブラリのライセンス名"},"text":{"type":"string","title":"Text","description":"依存ライブラリのライセンス本文"}},"type":"object","required":["name","text"],"title":"LicenseInfo","description":"依存ライブラリのライセンス情報"},"Mora":{"properties":{"text":{"type":"string","title":"Text","description":"文字"},"consonant":{"type":"string","title":"Consonant","description":"子音の音素"},"consonant_length":{"type":"number","title":"Consonant Length","description":"子音の音長"},"vowel":{"type":"string","title":"Vowel","description":"母音の音素"},"vowel_length":{"type":"number","title":"Vowel Length","description":"母音の音長"},"pitch":{"type":"number","title":"Pitch","description":"音高"}},"type":"object","required":["text","vowel","vowel_length","pitch"],"title":"Mora","description":"モーラ(子音+母音)ごとの情報"},"MorphableTargetInfo":{"properties":{"is_morphable":{"type":"boolean","title":"Is Morphable","description":"指定したキャラクターに対してモーフィングの可否"}},"type":"object","required":["is_morphable"],"title":"MorphableTargetInfo"},"Note":{"properties":{"id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Id","description":"ID"},"key":{"type":"integer","title":"Key","description":"音階"},"frame_length":{"type":"integer","title":"Frame Length","description":"音符のフレーム長"},"lyric":{"type":"string","title":"Lyric","description":"音符の歌詞"}},"type":"object","required":["frame_length","lyric"],"title":"Note","description":"音符ごとの情報"},"ParseKanaBadRequest":{"properties":{"text":{"type":"string","title":"Text","description":"エラーメッセージ"},"error_name":{"type":"string","title":"Error Name","description":"エラー名\n\n|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":{"additionalProperties":{"type":"string"},"type":"object","title":"Error Args","description":"エラーを起こした箇所"}},"type":"object","required":["text","error_name","error_args"],"title":"ParseKanaBadRequest"},"Preset":{"properties":{"id":{"type":"integer","title":"Id","description":"プリセットID"},"name":{"type":"string","title":"Name","description":"プリセット名"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"style_id":{"type":"integer","title":"Style Id","description":"スタイルID"},"speedScale":{"type":"number","title":"Speedscale","description":"全体の話速"},"pitchScale":{"type":"number","title":"Pitchscale","description":"全体の音高"},"intonationScale":{"type":"number","title":"Intonationscale","description":"全体の抑揚"},"volumeScale":{"type":"number","title":"Volumescale","description":"全体の音量"},"prePhonemeLength":{"type":"number","title":"Prephonemelength","description":"音声の前の無音時間"},"postPhonemeLength":{"type":"number","title":"Postphonemelength","description":"音声の後の無音時間"},"pauseLength":{"type":"number","title":"Pauselength","description":"句読点などの無音時間"},"pauseLengthScale":{"type":"number","title":"Pauselengthscale","description":"句読点などの無音時間(倍率)","default":1}},"type":"object","required":["id","name","speaker_uuid","style_id","speedScale","pitchScale","intonationScale","volumeScale","prePhonemeLength","postPhonemeLength"],"title":"Preset","description":"プリセット情報"},"Score":{"properties":{"notes":{"items":{"$ref":"#/components/schemas/Note"},"type":"array","title":"Notes","description":"音符のリスト"}},"type":"object","required":["notes"],"title":"Score","description":"楽譜情報"},"Speaker":{"properties":{"name":{"type":"string","title":"Name","description":"名前"},"speaker_uuid":{"type":"string","title":"Speaker Uuid","description":"キャラクターのUUID"},"styles":{"items":{"$ref":"#/components/schemas/SpeakerStyle"},"type":"array","title":"Styles","description":"スタイルの一覧"},"version":{"type":"string","title":"Version","description":"キャラクターのバージョン"},"supported_features":{"$ref":"#/components/schemas/SpeakerSupportedFeatures","description":"キャラクターの対応機能"}},"type":"object","required":["name","speaker_uuid","styles","version"],"title":"Speaker","description":"キャラクター情報"},"SpeakerInfo":{"properties":{"policy":{"type":"string","title":"Policy","description":"policy.md"},"portrait":{"type":"string","title":"Portrait","description":"立ち絵画像をbase64エンコードしたもの、あるいはURL"},"style_infos":{"items":{"$ref":"#/components/schemas/StyleInfo"},"type":"array","title":"Style Infos","description":"スタイルの追加情報"}},"type":"object","required":["policy","portrait","style_infos"],"title":"SpeakerInfo","description":"キャラクターの追加情報"},"SpeakerStyle":{"properties":{"name":{"type":"string","title":"Name","description":"スタイル名"},"id":{"type":"integer","title":"Id","description":"スタイルID"},"type":{"type":"string","enum":["talk","singing_teacher","frame_decode","sing"],"title":"Type","description":"スタイルの種類。talk:音声合成クエリの作成と音声合成が可能。singing_teacher:歌唱音声合成用のクエリの作成が可能。frame_decode:歌唱音声合成が可能。sing:歌唱音声合成用のクエリの作成と歌唱音声合成が可能。","default":"talk"}},"type":"object","required":["name","id"],"title":"SpeakerStyle","description":"キャラクターのスタイル情報"},"SpeakerSupportedFeatures":{"properties":{"permitted_synthesis_morphing":{"type":"string","enum":["ALL","SELF_ONLY","NOTHING"],"title":"Permitted Synthesis Morphing","description":"モーフィング機能への対応。'ALL' は「全て許可」、'SELF_ONLY' は「同じキャラクター内でのみ許可」、'NOTHING' は「全て禁止」","default":"ALL"}},"type":"object","title":"SpeakerSupportedFeatures","description":"キャラクターの対応機能の情報"},"StyleInfo":{"properties":{"id":{"type":"integer","title":"Id","description":"スタイルID"},"icon":{"type":"string","title":"Icon","description":"このスタイルのアイコンをbase64エンコードしたもの、あるいはURL"},"portrait":{"type":"string","title":"Portrait","description":"このスタイルの立ち絵画像をbase64エンコードしたもの、あるいはURL"},"voice_samples":{"items":{"type":"string"},"type":"array","title":"Voice Samples","description":"サンプル音声をbase64エンコードしたもの、あるいはURL"}},"type":"object","required":["id","icon","voice_samples"],"title":"StyleInfo","description":"スタイルの追加情報"},"SupportedDevicesInfo":{"properties":{"cpu":{"type":"boolean","title":"Cpu","description":"CPUに対応しているか"},"cuda":{"type":"boolean","title":"Cuda","description":"CUDA(Nvidia GPU)に対応しているか"},"dml":{"type":"boolean","title":"Dml","description":"DirectML(Nvidia GPU/Radeon GPU等)に対応しているか"}},"type":"object","required":["cpu","cuda","dml"],"title":"SupportedDevicesInfo","description":"対応しているデバイスの情報"},"SupportedFeatures":{"properties":{"adjust_mora_pitch":{"type":"boolean","title":"Adjust Mora Pitch","description":"モーラごとの音高の調整"},"adjust_phoneme_length":{"type":"boolean","title":"Adjust Phoneme Length","description":"音素ごとの長さの調整"},"adjust_speed_scale":{"type":"boolean","title":"Adjust Speed Scale","description":"全体の話速の調整"},"adjust_pitch_scale":{"type":"boolean","title":"Adjust Pitch Scale","description":"全体の音高の調整"},"adjust_intonation_scale":{"type":"boolean","title":"Adjust Intonation Scale","description":"全体の抑揚の調整"},"adjust_volume_scale":{"type":"boolean","title":"Adjust Volume Scale","description":"全体の音量の調整"},"adjust_pause_length":{"type":"boolean","title":"Adjust Pause Length","description":"句読点などの無音時間の調整"},"interrogative_upspeak":{"type":"boolean","title":"Interrogative Upspeak","description":"疑問文の自動調整"},"synthesis_morphing":{"type":"boolean","title":"Synthesis Morphing","description":"2種類のスタイルでモーフィングした音声を合成"},"sing":{"type":"boolean","title":"Sing","description":"歌唱音声合成"},"manage_library":{"type":"boolean","title":"Manage Library","description":"音声ライブラリのインストール・アンインストール"},"return_resource_url":{"type":"boolean","title":"Return Resource Url","description":"キャラクター情報のリソースをURLで返送"}},"type":"object","required":["adjust_mora_pitch","adjust_phoneme_length","adjust_speed_scale","adjust_pitch_scale","adjust_intonation_scale","adjust_volume_scale","interrogative_upspeak","synthesis_morphing"],"title":"SupportedFeatures","description":"エンジンが持つ機能の一覧"},"UpdateInfo":{"properties":{"version":{"type":"string","title":"Version","description":"エンジンのバージョン名"},"descriptions":{"items":{"type":"string"},"type":"array","title":"Descriptions","description":"アップデートの詳細についての説明"},"contributors":{"items":{"type":"string"},"type":"array","title":"Contributors","description":"貢献者名"}},"type":"object","required":["version","descriptions"],"title":"UpdateInfo","description":"エンジンのアップデート情報"},"UserDictWord":{"properties":{"surface":{"type":"string","title":"Surface","description":"表層形"},"priority":{"type":"integer","maximum":10.0,"minimum":0.0,"title":"Priority","description":"優先度"},"context_id":{"type":"integer","title":"Context Id","description":"文脈ID","default":1348},"part_of_speech":{"type":"string","title":"Part Of Speech","description":"品詞"},"part_of_speech_detail_1":{"type":"string","title":"Part Of Speech Detail 1","description":"品詞細分類1"},"part_of_speech_detail_2":{"type":"string","title":"Part Of Speech Detail 2","description":"品詞細分類2"},"part_of_speech_detail_3":{"type":"string","title":"Part Of Speech Detail 3","description":"品詞細分類3"},"inflectional_type":{"type":"string","title":"Inflectional Type","description":"活用型"},"inflectional_form":{"type":"string","title":"Inflectional Form","description":"活用形"},"stem":{"type":"string","title":"Stem","description":"原形"},"yomi":{"type":"string","title":"Yomi","description":"読み"},"pronunciation":{"type":"string","title":"Pronunciation","description":"発音"},"accent_type":{"type":"integer","title":"Accent Type","description":"アクセント型"},"mora_count":{"type":"integer","title":"Mora Count","description":"モーラ数"},"accent_associative_rule":{"type":"string","title":"Accent Associative Rule","description":"アクセント結合規則"}},"type":"object","required":["surface","priority","part_of_speech","part_of_speech_detail_1","part_of_speech_detail_2","part_of_speech_detail_3","inflectional_type","inflectional_form","stem","yomi","pronunciation","accent_type","accent_associative_rule"],"title":"UserDictWord","description":"辞書のコンパイルに使われる情報"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WordTypes":{"type":"string","enum":["PROPER_NOUN","COMMON_NOUN","VERB","ADJECTIVE","SUFFIX"],"title":"WordTypes","description":"品詞"},"BaseLibraryInfo":{"description":"音声ライブラリの情報","properties":{"name":{"description":"音声ライブラリの名前","title":"Name","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"version":{"description":"音声ライブラリのバージョン","title":"Version","type":"string"},"download_url":{"description":"音声ライブラリのダウンロードURL","title":"Download Url","type":"string"},"bytes":{"description":"音声ライブラリのバイト数","title":"Bytes","type":"integer"},"speakers":{"description":"音声ライブラリに含まれるキャラクターのリスト","items":{"$ref":"#/components/schemas/LibrarySpeaker"},"title":"Speakers","type":"array"}},"required":["name","uuid","version","download_url","bytes","speakers"],"title":"BaseLibraryInfo","type":"object"},"VvlibManifest":{"description":"vvlib(VOICEVOX Library)に関する情報","properties":{"manifest_version":{"description":"マニフェストバージョン","title":"Manifest Version","type":"string"},"name":{"description":"音声ライブラリ名","title":"Name","type":"string"},"version":{"description":"音声ライブラリバージョン","title":"Version","type":"string"},"uuid":{"description":"音声ライブラリのUUID","title":"Uuid","type":"string"},"brand_name":{"description":"エンジンのブランド名","title":"Brand Name","type":"string"},"engine_name":{"description":"エンジン名","title":"Engine Name","type":"string"},"engine_uuid":{"description":"エンジンのUUID","title":"Engine Uuid","type":"string"}},"required":["manifest_version","name","version","uuid","brand_name","engine_name","engine_uuid"],"title":"VvlibManifest","type":"object"}}}} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts index 155d4e8b75..529bf25210 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,8 +1,15 @@ -import type { PlaywrightTestConfig, Project } from "@playwright/test"; -import { z } from "zod"; +/** + * e2eテストと .env の設計: + * - デフォルトで .env.test を読み込む。 + * モックエンジンが使われる。 + * - Electronテストはテストファイル内で様々な .env を読み込む。 + * テスト条件によって用意したい環境が異なるため。 + */ +import type { PlaywrightTestConfig, Project } from "@playwright/test"; import dotenv from "dotenv"; -dotenv.config({ override: true }); + +dotenv.config({ path: ".env.test", override: true }); let project: Project; let webServers: PlaywrightTestConfig["webServer"]; @@ -10,26 +17,6 @@ const isElectron = process.env.VITE_TARGET === "electron"; const isBrowser = process.env.VITE_TARGET === "browser"; const isStorybook = process.env.TARGET === "storybook"; -// エンジンの起動が必要 -const defaultEngineInfosEnv = process.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]"; -const envSchema = z // FIXME: electron起動時のものと共通化したい - .object({ - host: z.string(), - executionFilePath: z.string(), - executionArgs: z.array(z.string()), - executionEnabled: z.boolean(), - }) - .passthrough() - .array(); -const engineInfos = envSchema.parse(JSON.parse(defaultEngineInfosEnv)); - -const engineServers = engineInfos - .filter((info) => info.executionEnabled) - .map((info) => ({ - command: `${info.executionFilePath} ${info.executionArgs.join(" ")}`, - url: `${info.host}/version`, - reuseExistingServer: !process.env.CI, - })); const viteServer = { command: "vite --mode test --port 7357", port: 7357, @@ -46,7 +33,7 @@ if (isElectron) { webServers = [viteServer]; } else if (isBrowser) { project = { name: "browser", testDir: "./tests/e2e/browser" }; - webServers = [viteServer, ...engineServers]; + webServers = [viteServer]; } else if (isStorybook) { project = { name: "storybook", testDir: "./tests/e2e/storybook" }; webServers = [storybookServer]; diff --git a/src/backend/browser/browserConfig.ts b/src/backend/browser/browserConfig.ts index 6508d9af8e..c5ffc900a3 100644 --- a/src/backend/browser/browserConfig.ts +++ b/src/backend/browser/browserConfig.ts @@ -21,7 +21,7 @@ const defaultEngineId = EngineId(defaultEngine.uuid); export async function getConfigManager() { await configManagerLock.acquire("configManager", async () => { if (!configManager) { - configManager = new BrowserConfigManager(isMac); + configManager = new BrowserConfigManager({ isMac }); await configManager.initialize(); } }); diff --git a/src/backend/browser/fakePath.ts b/src/backend/browser/fakePath.ts new file mode 100644 index 0000000000..4c3d42074e --- /dev/null +++ b/src/backend/browser/fakePath.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { uuid4 } from "@/helpers/random"; + +const fakePathSchema = z + .string() + .regex(/^-.+$/) + .brand("FakePath"); +export type FakePath = z.infer; + +export const isFakePath = (path: string): path is FakePath => { + return fakePathSchema.safeParse(path).success; +}; + +export const createFakePath = (name: string): FakePath => { + return fakePathSchema.parse(`-${name}`); +}; diff --git a/src/backend/browser/fileImpl.ts b/src/backend/browser/fileImpl.ts index 2c78966774..fcfc18742b 100644 --- a/src/backend/browser/fileImpl.ts +++ b/src/backend/browser/fileImpl.ts @@ -1,11 +1,12 @@ import { directoryHandleStoreKey } from "./contract"; import { openDB } from "./browserConfig"; +import { createFakePath, FakePath, isFakePath } from "./fakePath"; import { SandboxKey } from "@/type/preload"; import { failure, success } from "@/type/result"; import { createLogger } from "@/domain/frontend/log"; -import { uuid4 } from "@/helpers/random"; import { normalizeError } from "@/helpers/normalizeError"; import path from "@/helpers/path"; +import { ExhaustiveError } from "@/type/utility"; const log = createLogger("fileImpl"); @@ -113,17 +114,48 @@ const getDirectoryHandleFromDirectoryPath = async ( } }; +export type WritableFilePath = + | { + // ファイル名のみ。ダウンロードとして扱われます。 + type: "nameOnly"; + path: string; + } + | { + // ディレクトリ内への書き込み。 + type: "child"; + path: string; + } + | { + // 疑似パス。 + type: "fake"; + path: FakePath; + }; + // NOTE: fixedExportEnabled が有効になっている GENERATE_AND_SAVE_AUDIO action では、ファイル名に加えディレクトリ名も指定された状態でfilePathが渡ってくる // また GENERATE_AND_SAVE_ALL_AUDIO action では fixedExportEnabled の有効の有無に関わらず、ディレクトリ名も指定された状態でfilePathが渡ってくる -export const writeFileImpl: (typeof window)[typeof SandboxKey]["writeFile"] = - async (obj: { filePath: string; buffer: ArrayBuffer }) => { - const filePath = obj.filePath; +// showExportFilePicker での疑似パスが渡ってくる可能性もある。 +export const writeFileImpl = async (obj: { + filePath: WritableFilePath; + buffer: ArrayBuffer; +}) => { + const filePath = obj.filePath; - if (!filePath.includes(path.SEPARATOR)) { + switch (filePath.type) { + case "fake": { + const fileHandle = fileHandleMap.get(filePath.path); + if (fileHandle == undefined) { + return failure(new Error(`ファイルが見つかりません: ${filePath.path}`)); + } + const writable = await fileHandle.createWritable(); + await writable.write(obj.buffer); + return writable.close().then(() => success(undefined)); + } + + case "nameOnly": { const aTag = document.createElement("a"); const blob = URL.createObjectURL(new Blob([obj.buffer])); aTag.href = blob; - aTag.download = filePath; + aTag.download = filePath.path; document.body.appendChild(aTag); aTag.click(); document.body.removeChild(aTag); @@ -131,27 +163,32 @@ export const writeFileImpl: (typeof window)[typeof SandboxKey]["writeFile"] = return success(undefined); } - const fileName = resolveFileName(filePath); - const maybeDirectoryHandleName = resolveDirectoryName(filePath); + case "child": { + const fileName = resolveFileName(filePath.path); + const maybeDirectoryHandleName = resolveDirectoryName(filePath.path); - const directoryHandle = await getDirectoryHandleFromDirectoryPath( - maybeDirectoryHandleName, - ); + const directoryHandle = await getDirectoryHandleFromDirectoryPath( + maybeDirectoryHandleName, + ); - directoryHandleMap.set(maybeDirectoryHandleName, directoryHandle); + directoryHandleMap.set(maybeDirectoryHandleName, directoryHandle); - return directoryHandle - .getFileHandle(fileName, { create: true }) - .then(async (fileHandle) => { - const writable = await fileHandle.createWritable(); - await writable.write(obj.buffer); - return writable.close(); - }) - .then(() => success(undefined)) - .catch((e) => { - return failure(normalizeError(e)); - }); - }; + return directoryHandle + .getFileHandle(fileName, { create: true }) + .then(async (fileHandle) => { + const writable = await fileHandle.createWritable(); + await writable.write(obj.buffer); + return writable.close(); + }) + .then(() => success(undefined)) + .catch((e) => { + return failure(normalizeError(e)); + }); + } + default: + throw new ExhaustiveError(filePath); + } +}; export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileExists"] = async (filePath) => { @@ -182,7 +219,7 @@ export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileE }; // FileSystemFileHandleを保持するMap。キーは生成した疑似パス。 -const fileHandleMap: Map = new Map(); +const fileHandleMap: Map = new Map(); // ファイル選択ダイアログを開く // 返り値はファイルパスではなく、疑似パスを返す @@ -201,7 +238,7 @@ export const showOpenFilePickerImpl = async (options: { }); const paths = []; for (const handle of handles) { - const fakePath = `-${handle.name}`; + const fakePath = createFakePath(handle.name); fileHandleMap.set(fakePath, handle); paths.push(fakePath); } @@ -214,6 +251,9 @@ export const showOpenFilePickerImpl = async (options: { // 指定した疑似パスのファイルを読み込む export const readFileImpl = async (filePath: string) => { + if (!isFakePath(filePath)) { + return failure(new Error(`疑似パスではありません: ${filePath}`)); + } const fileHandle = fileHandleMap.get(filePath); if (fileHandle == undefined) { return failure(new Error(`ファイルが見つかりません: ${filePath}`)); @@ -222,3 +262,29 @@ export const readFileImpl = async (filePath: string) => { const buffer = await file.arrayBuffer(); return success(buffer); }; + +// ファイル選択ダイアログを開く +// 返り値はファイルパスではなく、疑似パスを返す +export const showExportFilePickerImpl: (typeof window)[typeof SandboxKey]["showExportFileDialog"] = + async (obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; + }) => { + const handle = await showSaveFilePicker({ + suggestedName: obj.defaultPath, + types: [ + { + description: obj.extensions.join("、"), + accept: { + "application/octet-stream": obj.extensions.map((ext) => `.${ext}`), + }, + }, + ], + }); + const fakePath = createFakePath(handle.name); + fileHandleMap.set(fakePath, handle); + + return fakePath; + }; diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index 38b0f12e7a..f1ef8025ab 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -2,11 +2,14 @@ import { defaultEngine } from "./contract"; import { checkFileExistsImpl, readFileImpl, + showExportFilePickerImpl, showOpenDirectoryDialogImpl, showOpenFilePickerImpl, + WritableFilePath, writeFileImpl, } from "./fileImpl"; import { getConfigManager } from "./browserConfig"; +import { isFakePath } from "./fakePath"; import { IpcSOData } from "@/type/ipc"; import { defaultToolbarButtonSetting, @@ -17,6 +20,7 @@ import { } from "@/type/preload"; import { AssetTextFileNames } from "@/type/staticResources"; import { HotkeySettingType } from "@/domain/hotkeyAction"; +import path from "@/helpers/path"; const toStaticPath = (fileName: string) => `${import.meta.env.BASE_URL}/${fileName}`.replaceAll(/\/\/+/g, "/"); @@ -72,34 +76,6 @@ export const api: Sandbox = { // NOTE: ブラウザ版ではサポートされていません return Promise.resolve({}); }, - showAudioSaveDialog(obj: { title: string; defaultPath?: string }) { - return new Promise((resolve, reject) => { - if (obj.defaultPath == undefined) { - reject( - // storeやvue componentからdefaultPathを設定していなかったらrejectされる - new Error( - "ブラウザ版ではファイルの保存機能が一部サポートされていません。", - ), - ); - } else { - resolve(obj.defaultPath); - } - }); - }, - showTextSaveDialog(obj: { title: string; defaultPath?: string }) { - return new Promise((resolve, reject) => { - if (obj.defaultPath == undefined) { - reject( - // storeやvue componentからdefaultPathを設定していなかったらrejectされる - new Error( - "ブラウザ版ではファイルの保存機能が一部サポートされていません。", - ), - ); - } else { - resolve(obj.defaultPath); - } - }); - }, showSaveDirectoryDialog(obj: { title: string }) { return showOpenDirectoryDialogImpl(obj); }, @@ -163,8 +139,26 @@ export const api: Sandbox = { }); return fileHandle?.[0]; }, + async showExportFileDialog(obj: { + defaultPath?: string; + extensionName: string; + extensions: string[]; + title: string; + }) { + const fileHandle = await showExportFilePickerImpl(obj); + return fileHandle; + }, writeFile(obj: { filePath: string; buffer: ArrayBuffer }) { - return writeFileImpl(obj); + let filePath: WritableFilePath; + if (isFakePath(obj.filePath)) { + filePath = { type: "fake", path: obj.filePath }; + } else if (obj.filePath.includes(path.SEPARATOR)) { + filePath = { type: "child", path: obj.filePath }; + } else { + filePath = { type: "nameOnly", path: obj.filePath }; + } + + return writeFileImpl({ filePath, buffer: obj.buffer }); }, readFile(obj: { filePath: string }) { return readFileImpl(obj.filePath); @@ -286,4 +280,7 @@ export const api: Sandbox = { reloadApp(/* obj: { isMultiEngineOffMode: boolean } */) { throw new Error(`Not supported on Browser version: reloadApp`); }, + getPathForFile(/* file: File */) { + throw new Error(`Not supported on Browser version: getPathForFile`); + }, }; diff --git a/src/backend/common/ConfigManager.ts b/src/backend/common/ConfigManager.ts index b5ddabb706..3d64caeb38 100644 --- a/src/backend/common/ConfigManager.ts +++ b/src/backend/common/ConfigManager.ts @@ -16,7 +16,6 @@ import { getDefaultHotkeySettings, HotkeySettingType, } from "@/domain/hotkeyAction"; -import { isMac } from "@/helpers/platform"; const lockKey = "save"; @@ -300,6 +299,7 @@ export type Metadata = { */ export abstract class BaseConfigManager { protected config: ConfigType | undefined; + protected isMac: boolean; private lock = new AsyncLock(); @@ -309,7 +309,9 @@ export abstract class BaseConfigManager { protected abstract getAppVersion(): string; - constructor(protected isMac: boolean) {} + constructor({ isMac }: { isMac: boolean }) { + this.isMac = isMac; + } public reset() { this.config = this.getDefaultConfig(); @@ -326,7 +328,7 @@ export abstract class BaseConfigManager { } } this.config = this.migrateHotkeySettings( - getConfigSchema(this.isMac).parse(data), + getConfigSchema({ isMac: this.isMac }).parse(data), ); this._save(); } else { @@ -357,7 +359,7 @@ export abstract class BaseConfigManager { private _save() { void this.lock.acquire(lockKey, async () => { await this.save({ - ...getConfigSchema(this.isMac).parse({ + ...getConfigSchema({ isMac: this.isMac }).parse({ ...this.config, }), __internal__: { @@ -392,22 +394,22 @@ export abstract class BaseConfigManager { private migrateHotkeySettings(data: ConfigType): ConfigType { const COMBINATION_IS_NONE = HotkeyCombination("####"); const loadedHotkeys = structuredClone(data.hotkeySettings); - const hotkeysWithoutNewCombination = getDefaultHotkeySettings(isMac).map( - (defaultHotkey) => { - const loadedHotkey = loadedHotkeys.find( - (loadedHotkey) => loadedHotkey.action === defaultHotkey.action, - ); - const hotkeyWithoutCombination: HotkeySettingType = { - action: defaultHotkey.action, - combination: COMBINATION_IS_NONE, - }; - return loadedHotkey ?? hotkeyWithoutCombination; - }, - ); + const hotkeysWithoutNewCombination = getDefaultHotkeySettings({ + isMac: this.isMac, + }).map((defaultHotkey) => { + const loadedHotkey = loadedHotkeys.find( + (loadedHotkey) => loadedHotkey.action === defaultHotkey.action, + ); + const hotkeyWithoutCombination: HotkeySettingType = { + action: defaultHotkey.action, + combination: COMBINATION_IS_NONE, + }; + return loadedHotkey ?? hotkeyWithoutCombination; + }); const migratedHotkeys = hotkeysWithoutNewCombination.map((hotkey) => { if (hotkey.combination === COMBINATION_IS_NONE) { const newHotkey = ensureNotNullish( - getDefaultHotkeySettings(isMac).find( + getDefaultHotkeySettings({ isMac: this.isMac }).find( (defaultHotkey) => defaultHotkey.action === hotkey.action, ), ); @@ -434,6 +436,6 @@ export abstract class BaseConfigManager { } protected getDefaultConfig(): ConfigType { - return getConfigSchema(this.isMac).parse({}); + return getConfigSchema({ isMac: this.isMac }).parse({}); } } diff --git a/src/backend/electron/electronConfig.ts b/src/backend/electron/electronConfig.ts index 2c39a083e4..fb61727315 100644 --- a/src/backend/electron/electronConfig.ts +++ b/src/backend/electron/electronConfig.ts @@ -36,7 +36,7 @@ let configManager: ElectronConfigManager | undefined; export function getConfigManager(): ElectronConfigManager { if (!configManager) { - configManager = new ElectronConfigManager(isMac); + configManager = new ElectronConfigManager({ isMac }); } return configManager; } diff --git a/src/backend/electron/engineAndVvppController.ts b/src/backend/electron/engineAndVvppController.ts index 5e0622c3ef..c9342818d5 100644 --- a/src/backend/electron/engineAndVvppController.ts +++ b/src/backend/electron/engineAndVvppController.ts @@ -1,13 +1,16 @@ import path from "path"; import fs from "fs"; +import { ReadableStream } from "node:stream/web"; import log from "electron-log/main"; -import { BrowserWindow, dialog } from "electron"; +import { dialog } from "electron"; import { getConfigManager } from "./electronConfig"; import { getEngineInfoManager } from "./manager/engineInfoManager"; import { getEngineProcessManager } from "./manager/engineProcessManager"; import { getRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { getVvppManager } from "./manager/vvppManager"; +import { getWindowManager } from "./manager/windowManager"; +import { ProgressCallback } from "./type"; import { EngineId, EngineInfo, @@ -45,16 +48,19 @@ export class EngineAndVvppController { /** * VVPPエンジンをインストールする。 */ - async installVvppEngine(vvppPath: string) { + async installVvppEngine( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { try { - await this.vvppManager.install(vvppPath); + await this.vvppManager.install(vvppPath, callbacks); return true; } catch (e) { + log.error(`Failed to install ${vvppPath},`, e); dialog.showErrorBox( "インストールエラー", `${vvppPath} をインストールできませんでした。`, ); - log.error(`Failed to install ${vvppPath},`, e); return false; } } @@ -67,14 +73,13 @@ export class EngineAndVvppController { vvppPath, reloadNeeded, reloadCallback, - win, }: { vvppPath: string; reloadNeeded: boolean; reloadCallback?: () => void; // 再読み込みが必要な場合のコールバック - win: BrowserWindow; // dialog表示に必要。 FIXME: dialog表示関数をDI可能にし、winを削除する }) { - const result = dialog.showMessageBoxSync(win, { + const windowManager = getWindowManager(); + const result = windowManager.showMessageBoxSync({ type: "warning", title: "エンジン追加の確認", message: `この操作はコンピュータに損害を与える可能性があります。エンジンの配布元が信頼できない場合は追加しないでください。`, @@ -89,8 +94,8 @@ export class EngineAndVvppController { await this.installVvppEngine(vvppPath); if (reloadNeeded) { - void dialog - .showMessageBox(win, { + void windowManager + .showMessageBox({ type: "info", title: "再読み込みが必要です", message: @@ -184,6 +189,7 @@ export class EngineAndVvppController { async downloadAndInstallVvppEngine( downloadDir: string, packageInfo: PackageInfo, + callbacks: { onProgress: ProgressCallback<"download" | "install"> }, ) { if (packageInfo.packages.length === 0) { throw new UnreachableError("No packages to download"); @@ -193,27 +199,58 @@ export class EngineAndVvppController { const downloadedPaths: string[] = []; try { // ダウンロード + callbacks.onProgress({ type: "download", progress: 0 }); + + let totalBytes = 0; + packageInfo.packages.forEach((p) => { + totalBytes += p.size; + }); + + let downloadedBytes = 0; await Promise.all( packageInfo.packages.map(async (p) => { - const { url, name, size } = p; - - log.info(`Download ${name} from ${url}, size: ${size}`); - const res = await fetch(url); - const buffer = await res.arrayBuffer(); if (failed) return; // 他のダウンロードが失敗していたら中断 + const { url, name } = p; + log.info(`Download ${name} from ${url}`); + + const res = await fetch(url); + if (!res.ok || res.body == null) + throw new Error(`Failed to download ${name} from ${url}`); const downloadPath = path.join(downloadDir, name); - await fs.promises.writeFile(downloadPath, Buffer.from(buffer)); // TODO: オンメモリじゃなくする - log.info(`Downloaded ${name} to ${downloadPath}`); + const fileStream = fs.createWriteStream(downloadPath); + + // ファイルに書き込む + // NOTE: なぜか型が合わないのでasを使っている + for await (const chunk of res.body as ReadableStream) { + fileStream.write(chunk); + downloadedBytes += chunk.length; + callbacks.onProgress({ + type: "download", + progress: (downloadedBytes / totalBytes) * 100, + }); + } + + // ファイルを確実に閉じる + const { promise, resolve, reject } = Promise.withResolvers(); + fileStream.on("close", resolve); + fileStream.on("error", reject); + fileStream.close(); + await promise; downloadedPaths.push(downloadPath); + log.info(`Downloaded ${name} to ${downloadPath}`); // TODO: ハッシュチェック }), ); // インストール - await this.installVvppEngine(downloadedPaths[0]); + await this.installVvppEngine(downloadedPaths[0], { + onProgress: ({ progress }) => { + callbacks.onProgress({ type: "install", progress }); + }, + }); } catch (e) { failed = true; log.error(`Failed to download and install VVPP engine:`, e); diff --git a/src/backend/electron/fileHelper.ts b/src/backend/electron/fileHelper.ts index 9d80ac0c44..cda62360b6 100644 --- a/src/backend/electron/fileHelper.ts +++ b/src/backend/electron/fileHelper.ts @@ -1,5 +1,7 @@ import fs from "fs"; +import log from "electron-log/main"; import { moveFileSync } from "move-file"; +import { uuid4 } from "@/helpers/random"; /** * 書き込みに失敗したときにファイルが消えないように、 @@ -9,10 +11,19 @@ export function writeFileSafely( path: string, data: string | NodeJS.ArrayBufferView, ) { - const tmpPath = `${path}.tmp`; - fs.writeFileSync(tmpPath, data); + const tmpPath = `${path}-${uuid4()}.tmp`; + fs.writeFileSync(tmpPath, data, { flag: "wx" }); - moveFileSync(tmpPath, path, { - overwrite: true, - }); + try { + moveFileSync(tmpPath, path, { + overwrite: true, + }); + } catch (error) { + if (fs.existsSync(tmpPath)) { + fs.promises.unlink(tmpPath).catch((reason) => { + log.warn("Failed to remove %s\n %o", tmpPath, reason); + }); + } + throw error; + } } diff --git a/src/backend/electron/main.ts b/src/backend/electron/main.ts index 2a810609b8..0bee5375cc 100644 --- a/src/backend/electron/main.ts +++ b/src/backend/electron/main.ts @@ -4,21 +4,11 @@ import path from "path"; import fs from "fs"; import { pathToFileURL } from "url"; -import { - app, - protocol, - BrowserWindow, - dialog, - Menu, - shell, - nativeTheme, - net, -} from "electron"; +import { app, dialog, Menu, nativeTheme, net, protocol, shell } from "electron"; import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; import log from "electron-log/main"; import dayjs from "dayjs"; -import windowStateKeeper from "electron-window-state"; import { hasSupportedGpu } from "./device"; import { getEngineInfoManager, @@ -29,6 +19,10 @@ import { initializeEngineProcessManager, } from "./manager/engineProcessManager"; import { initializeVvppManager, isVvppFile } from "./manager/vvppManager"; +import { + getWindowManager, + initializeWindowManager, +} from "./manager/windowManager"; import configMigration014 from "./configMigration014"; import { initializeRuntimeInfoManager } from "./manager/RuntimeInfoManager"; import { registerIpcMainHandle, ipcMainSendProxy, IpcMainHandle } from "./ipc"; @@ -44,7 +38,6 @@ import { EngineId, TextAsset, } from "@/type/preload"; -import { themes } from "@/domain/theme"; import { isMac } from "@/helpers/platform"; type SingleInstanceLockData = { @@ -110,8 +103,6 @@ if (errorForRemoveBeforeUserDataDir != undefined) { log.error(errorForRemoveBeforeUserDataDir); } -let win: BrowserWindow; - process.on("uncaughtException", (error) => { log.error(error); @@ -151,7 +142,28 @@ protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); -const firstUrl = import.meta.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; +// ソフトウェア起動時はプロトコルを app にする +void app.whenReady().then(() => { + protocol.handle("app", (request) => { + // 読み取り先のファイルがインストールディレクトリ内であることを確認する + // ref: https://www.electronjs.org/ja/docs/latest/api/protocol#protocolhandlescheme-handler + const { pathname } = new URL(request.url); + const pathToServe = path.resolve(path.join(__dirname, pathname)); + const relativePath = path.relative(__dirname, pathToServe); + const isUnsafe = + path.isAbsolute(relativePath) || + relativePath.startsWith("..") || + relativePath === ""; + if (isUnsafe) { + log.error(`Bad Request URL: ${request.url}`); + return new Response("bad", { + status: 400, + headers: { "content-type": "text/html" }, + }); + } + return net.fetch(pathToFileURL(pathToServe).toString()); + }); +}); // engine const vvppEngineDir = path.join(app.getPath("userData"), "vvpp-engines"); @@ -166,6 +178,7 @@ const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { // winが作られる前にエラーが発生した場合はwinへの通知を諦める // FIXME: winが作られた後にエンジンを起動させる + const win = windowManager.win; if (win != undefined) { ipcMainSendProxy.DETECTED_ENGINE_ERROR(win, { engineId }); } else { @@ -175,6 +188,16 @@ const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { dialog.showErrorBox("音声合成エンジンエラー", error.message); }; +const appState = { + willQuit: false, +}; + +initializeWindowManager({ + appStateGetter: () => appState, + isDevelopment, + isTest, + staticDir: __static, +}); initializeRuntimeInfoManager({ runtimeInfoPath: path.join(app.getPath("userData"), "runtime-info.json"), appVersion: app.getVersion(), @@ -187,6 +210,7 @@ initializeEngineProcessManager({ onEngineProcessError }); initializeVvppManager({ vvppEngineDir }); const configManager = getConfigManager(); +const windowManager = getWindowManager(); const engineInfoManager = getEngineInfoManager(); const engineProcessManager = getEngineProcessManager(); const engineAndVvppController = getEngineAndVvppController(); @@ -207,7 +231,7 @@ function openEngineDirectory(engineId: EngineId) { function checkMultiEngineEnabled(): boolean { const enabled = configManager.get("enableMultiEngine"); if (!enabled) { - dialog.showMessageBoxSync(win, { + windowManager.showMessageBoxSync({ type: "info", title: "マルチエンジン機能が無効です", message: `マルチエンジン機能が無効です。vvppファイルを使用するには設定からマルチエンジン機能を有効にしてください。`, @@ -218,148 +242,7 @@ function checkMultiEngineEnabled(): boolean { return enabled; } -const appState = { - willQuit: false, -}; let filePathOnMac: string | undefined = undefined; -// create window -async function createWindow() { - const mainWindowState = windowStateKeeper({ - defaultWidth: 1024, - defaultHeight: 630, - }); - - const currentTheme = configManager.get("currentTheme"); - const backgroundColor = themes.find((value) => value.name == currentTheme) - ?.colors.background; - - win = new BrowserWindow({ - x: mainWindowState.x, - y: mainWindowState.y, - width: mainWindowState.width, - height: mainWindowState.height, - frame: false, - titleBarStyle: "hidden", - trafficLightPosition: { x: 6, y: 4 }, - minWidth: 320, - show: false, - backgroundColor, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - icon: path.join(__static, "icon.png"), - }); - - let projectFilePath = ""; - if (isMac) { - if (filePathOnMac) { - if (filePathOnMac.endsWith(".vvproj")) { - projectFilePath = filePathOnMac; - } - filePathOnMac = undefined; - } - } else { - if (process.argv.length >= 2) { - const filePath = process.argv[1]; - if ( - fs.existsSync(filePath) && - fs.statSync(filePath).isFile() && - filePath.endsWith(".vvproj") - ) { - projectFilePath = filePath; - } - } - } - - // ソフトウェア起動時はプロトコルを app にする - if (import.meta.env.VITE_DEV_SERVER_URL == undefined) { - protocol.handle("app", (request) => { - // 読み取り先のファイルがインストールディレクトリ内であることを確認する - // ref: https://www.electronjs.org/ja/docs/latest/api/protocol#protocolhandlescheme-handler - const { pathname } = new URL(request.url); - const pathToServe = path.resolve(path.join(__dirname, pathname)); - const relativePath = path.relative(__dirname, pathToServe); - const isUnsafe = - path.isAbsolute(relativePath) || - relativePath.startsWith("..") || - relativePath === ""; - if (isUnsafe) { - log.error(`Bad Request URL: ${request.url}`); - return new Response("bad", { - status: 400, - headers: { "content-type": "text/html" }, - }); - } - return net.fetch(pathToFileURL(pathToServe).toString()); - }); - } - - await loadUrl({ projectFilePath }); - - if (isDevelopment && !isTest) win.webContents.openDevTools(); - - win.on("maximize", () => { - ipcMainSendProxy.DETECT_MAXIMIZED(win); - }); - win.on("unmaximize", () => { - ipcMainSendProxy.DETECT_UNMAXIMIZED(win); - }); - win.on("enter-full-screen", () => { - ipcMainSendProxy.DETECT_ENTER_FULLSCREEN(win); - }); - win.on("leave-full-screen", () => { - ipcMainSendProxy.DETECT_LEAVE_FULLSCREEN(win); - }); - win.on("always-on-top-changed", () => { - win.isAlwaysOnTop() - ? ipcMainSendProxy.DETECT_PINNED(win) - : ipcMainSendProxy.DETECT_UNPINNED(win); - }); - win.on("close", (event) => { - if (!appState.willQuit) { - event.preventDefault(); - ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { - closeOrReload: "close", - }); - return; - } - }); - - win.on("resize", () => { - const windowSize = win.getSize(); - ipcMainSendProxy.DETECT_RESIZED(win, { - width: windowSize[0], - height: windowSize[1], - }); - }); - - mainWindowState.manage(win); -} - -/** - * 画面の読み込みを開始する。 - * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 - * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 - * @returns ロードの完了を待つPromise。 - */ -async function loadUrl(obj: { - isMultiEngineOffMode?: boolean; - projectFilePath?: string; -}) { - const url = new URL(firstUrl); - url.searchParams.append( - "isMultiEngineOffMode", - (obj?.isMultiEngineOffMode ?? false).toString(), - ); - url.searchParams.append("projectFilePath", obj?.projectFilePath ?? ""); - return win.loadURL(url.toString()); -} - -// 開始。その他の準備が完了した後に呼ばれる。 -async function start() { - await engineAndVvppController.launchEngines(); - await createWindow(); -} const menuTemplateForMac: Electron.MenuItemConstructorOptions[] = [ { @@ -416,7 +299,7 @@ const retryShowSaveDialogWhileSafeDir = async < */ const showWarningDialog = async () => { const productName = app.getName().toUpperCase(); - const warningResult = await dialog.showMessageBox(win, { + const warningResult = await windowManager.showMessageBox({ message: `指定された保存先は${productName}により自動的に削除される可能性があります。\n他の場所に保存することをおすすめします。`, type: "warning", buttons: ["保存場所を変更", "無視して保存"], @@ -469,41 +352,12 @@ registerIpcMainHandle({ return engineInfoManager.altPortInfos; }, - SHOW_AUDIO_SAVE_DIALOG: async (_, { title, defaultPath }) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { - title, - defaultPath, - filters: [ - { - name: "WAVファイル", - extensions: ["wav"], - }, - ], - properties: ["createDirectory"], - }), - ); - return result.filePath; - }, - - SHOW_TEXT_SAVE_DIALOG: async (_, { title, defaultPath }) => { - const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { - title, - defaultPath, - filters: [{ name: "Text File", extensions: ["txt"] }], - properties: ["createDirectory"], - }), - ); - return result.filePath; - }, - /** * 保存先になるディレクトリを選ぶダイアログを表示する。 */ SHOW_SAVE_DIRECTORY_DIALOG: async (_, { title }) => { const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showOpenDialog(win, { + windowManager.showOpenDialog({ title, properties: [ "openDirectory", @@ -519,7 +373,7 @@ registerIpcMainHandle({ }, SHOW_VVPP_OPEN_DIALOG: async (_, { title, defaultPath }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, defaultPath, filters: [ @@ -535,7 +389,7 @@ registerIpcMainHandle({ * 保存先として選ぶ場合は SHOW_SAVE_DIRECTORY_DIALOG を使うべき。 */ SHOW_OPEN_DIRECTORY_DIALOG: async (_, { title }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, properties: [ "openDirectory", @@ -551,7 +405,7 @@ registerIpcMainHandle({ SHOW_PROJECT_SAVE_DIALOG: async (_, { title, defaultPath }) => { const result = await retryShowSaveDialogWhileSafeDir(() => - dialog.showSaveDialog(win, { + windowManager.showSaveDialog({ title, defaultPath, filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }], @@ -565,7 +419,7 @@ registerIpcMainHandle({ }, SHOW_PROJECT_LOAD_DIALOG: async (_, { title }) => { - const result = await dialog.showOpenDialog(win, { + const result = await windowManager.showOpenDialog({ title, filters: [{ name: "VOICEVOX Project file", extensions: ["vvproj"] }], properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], @@ -577,7 +431,7 @@ registerIpcMainHandle({ }, SHOW_WARNING_DIALOG: (_, { title, message }) => { - return dialog.showMessageBox(win, { + return windowManager.showMessageBox({ type: "warning", title, message, @@ -585,7 +439,7 @@ registerIpcMainHandle({ }, SHOW_ERROR_DIALOG: (_, { title, message }) => { - return dialog.showMessageBox(win, { + return windowManager.showMessageBox({ type: "error", title, message, @@ -593,60 +447,60 @@ registerIpcMainHandle({ }, SHOW_IMPORT_FILE_DIALOG: (_, { title, name, extensions }) => { - return dialog.showOpenDialogSync(win, { + return windowManager.showOpenDialogSync({ title, filters: [{ name: name ?? "Text", extensions: extensions ?? ["txt"] }], properties: ["openFile", "createDirectory", "treatPackageAsDirectory"], })?.[0]; }, + SHOW_EXPORT_FILE_DIALOG: async ( + _, + { title, defaultPath, extensionName, extensions }, + ) => { + const result = await retryShowSaveDialogWhileSafeDir(() => + windowManager.showSaveDialog({ + title, + defaultPath, + filters: [{ name: extensionName, extensions: extensions }], + properties: ["createDirectory"], + }), + ); + return result.filePath; + }, + IS_AVAILABLE_GPU_MODE: () => { return hasSupportedGpu(process.platform); }, IS_MAXIMIZED_WINDOW: () => { - return win.isMaximized(); + return windowManager.isMaximized(); }, CLOSE_WINDOW: () => { appState.willQuit = true; - win.destroy(); + windowManager.destroyWindow(); }, MINIMIZE_WINDOW: () => { - win.minimize(); + windowManager.minimize(); }, TOGGLE_MAXIMIZE_WINDOW: () => { - // 全画面表示中は、全画面表示解除のみを行い、最大化解除処理は実施しない - if (win.isFullScreen()) { - win.setFullScreen(false); - } else if (win.isMaximized()) { - win.unmaximize(); - } else { - win.maximize(); - } + windowManager.toggleMaximizeWindow(); }, TOGGLE_FULLSCREEN: () => { - if (win.isFullScreen()) { - win.setFullScreen(false); - } else { - win.setFullScreen(true); - } + windowManager.toggleFullScreen(); }, /** UIの拡大 */ ZOOM_IN: () => { - win.webContents.setZoomFactor( - Math.min(Math.max(win.webContents.getZoomFactor() + 0.1, 0.5), 3), - ); + windowManager.zoomIn(); }, /** UIの縮小 */ ZOOM_OUT: () => { - win.webContents.setZoomFactor( - Math.min(Math.max(win.webContents.getZoomFactor() - 0.1, 0.5), 3), - ); + windowManager.zoomOut(); }, /** UIの拡大率リセット */ ZOOM_RESET: () => { - win.webContents.setZoomFactor(1); + windowManager.zoomReset(); }, OPEN_LOG_DIRECTORY: () => { void shell.openPath(app.getPath("logs")); @@ -680,18 +534,14 @@ registerIpcMainHandle({ }, ON_VUEX_READY: () => { - win.show(); + windowManager.show(); }, CHECK_FILE_EXISTS: (_, { file }) => { return fs.existsSync(file); }, CHANGE_PIN_WINDOW: () => { - if (win.isAlwaysOnTop()) { - win.setAlwaysOnTop(false); - } else { - win.setAlwaysOnTop(true); - } + windowManager.togglePinWindow(); }, GET_DEFAULT_TOOLBAR_SETTING: () => { @@ -728,24 +578,7 @@ registerIpcMainHandle({ }, RELOAD_APP: async (_, { isMultiEngineOffMode }) => { - win.hide(); // FIXME: ダミーページ表示のほうが良い - - // 一旦適当なURLに飛ばしてページをアンロードする - await win.loadURL("about:blank"); - - log.info("Checking ENGINE status before reload app"); - const engineCleanupResult = engineAndVvppController.cleanupEngines(); - - // エンジンの停止とエンジン終了後処理の待機 - if (engineCleanupResult != "alreadyCompleted") { - await engineCleanupResult; - } - log.info("Post engine kill process done. Now reloading app"); - - await engineAndVvppController.launchEngines(); - - await loadUrl({ isMultiEngineOffMode: !!isMultiEngineOffMode }); - win.show(); + await windowManager.reload(isMultiEngineOffMode); }, WRITE_FILE: (_, { filePath, buffer }) => { @@ -806,7 +639,9 @@ app.on("window-all-closed", () => { app.on("before-quit", async (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { closeOrReload: "close" }); + ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(windowManager.getWindow(), { + closeOrReload: "close", + }); return; } @@ -855,7 +690,7 @@ app.once("will-finish-launching", () => { }); }); -app.on("ready", async () => { +void app.whenReady().then(async () => { await configManager.initialize().catch(async (e) => { log.error(e); @@ -950,7 +785,7 @@ app.on("ready", async () => { await engineAndVvppController.fetchInsallablePackageInfos(); for (const { engineName, packageInfo } of packageInfos) { // インストールするか確認 - const result = dialog.showMessageBoxSync(win, { + const result = dialog.showMessageBoxSync({ type: "info", title: "デフォルトエンジンのインストール", message: `${engineName} をインストールしますか?`, @@ -962,9 +797,20 @@ app.on("ready", async () => { } // ダウンロードしてインストールする + let lastLogTime = 0; // とりあえずログを0.1秒に1回だけ出力する await engineAndVvppController.downloadAndInstallVvppEngine( app.getPath("downloads"), packageInfo, + { + onProgress: ({ type, progress }) => { + if (Date.now() - lastLogTime > 100) { + log.info( + `VVPP default engine progress: ${type}: ${Math.floor(progress)}%`, + ); + lastLogTime = Date.now(); + } + }, + }, ); } @@ -979,6 +825,7 @@ app.on("ready", async () => { } // 多重起動防止 + // TODO: readyを待たずにもっと早く実行すべき if ( !isDevelopment && !isTest && @@ -1000,17 +847,23 @@ app.on("ready", async () => { await engineAndVvppController.installVvppEngineWithWarning({ vvppPath: filePath, reloadNeeded: false, - win, }); } } - void start(); + await engineAndVvppController.launchEngines(); + await windowManager.createWindow(filePath); }); // 他のプロセスが起動したとき、`requestSingleInstanceLock`経由で`rawData`が送信される。 app.on("second-instance", async (_event, _argv, _workDir, rawData) => { const data = rawData as SingleInstanceLockData; + const win = windowManager.win; + if (win == undefined) { + // TODO: 起動シーケンス中の場合はWindowが作られるまで待つ + log.warn("A 'second-instance' event was emitted but there is no window."); + return; + } if (!data.filePath) { log.info("No file path sent"); } else if (isVvppFile(data.filePath)) { @@ -1025,7 +878,6 @@ app.on("second-instance", async (_event, _argv, _workDir, rawData) => { closeOrReload: "reload", }); }, - win, }); } } else if (data.filePath.endsWith(".vvproj")) { @@ -1034,10 +886,7 @@ app.on("second-instance", async (_event, _argv, _workDir, rawData) => { filePath: data.filePath, }); } - if (win) { - if (win.isMinimized()) win.restore(); - win.focus(); - } + windowManager.restoreAndFocus(); }); if (isDevelopment) { diff --git a/src/backend/electron/manager/engineProcessManager.ts b/src/backend/electron/manager/engineProcessManager.ts index 2387b68da5..e02ec2fd30 100644 --- a/src/backend/electron/manager/engineProcessManager.ts +++ b/src/backend/electron/manager/engineProcessManager.ts @@ -99,7 +99,7 @@ export class EngineProcessManager { if (pid != undefined) { const processName = await getProcessNameFromPid(engineHostInfo, pid); log.warn( - `ENGINE ${engineId}: Port ${port} has already been assigned by ${processName} (pid=${pid})`, + `ENGINE ${engineId}: Port ${port} has already been assigned by ${processName ?? "(not found)"} (pid=${pid})`, ); } else { // ポートは使用不可能だがプロセスidは見つからなかった diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index edd3177df7..adaac308bc 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -7,6 +7,7 @@ import { app, dialog } from "electron"; import MultiStream from "multistream"; import { glob } from "glob"; import AsyncLock from "async-lock"; +import { ProgressCallback } from "../type"; import { EngineId, EngineInfo, @@ -32,6 +33,136 @@ export const isVvppFile = (filePath: string) => { const lockKey = "lock-key-for-vvpp-manager"; +/** VVPPファイルが分割されている場合、それらのファイルを取得する */ +async function getArchiveFileParts( + vvppLikeFilePath: string, +): Promise { + let archiveFileParts: string[]; + // 名前.数値.vvpppの場合は分割されているとみなして連結する + if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { + log.log("vvpp is split, finding other parts..."); + const vvpppPathGlob = vvppLikeFilePath + .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") + .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する + const filePaths: string[] = []; + for (const p of await glob(vvpppPathGlob)) { + if (!p.match(/\.[0-9]+\.vvppp$/)) { + continue; + } + log.log(`found ${p}`); + filePaths.push(p); + } + filePaths.sort((a, b) => { + const aMatch = a.match(/\.([0-9]+)\.vvppp$/); + const bMatch = b.match(/\.([0-9]+)\.vvppp$/); + if (aMatch == null || bMatch == null) { + throw new Error(`match is null: a=${a}, b=${b}`); + } + return parseInt(aMatch[1]) - parseInt(bMatch[1]); + }); + archiveFileParts = filePaths; + } else { + log.log("Not a split file"); + archiveFileParts = [vvppLikeFilePath]; + } + return archiveFileParts; +} + +/** 分割されているVVPPファイルを連結して返す */ +async function concatenateVvppFiles( + format: "zip" | "7z", + archiveFileParts: string[], +) { + // -siオプションでの7z解凍はサポートされていないため、 + // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 + log.log(`Concatenating ${archiveFileParts.length} files...`); + const tmpConcatenatedFile = path.join( + app.getPath("temp"), + `vvpp-${new Date().getTime()}.${format}`, + ); + log.log("Temporary file:", tmpConcatenatedFile); + await new Promise((resolve, reject) => { + if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); + const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); + const outputStream = fs.createWriteStream(tmpConcatenatedFile); + new MultiStream(inputStreams) + .pipe(outputStream) + .on("close", () => { + outputStream.close(); + resolve(); + }) + .on("error", reject); + }); + log.log("Concatenated"); + return tmpConcatenatedFile; +} + +/** 7zでファイルを解凍する */ +async function unarchive( + payload: { + archiveFile: string; + outputDir: string; + format: "zip" | "7z"; + }, + callbacks?: { onProgress?: ProgressCallback }, +) { + const { archiveFile, outputDir, format } = payload; + + const args = [ + "x", + "-o" + outputDir, + archiveFile, + "-t" + format, + "-bsp1", // 進捗出力 + ]; + + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; + if (!sevenZipPath) { + throw new Error("7z path is not defined"); + } + if (import.meta.env.PROD) { + sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); + } + log.log("Spawning 7z:", sevenZipPath, args.join(" ")); + await new Promise((resolve, reject) => { + const child = spawn(sevenZipPath, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + child.stdout?.on("data", (data: Buffer) => { + const output = data.toString("utf-8"); + log.info(`7z STDOUT: ${output}`); + + // 進捗を取得 + // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る + // TODO: 出力が変わるかもしれないのでテストが必要 + const progressMatch = output.match( + / *(?\d+)% ?(?\d+)? ?(?.*)/, + ); + if (progressMatch?.groups?.percent) { + callbacks?.onProgress?.({ + progress: parseInt(progressMatch.groups.percent), + }); + } + }); + + child.stderr?.on("data", (data: Buffer) => { + log.error(`7z STDERR: ${data.toString("utf-8")}`); + }); + + child.on("exit", (code) => { + if (code === 0) { + callbacks?.onProgress?.({ progress: 100 }); + resolve(); + } else { + reject(new Error(`7z exited with code ${code}`)); + } + }); + // FIXME: rejectが2回呼ばれることがある + child.on("error", reject); + }); +} + // # 軽い概要 // // フォルダ名:"エンジン名+UUID" @@ -109,38 +240,14 @@ export class VvppManager { private async extractVvpp( vvppLikeFilePath: string, + callbacks?: { onProgress?: ProgressCallback }, ): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> { + callbacks?.onProgress?.({ progress: 0 }); + const nonce = new Date().getTime().toString(); const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce); - let archiveFileParts: string[]; - // 名前.数値.vvpppの場合は分割されているとみなして連結する - if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { - log.log("vvpp is split, finding other parts..."); - const vvpppPathGlob = vvppLikeFilePath - .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") - .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する - const filePaths: string[] = []; - for (const p of await glob(vvpppPathGlob)) { - if (!p.match(/\.[0-9]+\.vvppp$/)) { - continue; - } - log.log(`found ${p}`); - filePaths.push(p); - } - filePaths.sort((a, b) => { - const aMatch = a.match(/\.([0-9]+)\.vvppp$/); - const bMatch = b.match(/\.([0-9]+)\.vvppp$/); - if (aMatch == null || bMatch == null) { - throw new Error(`match is null: a=${a}, b=${b}`); - } - return parseInt(aMatch[1]) - parseInt(bMatch[1]); - }); - archiveFileParts = filePaths; - } else { - log.log("Not a split file"); - archiveFileParts = [vvppLikeFilePath]; - } + const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath); const format = await this.detectFileFormat(archiveFileParts[0]); if (!format) { @@ -153,75 +260,17 @@ export class VvppManager { let archiveFile: string; try { if (archiveFileParts.length > 1) { - // -siオプションでの7z解凍はサポートされていないため、 - // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 - log.log(`Concatenating ${archiveFileParts.length} files...`); - tmpConcatenatedFile = path.join( - app.getPath("temp"), - `vvpp-${new Date().getTime()}.${format}`, + tmpConcatenatedFile = await concatenateVvppFiles( + format, + archiveFileParts, ); - log.log("Temporary file:", tmpConcatenatedFile); archiveFile = tmpConcatenatedFile; - await new Promise((resolve, reject) => { - if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); - const inputStreams = archiveFileParts.map((f) => - fs.createReadStream(f), - ); - const outputStream = fs.createWriteStream(tmpConcatenatedFile); - new MultiStream(inputStreams) - .pipe(outputStream) - .on("close", () => { - outputStream.close(); - resolve(); - }) - .on("error", reject); - }); - log.log("Concatenated"); } else { archiveFile = archiveFileParts[0]; log.log("Single file, not concatenating"); } - const args = ["x", "-o" + outputDir, archiveFile, "-t" + format]; - - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; - if (!sevenZipPath) { - throw new Error("7z path is not defined"); - } - if (import.meta.env.PROD) { - sevenZipPath = path.join( - path.dirname(app.getPath("exe")), - sevenZipPath, - ); - } - log.log( - "Spawning 7z:", - sevenZipPath, - args.map((a) => JSON.stringify(a)).join(" "), - ); - await new Promise((resolve, reject) => { - const child = spawn(sevenZipPath, args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - child.stdout?.on("data", (data: Buffer) => { - log.info(`7z STDOUT: ${data.toString("utf-8")}`); - }); - - child.stderr?.on("data", (data: Buffer) => { - log.error(`7z STDERR: ${data.toString("utf-8")}`); - }); - - child.on("exit", (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`7z exited with code ${code}`)); - } - }); - // FIXME: rejectが2回呼ばれることがある - child.on("error", reject); - }); + await unarchive({ archiveFile, outputDir, format }, callbacks); } finally { if (tmpConcatenatedFile) { log.log("Removing temporary file", tmpConcatenatedFile); @@ -253,11 +302,18 @@ export class VvppManager { /** * 追加 */ - async install(vvppPath: string) { - await this.lock.acquire(lockKey, () => this._install(vvppPath)); + async install( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { + await this.lock.acquire(lockKey, () => this._install(vvppPath, callbacks)); } - private async _install(vvppPath: string) { - const { outputDir, manifest } = await this.extractVvpp(vvppPath); + private async _install( + vvppPath: string, + callbacks?: { onProgress?: ProgressCallback }, + ) { + const { outputDir, manifest } = await this.extractVvpp(vvppPath, callbacks); + const dirName = this.toValidDirName(manifest); const engineDirectory = path.join(this.vvppEngineDir, dirName); const oldEngineDirName = ( diff --git a/src/backend/electron/manager/windowManager.ts b/src/backend/electron/manager/windowManager.ts new file mode 100644 index 0000000000..c0a49734af --- /dev/null +++ b/src/backend/electron/manager/windowManager.ts @@ -0,0 +1,315 @@ +import fs from "fs"; +import path from "path"; +import { + BrowserWindow, + dialog, + MessageBoxOptions, + MessageBoxSyncOptions, + OpenDialogOptions, + OpenDialogSyncOptions, + SaveDialogOptions, +} from "electron"; +import log from "electron-log/main"; +import windowStateKeeper from "electron-window-state"; +import { getConfigManager } from "../electronConfig"; +import { getEngineAndVvppController } from "../engineAndVvppController"; +import { ipcMainSendProxy } from "../ipc"; +import { isMac } from "@/helpers/platform"; +import { themes } from "@/domain/theme"; + +type WindowManagerOption = { + appStateGetter: () => { willQuit: boolean }; + staticDir: string; + isDevelopment: boolean; + isTest: boolean; +}; + +class WindowManager { + private _win: BrowserWindow | undefined; + private appStateGetter: () => { willQuit: boolean }; + private staticDir: string; + private isDevelopment: boolean; + private isTest: boolean; + + constructor(payload: WindowManagerOption) { + this.appStateGetter = payload.appStateGetter; + this.staticDir = payload.staticDir; + this.isDevelopment = payload.isDevelopment; + this.isTest = payload.isTest; + } + + /** + * BrowserWindowを取得する + */ + public get win() { + return this._win; + } + + /** + * BrowserWindowを取得するが存在しない場合は例外を投げる + */ + public getWindow() { + if (this._win == undefined) { + throw new Error("_win == undefined"); + } + return this._win; + } + + public async createWindow(filePathOnMac: string | undefined) { + if (this.win != undefined) { + throw new Error("Window has already been created"); + } + const mainWindowState = windowStateKeeper({ + defaultWidth: 1024, + defaultHeight: 630, + }); + + const configManager = getConfigManager(); + const currentTheme = configManager.get("currentTheme"); + const backgroundColor = themes.find((value) => value.name == currentTheme) + ?.colors.background; + + const win = new BrowserWindow({ + x: mainWindowState.x, + y: mainWindowState.y, + width: mainWindowState.width, + height: mainWindowState.height, + frame: false, + titleBarStyle: "hidden", + trafficLightPosition: { x: 6, y: 4 }, + minWidth: 320, + show: false, + backgroundColor, + webPreferences: { + preload: path.join(__dirname, "preload.js"), + }, + icon: path.join(this.staticDir, "icon.png"), + }); + + let projectFilePath = ""; + if (isMac) { + if (filePathOnMac) { + if (filePathOnMac.endsWith(".vvproj")) { + projectFilePath = filePathOnMac; + } + filePathOnMac = undefined; + } + } else { + if (process.argv.length >= 2) { + const filePath = process.argv[1]; + if ( + fs.existsSync(filePath) && + fs.statSync(filePath).isFile() && + filePath.endsWith(".vvproj") + ) { + projectFilePath = filePath; + } + } + } + + win.on("maximize", () => { + ipcMainSendProxy.DETECT_MAXIMIZED(win); + }); + win.on("unmaximize", () => { + ipcMainSendProxy.DETECT_UNMAXIMIZED(win); + }); + win.on("enter-full-screen", () => { + ipcMainSendProxy.DETECT_ENTER_FULLSCREEN(win); + }); + win.on("leave-full-screen", () => { + ipcMainSendProxy.DETECT_LEAVE_FULLSCREEN(win); + }); + win.on("always-on-top-changed", () => { + win.isAlwaysOnTop() + ? ipcMainSendProxy.DETECT_PINNED(win) + : ipcMainSendProxy.DETECT_UNPINNED(win); + }); + win.on("close", (event) => { + const appState = this.appStateGetter(); + if (!appState.willQuit) { + event.preventDefault(); + ipcMainSendProxy.CHECK_EDITED_AND_NOT_SAVE(win, { + closeOrReload: "close", + }); + return; + } + }); + win.on("closed", () => { + this._win = undefined; + }); + win.on("resize", () => { + const windowSize = win.getSize(); + ipcMainSendProxy.DETECT_RESIZED(win, { + width: windowSize[0], + height: windowSize[1], + }); + }); + mainWindowState.manage(win); + this._win = win; + + await this.load({ projectFilePath }); + + if (this.isDevelopment && !this.isTest) win.webContents.openDevTools(); + } + + /** + * 画面の読み込みを開始する。 + * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 + * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 + * @returns ロードの完了を待つPromise。 + */ + public async load(obj: { + isMultiEngineOffMode?: boolean; + projectFilePath?: string; + }) { + const win = this.getWindow(); + const firstUrl = + import.meta.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; + const url = new URL(firstUrl); + url.searchParams.append( + "isMultiEngineOffMode", + (obj?.isMultiEngineOffMode ?? false).toString(), + ); + url.searchParams.append("projectFilePath", obj?.projectFilePath ?? ""); + await win.loadURL(url.toString()); + } + + public async reload(isMultiEngineOffMode: boolean | undefined) { + const win = this.getWindow(); + win.hide(); // FIXME: ダミーページ表示のほうが良い + + // 一旦適当なURLに飛ばしてページをアンロードする + await win.loadURL("about:blank"); + + log.info("Checking ENGINE status before reload app"); + const engineAndVvppController = getEngineAndVvppController(); + const engineCleanupResult = engineAndVvppController.cleanupEngines(); + + // エンジンの停止とエンジン終了後処理の待機 + if (engineCleanupResult != "alreadyCompleted") { + await engineCleanupResult; + } + log.info("Post engine kill process done. Now reloading app"); + + await engineAndVvppController.launchEngines(); + + await this.load({ + isMultiEngineOffMode: !!isMultiEngineOffMode, + }); + win.show(); + } + + public togglePinWindow() { + const win = this.getWindow(); + if (win.isAlwaysOnTop()) { + win.setAlwaysOnTop(false); + } else { + win.setAlwaysOnTop(true); + } + } + + public toggleMaximizeWindow() { + const win = this.getWindow(); + // 全画面表示中は、全画面表示解除のみを行い、最大化解除処理は実施しない + if (win.isFullScreen()) { + win.setFullScreen(false); + } else if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + } + + public toggleFullScreen() { + const win = this.getWindow(); + if (win.isFullScreen()) { + win.setFullScreen(false); + } else { + win.setFullScreen(true); + } + } + + public restoreAndFocus() { + const win = this.getWindow(); + if (win.isMinimized()) win.restore(); + win.focus(); + } + + public zoomIn() { + const win = this.getWindow(); + win.webContents.setZoomFactor( + Math.min(Math.max(win.webContents.getZoomFactor() + 0.1, 0.5), 3), + ); + } + + public zoomOut() { + const win = this.getWindow(); + win.webContents.setZoomFactor( + Math.min(Math.max(win.webContents.getZoomFactor() - 0.1, 0.5), 3), + ); + } + + public zoomReset() { + const win = this.getWindow(); + win.webContents.setZoomFactor(1); + } + + public destroyWindow() { + this.getWindow().destroy(); + } + + public show() { + this.getWindow().show(); + } + + public minimize() { + this.getWindow().minimize(); + } + + public isMaximized() { + return this.getWindow().isMaximized(); + } + + public showOpenDialogSync(options: OpenDialogSyncOptions) { + return this._win == undefined + ? dialog.showOpenDialogSync(options) + : dialog.showOpenDialogSync(this.getWindow(), options); + } + + public showOpenDialog(options: OpenDialogOptions) { + return this._win == undefined + ? dialog.showOpenDialog(options) + : dialog.showOpenDialog(this.getWindow(), options); + } + + public showSaveDialog(options: SaveDialogOptions) { + return this._win == undefined + ? dialog.showSaveDialog(options) + : dialog.showSaveDialog(this.getWindow(), options); + } + + public showMessageBoxSync(options: MessageBoxSyncOptions) { + return this._win == undefined + ? dialog.showMessageBoxSync(options) + : dialog.showMessageBoxSync(this.getWindow(), options); + } + + public showMessageBox(options: MessageBoxOptions) { + return this._win == undefined + ? dialog.showMessageBox(options) + : dialog.showMessageBox(this.getWindow(), options); + } +} + +let windowManager: WindowManager | undefined; + +export function initializeWindowManager(payload: WindowManagerOption) { + windowManager = new WindowManager(payload); +} + +export function getWindowManager() { + if (windowManager == undefined) { + throw new Error("WindowManager is not initialized"); + } + return windowManager; +} diff --git a/src/backend/electron/portHelper.ts b/src/backend/electron/portHelper.ts index cb68f9c15b..73fc05eefe 100644 --- a/src/backend/electron/portHelper.ts +++ b/src/backend/electron/portHelper.ts @@ -148,12 +148,12 @@ export async function getPidFromPort( export async function getProcessNameFromPid( hostInfo: HostInfo, pid: number, -): Promise { +): Promise { portLog(hostInfo.port, `Getting process name from pid=${pid}...`); const exec = isWindows ? { - cmd: "wmic", - args: ["process", "where", `"ProcessID=${pid}"`, "get", "name"], + cmd: "tasklist", + args: ["/FI", `"PID eq ${pid}"`, "/NH"], } : { cmd: "ps", @@ -165,15 +165,22 @@ export async function getProcessNameFromPid( /* * ex) stdout: * ``` - * Name - * node.exe + * + * node.exe 25180 Console 1 86,544 K * ``` * -> `node.exe` */ - const processName = isWindows ? stdout.split("\n")[1] : stdout; + const processName = ( + isWindows ? stdout.split("\r\n").at(1)?.split(/ +/)?.at(0) : stdout + )?.trim(); - portLog(hostInfo.port, `Found process name: ${processName}`); - return processName.trim(); + if (processName == undefined) { + portWarn(hostInfo.port, `Not found process name from pid=${pid}!`); + return undefined; + } else { + portLog(hostInfo.port, `Found process name: ${processName}`); + return processName; + } } /** diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index 2d3015b124..23a3370a4f 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import type { IpcRendererInvoke } from "./ipc"; import { ConfigType, @@ -33,17 +33,6 @@ const api: Sandbox = { return await ipcRendererInvokeProxy.GET_ALT_PORT_INFOS(); }, - showAudioSaveDialog: ({ title, defaultPath }) => { - return ipcRendererInvokeProxy.SHOW_AUDIO_SAVE_DIALOG({ - title, - defaultPath, - }); - }, - - showTextSaveDialog: ({ title, defaultPath }) => { - return ipcRendererInvokeProxy.SHOW_TEXT_SAVE_DIALOG({ title, defaultPath }); - }, - showSaveDirectoryDialog: ({ title }) => { return ipcRendererInvokeProxy.SHOW_SAVE_DIRECTORY_DIALOG({ title }); }, @@ -75,6 +64,15 @@ const api: Sandbox = { }); }, + showExportFileDialog: ({ title, defaultPath, extensionName, extensions }) => { + return ipcRendererInvokeProxy.SHOW_EXPORT_FILE_DIALOG({ + title, + defaultPath, + extensionName, + extensions, + }); + }, + writeFile: async ({ filePath, buffer }) => { return await ipcRendererInvokeProxy.WRITE_FILE({ filePath, buffer }); }, @@ -230,6 +228,11 @@ const api: Sandbox = { reloadApp: async ({ isMultiEngineOffMode }) => { await ipcRendererInvokeProxy.RELOAD_APP({ isMultiEngineOffMode }); }, + + /** webUtils.getPathForFileを呼ぶ */ + getPathForFile: (file) => { + return webUtils.getPathForFile(file); + }, }; contextBridge.exposeInMainWorld(SandboxKey, api); diff --git a/src/backend/electron/type.ts b/src/backend/electron/type.ts new file mode 100644 index 0000000000..63c83eedca --- /dev/null +++ b/src/backend/electron/type.ts @@ -0,0 +1,6 @@ +/** 進捗を返すコールバック */ +export type ProgressCallback = [T] extends [ + void, +] + ? (payload: { progress: number }) => void + : (payload: { type: T; progress: number }) => void; diff --git a/src/components/App.vue b/src/components/App.vue index f49b89c372..51cdbfea1b 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -165,6 +165,7 @@ onMounted(async () => { // プロジェクトファイルが指定されていればロード if (typeof projectFilePath === "string" && projectFilePath !== "") { isProjectFileLoaded.value = await store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); } else { diff --git a/src/components/Dialog/Dialog.ts b/src/components/Dialog/Dialog.ts index a5aa17a655..38edc9b31f 100644 --- a/src/components/Dialog/Dialog.ts +++ b/src/components/Dialog/Dialog.ts @@ -13,7 +13,7 @@ import { import { DotNotationDispatch } from "@/store/vuex"; import { withProgress } from "@/store/ui"; -type MediaType = "audio" | "text" | "label"; +type MediaType = "audio" | "text" | "project" | "label"; export type TextDialogResult = "OK" | "CANCEL"; export type MessageDialogOptions = { @@ -302,6 +302,7 @@ const showWriteSuccessNotify = ({ const mediaTypeNames: Record = { audio: "音声", text: "テキスト", + project: "プロジェクト", label: "labファイル", }; void actions.SHOW_NOTIFY_AND_NOT_SHOW_AGAIN_BUTTON({ diff --git a/src/components/Dialog/DictionaryEditWordDialog.vue b/src/components/Dialog/DictionaryEditWordDialog.vue index 5d51cfa6bf..7b916a9bdc 100644 --- a/src/components/Dialog/DictionaryEditWordDialog.vue +++ b/src/components/Dialog/DictionaryEditWordDialog.vue @@ -296,12 +296,12 @@ const saveWord = async () => { accentType: accent, priority: wordPriority.value, }); - } catch { + } catch (e) { void store.actions.SHOW_ALERT_DIALOG({ title: "単語の更新に失敗しました", message: "エンジンの再起動をお試しください。", }); - return; + throw e; } } else { try { @@ -313,12 +313,12 @@ const saveWord = async () => { priority: wordPriority.value, }), ); - } catch { + } catch (e) { void store.actions.SHOW_ALERT_DIALOG({ title: "単語の登録に失敗しました", message: "エンジンの再起動をお試しください。", }); - return; + throw e; } } await loadingDictProcess(); diff --git a/src/components/Dialog/HotkeySettingDialog.vue b/src/components/Dialog/HotkeySettingDialog.vue index 47ebf88726..05fc4c48ec 100644 --- a/src/components/Dialog/HotkeySettingDialog.vue +++ b/src/components/Dialog/HotkeySettingDialog.vue @@ -227,7 +227,7 @@ const setHotkeyDialogOpened = () => { }; const isDefaultCombination = (action: string) => { - const defaultSetting = getDefaultHotkeySettings(isMac).find( + const defaultSetting = getDefaultHotkeySettings({ isMac }).find( (value) => value.action === action, ); const hotkeySetting = hotkeySettings.value.find( @@ -245,7 +245,7 @@ const resetHotkey = async (action: string) => { if (result !== "OK") return; - const setting = getDefaultHotkeySettings(isMac).find( + const setting = getDefaultHotkeySettings({ isMac }).find( (value) => value.action == action, ); if (setting == undefined) { diff --git a/src/components/Menu/MenuBar/MenuBar.vue b/src/components/Menu/MenuBar/MenuBar.vue index 4ad68272b9..271a4854ca 100644 --- a/src/components/Menu/MenuBar/MenuBar.vue +++ b/src/components/Menu/MenuBar/MenuBar.vue @@ -157,7 +157,7 @@ const saveProjectAs = async () => { const importProject = () => { if (!uiLocked.value) { - void store.actions.LOAD_PROJECT_FILE({}); + void store.actions.LOAD_PROJECT_FILE({ type: "dialog" }); } }; @@ -198,6 +198,7 @@ const updateRecentProjects = async () => { label: projectFilePath, onClick: () => { void store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); }, diff --git a/src/components/Sing/menuBarData.ts b/src/components/Sing/menuBarData.ts index 3ddeaaee4e..6f43491f5a 100644 --- a/src/components/Sing/menuBarData.ts +++ b/src/components/Sing/menuBarData.ts @@ -2,6 +2,7 @@ import { computed } from "vue"; import { useStore } from "@/store"; import { MenuItemData } from "@/components/Menu/type"; import { useRootMiscSetting } from "@/composables/useRootMiscSetting"; +import { ExportSongProjectFileType } from "@/store/type"; import { notifyResult } from "@/components/Dialog/Dialog"; export const useMenuBarData = () => { @@ -25,6 +26,23 @@ export const useMenuBarData = () => { }); }; + const exportSongProject = async ( + fileType: ExportSongProjectFileType, + fileTypeLabel: string, + ) => { + if (uiLocked.value) return; + const result = await store.actions.EXPORT_SONG_PROJECT({ + fileType, + fileTypeLabel, + }); + notifyResult( + result, + "project", + store.actions, + store.state.confirmedTips.notifyOnGenerate, + ); + }; + const exportLabelFile = async () => { const results = await store.actions.EXPORT_LABEL_FILES({}); @@ -60,12 +78,35 @@ export const useMenuBarData = () => { { type: "separator" }, { type: "button", - label: "インポート", + label: "プロジェクトをインポート", onClick: () => { void importExternalSongProject(); }, disableWhenUiLocked: true, }, + { + type: "root", + label: "プロジェクトをエクスポート", + subMenu: ( + [ + ["smf", "MIDI (SMF)"], + ["musicxml", "MusicXML"], + ["ufdata", "Utaformatix"], + ["ust", "UTAU"], + ] satisfies [fileType: ExportSongProjectFileType, label: string][] + ).map( + ([fileType, label]) => + ({ + type: "button", + label, + onClick: () => { + void exportSongProject(fileType, label); + }, + disableWhenUiLocked: true, + }) satisfies MenuItemData, + ), + disableWhenUiLocked: true, + }, ]); // 「編集」メニュー diff --git a/src/components/Talk/TalkEditor.vue b/src/components/Talk/TalkEditor.vue index ecf68b6041..dda58caba7 100644 --- a/src/components/Talk/TalkEditor.vue +++ b/src/components/Talk/TalkEditor.vue @@ -143,6 +143,7 @@ import { actionPostfixSelectNthCharacter, HotkeyActionNameType, } from "@/domain/hotkeyAction"; +import { isElectron } from "@/helpers/platform"; const props = defineProps<{ isEnginesReady: boolean; @@ -574,13 +575,27 @@ const dragEventCounter = ref(0); const loadDraggedFile = (event: { dataTransfer: DataTransfer | null }) => { if (!event.dataTransfer || event.dataTransfer.files.length === 0) return; const file = event.dataTransfer.files[0]; + + // electronの場合のみファイルパスを取得できる + const filePath = isElectron ? window.backend.getPathForFile(file) : undefined; + switch (path.extname(file.name)) { case ".txt": - void store.actions.COMMAND_IMPORT_FROM_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "path", filePath }); + } else { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "file", file }); + } break; + case ".vvproj": - void store.actions.LOAD_PROJECT_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.LOAD_PROJECT_FILE({ type: "path", filePath }); + } else { + void store.actions.LOAD_PROJECT_FILE({ type: "file", file }); + } break; + default: void store.actions.SHOW_ALERT_DIALOG({ title: "対応していないファイルです", diff --git a/src/components/Talk/ToolBar.vue b/src/components/Talk/ToolBar.vue index 5bbb6e1e2c..12b1bbbc86 100644 --- a/src/components/Talk/ToolBar.vue +++ b/src/components/Talk/ToolBar.vue @@ -145,7 +145,7 @@ const saveProject = async () => { await store.actions.SAVE_PROJECT_FILE({ overwrite: true }); }; const importTextFile = () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }; const usableButtons: Record< diff --git a/src/components/Talk/menuBarData.ts b/src/components/Talk/menuBarData.ts index 5a9b73c2ea..cea0fd3a8c 100644 --- a/src/components/Talk/menuBarData.ts +++ b/src/components/Talk/menuBarData.ts @@ -46,7 +46,7 @@ export const useMenuBarData = () => { type: "button", label: "テキスト読み込み", onClick: () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }, disableWhenUiLocked: true, }, diff --git a/src/domain/defaultEngine/envEngineInfo.ts b/src/domain/defaultEngine/envEngineInfo.ts index f45098c264..7aabe35227 100644 --- a/src/domain/defaultEngine/envEngineInfo.ts +++ b/src/domain/defaultEngine/envEngineInfo.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { engineIdSchema } from "@/type/preload"; +import { isElectron } from "@/helpers/platform"; /** .envに書くデフォルトエンジン情報のスキーマ */ const envEngineInfoSchema = z @@ -32,10 +33,28 @@ const envEngineInfoSchema = z ); type EnvEngineInfoType = z.infer; +/** + * デフォルトエンジン情報の環境変数を取得する + * electronのときはプロセスの環境変数を優先する。 + * NOTE: electronテスト環境を切り替えるため。テスト環境が1本化されればimport.meta.envを使う。 + */ +function getDefaultEngineInfosEnv(): string { + let engineInfos; + if (isElectron) { + engineInfos = process?.env?.VITE_DEFAULT_ENGINE_INFOS; + } + if (engineInfos == undefined) { + engineInfos = import.meta.env.VITE_DEFAULT_ENGINE_INFOS; + } + if (engineInfos == undefined) { + engineInfos = "[]"; + } + return engineInfos; +} + /** .envからデフォルトエンジン情報を読み込む */ export function loadEnvEngineInfos(): EnvEngineInfoType[] { - const defaultEngineInfosEnv = - import.meta.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]"; + const defaultEngineInfosEnv = getDefaultEngineInfosEnv(); // FIXME: 「.envを書き換えてください」というログを出したい // NOTE: domainディレクトリなのでログを出す方法がなく、Errorオプションのcauseを用いてもelectron-logがcauseのログを出してくれない diff --git a/src/domain/hotkeyAction.ts b/src/domain/hotkeyAction.ts index 41ddd8f850..6c92c63556 100644 --- a/src/domain/hotkeyAction.ts +++ b/src/domain/hotkeyAction.ts @@ -60,7 +60,11 @@ export const hotkeySettingSchema = z.object({ }); export type HotkeySettingType = z.infer; -export function getDefaultHotkeySettings(isMac: boolean): HotkeySettingType[] { +export function getDefaultHotkeySettings({ + isMac, +}: { + isMac: boolean; +}): HotkeySettingType[] { return [ { action: "音声書き出し", diff --git a/src/helpers/typedEntries.ts b/src/helpers/typedEntries.ts new file mode 100644 index 0000000000..a8a07c115b --- /dev/null +++ b/src/helpers/typedEntries.ts @@ -0,0 +1,15 @@ +/** 型付きのObject.entries */ +export const objectEntries = >( + obj: T, +): [keyof T, T[keyof T]][] => { + return Object.entries(obj) as [keyof T, T[keyof T]][]; +}; + +/** 型付きのObject.fromEntries */ +export const objectFromEntries = < + const T extends ReadonlyArray, +>( + entries: T, +): { [K in T[number] as K[0]]: K[1] } => { + return Object.fromEntries(entries) as { [K in T[number] as K[0]]: K[1] }; +}; diff --git a/src/infrastructures/EngineConnector.ts b/src/infrastructures/EngineConnector.ts index 7ddede8273..be2870b803 100644 --- a/src/infrastructures/EngineConnector.ts +++ b/src/infrastructures/EngineConnector.ts @@ -1,3 +1,5 @@ +import { createEngineUrl, EngineUrlParams } from "@/domain/url"; +import { createOpenAPIEngineMock } from "@/mock/engineMock"; import { Configuration, DefaultApi, DefaultApiInterface } from "@/openapi"; export interface IEngineConnectorFactory { @@ -6,6 +8,7 @@ export interface IEngineConnectorFactory { instance: (host: string) => DefaultApiInterface; } +// 通常エンジン const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { const instanceMapper: Record = {}; return { @@ -21,6 +24,45 @@ const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { }, }; }; - export const OpenAPIEngineConnectorFactory = OpenAPIEngineConnectorFactoryImpl(); + +// モック用エンジン +const OpenAPIMockEngineConnectorFactoryImpl = (): IEngineConnectorFactory => { + let mockInstance: DefaultApiInterface | undefined; + return { + instance: () => { + if (!mockInstance) { + mockInstance = createOpenAPIEngineMock(); + } + return mockInstance; + }, + }; +}; +export const OpenAPIMockEngineConnectorFactory = + OpenAPIMockEngineConnectorFactoryImpl(); + +// 通常エンジンとモック用エンジンの両対応 +// モック用エンジンのURLのときはモックを、そうじゃないときは通常エンジンを返す。 +const OpenAPIEngineAndMockConnectorFactoryImpl = + (): IEngineConnectorFactory => { + // モック用エンジンのURLは `mock://mock` とする + const mockUrlParams: EngineUrlParams = { + protocol: "mock:", + hostname: "mock", + port: "", + pathname: "", + }; + + return { + instance: (host: string) => { + if (host == createEngineUrl(mockUrlParams)) { + return OpenAPIMockEngineConnectorFactory.instance(host); + } else { + return OpenAPIEngineConnectorFactory.instance(host); + } + }, + }; + }; +export const OpenAPIEngineAndMockConnectorFactory = + OpenAPIEngineAndMockConnectorFactoryImpl(); diff --git a/src/mock/engineMock/talkModelMock.ts b/src/mock/engineMock/talkModelMock.ts index 016dbc30e7..cb55943a6d 100644 --- a/src/mock/engineMock/talkModelMock.ts +++ b/src/mock/engineMock/talkModelMock.ts @@ -182,16 +182,23 @@ export async function textToActtentPhrasesMock(text: string, styleId: number) { for (const token of tokens) { // 記号の場合は無音を入れて区切る if (token.pos == "記号") { - if (textPhrase.length == 0) continue; - - const accentPhrase = textToAccentPhraseMock(textPhrase); - accentPhrase.pauseMora = { + const pauseMora = { text: "、", vowel: "pau", vowelLength: 1 - 1 / (accentPhrases.length + 1), pitch: 0, }; - accentPhrases.push(accentPhrase); + + // テキストが空の場合は前のアクセント句に無音を追加、空でない場合は新しいアクセント句を追加 + let accentPhrase: AccentPhrase; + if (textPhrase.length === 0) { + accentPhrase = accentPhrases[accentPhrases.length - 1]; + } else { + accentPhrase = textToAccentPhraseMock(textPhrase); + accentPhrases.push(accentPhrase); + } + accentPhrase.pauseMora = pauseMora; + textPhrase = ""; continue; } diff --git a/src/openapi/.openapi-generator/FILES b/src/openapi/.openapi-generator/FILES index 06624bd77c..44234c1771 100644 --- a/src/openapi/.openapi-generator/FILES +++ b/src/openapi/.openapi-generator/FILES @@ -4,6 +4,7 @@ index.ts models/AccentPhrase.ts models/AudioQuery.ts models/BaseLibraryInfo.ts +models/BodySingFrameF0SingFrameF0Post.ts models/BodySingFrameVolumeSingFrameVolumePost.ts models/CorsPolicyMode.ts models/DownloadableLibraryInfo.ts diff --git a/src/openapi/apis/DefaultApi.ts b/src/openapi/apis/DefaultApi.ts index fbbdd86629..8718b36686 100644 --- a/src/openapi/apis/DefaultApi.ts +++ b/src/openapi/apis/DefaultApi.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -17,6 +17,7 @@ import * as runtime from '../runtime'; import type { AccentPhrase, AudioQuery, + BodySingFrameF0SingFrameF0Post, BodySingFrameVolumeSingFrameVolumePost, CorsPolicyMode, DownloadableLibraryInfo, @@ -39,6 +40,8 @@ import { AccentPhraseToJSON, AudioQueryFromJSON, AudioQueryToJSON, + BodySingFrameF0SingFrameF0PostFromJSON, + BodySingFrameF0SingFrameF0PostToJSON, BodySingFrameVolumeSingFrameVolumePostFromJSON, BodySingFrameVolumeSingFrameVolumePostToJSON, CorsPolicyModeFromJSON, @@ -197,6 +200,12 @@ export interface SingFrameAudioQuerySingFrameAudioQueryPostRequest { coreVersion?: string; } +export interface SingFrameF0SingFrameF0PostRequest { + speaker: number; + bodySingFrameF0SingFrameF0Post: BodySingFrameF0SingFrameF0Post; + coreVersion?: string; +} + export interface SingFrameVolumeSingFrameVolumePostRequest { speaker: number; bodySingFrameVolumeSingFrameVolumePost: BodySingFrameVolumeSingFrameVolumePost; @@ -766,7 +775,24 @@ export interface DefaultApiInterface { /** * - * @summary スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * @summary 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + * @param {number} speaker + * @param {BodySingFrameF0SingFrameF0Post} bodySingFrameF0SingFrameF0Post + * @param {string} [coreVersion] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApiInterface + */ + singFrameF0SingFrameF0PostRaw(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + singFrameF0SingFrameF0Post(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; + + /** + * + * @summary 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る * @param {number} speaker * @param {BodySingFrameVolumeSingFrameVolumePost} bodySingFrameVolumeSingFrameVolumePost * @param {string} [coreVersion] @@ -777,7 +803,7 @@ export interface DefaultApiInterface { singFrameVolumeSingFrameVolumePostRaw(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>>; /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ singFrameVolumeSingFrameVolumePost(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; @@ -2188,7 +2214,52 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { } /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + async singFrameF0SingFrameF0PostRaw(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + if (requestParameters.speaker === null || requestParameters.speaker === undefined) { + throw new runtime.RequiredError('speaker','Required parameter requestParameters.speaker was null or undefined when calling singFrameF0SingFrameF0Post.'); + } + + if (requestParameters.bodySingFrameF0SingFrameF0Post === null || requestParameters.bodySingFrameF0SingFrameF0Post === undefined) { + throw new runtime.RequiredError('bodySingFrameF0SingFrameF0Post','Required parameter requestParameters.bodySingFrameF0SingFrameF0Post was null or undefined when calling singFrameF0SingFrameF0Post.'); + } + + const queryParameters: any = {}; + + if (requestParameters.speaker !== undefined) { + queryParameters['speaker'] = requestParameters.speaker; + } + + if (requestParameters.coreVersion !== undefined) { + queryParameters['core_version'] = requestParameters.coreVersion; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters['Content-Type'] = 'application/json'; + + const response = await this.request({ + path: `/sing_frame_f0`, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: BodySingFrameF0SingFrameF0PostToJSON(requestParameters.bodySingFrameF0SingFrameF0Post), + }, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの基本周波数を得る + */ + async singFrameF0SingFrameF0Post(requestParameters: SingFrameF0SingFrameF0PostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.singFrameF0SingFrameF0PostRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ async singFrameVolumeSingFrameVolumePostRaw(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { if (requestParameters.speaker === null || requestParameters.speaker === undefined) { @@ -2225,7 +2296,7 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { } /** - * スコア・歌唱音声合成用のクエリからフレームごとの音量を得る + * 楽譜・歌唱音声合成用のクエリからフレームごとの音量を得る */ async singFrameVolumeSingFrameVolumePost(requestParameters: SingFrameVolumeSingFrameVolumePostRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { const response = await this.singFrameVolumeSingFrameVolumePostRaw(requestParameters, initOverrides); diff --git a/src/openapi/models/AccentPhrase.ts b/src/openapi/models/AccentPhrase.ts index 2df96050d3..88c7fc2b95 100644 --- a/src/openapi/models/AccentPhrase.ts +++ b/src/openapi/models/AccentPhrase.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -39,7 +39,7 @@ export interface AccentPhrase { */ accent: number; /** - * + * 後ろに無音を付けるかどうか * @type {Mora} * @memberof AccentPhrase */ diff --git a/src/openapi/models/AudioQuery.ts b/src/openapi/models/AudioQuery.ts index 8eeecebf6a..afb9854250 100644 --- a/src/openapi/models/AudioQuery.ts +++ b/src/openapi/models/AudioQuery.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/BaseLibraryInfo.ts b/src/openapi/models/BaseLibraryInfo.ts index 3f3c043048..8475c8948f 100644 --- a/src/openapi/models/BaseLibraryInfo.ts +++ b/src/openapi/models/BaseLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts b/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts new file mode 100644 index 0000000000..601aabd7e4 --- /dev/null +++ b/src/openapi/models/BodySingFrameF0SingFrameF0Post.ts @@ -0,0 +1,88 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * DUMMY Engine + * DUMMY の音声合成エンジンです。 + * + * The version of the OpenAPI document: latest + * + * + * 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'; +import type { FrameAudioQuery } from './FrameAudioQuery'; +import { + FrameAudioQueryFromJSON, + FrameAudioQueryFromJSONTyped, + FrameAudioQueryToJSON, +} from './FrameAudioQuery'; +import type { Score } from './Score'; +import { + ScoreFromJSON, + ScoreFromJSONTyped, + ScoreToJSON, +} from './Score'; + +/** + * + * @export + * @interface BodySingFrameF0SingFrameF0Post + */ +export interface BodySingFrameF0SingFrameF0Post { + /** + * + * @type {Score} + * @memberof BodySingFrameF0SingFrameF0Post + */ + score: Score; + /** + * + * @type {FrameAudioQuery} + * @memberof BodySingFrameF0SingFrameF0Post + */ + frameAudioQuery: FrameAudioQuery; +} + +/** + * Check if a given object implements the BodySingFrameF0SingFrameF0Post interface. + */ +export function instanceOfBodySingFrameF0SingFrameF0Post(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "score" in value; + isInstance = isInstance && "frameAudioQuery" in value; + + return isInstance; +} + +export function BodySingFrameF0SingFrameF0PostFromJSON(json: any): BodySingFrameF0SingFrameF0Post { + return BodySingFrameF0SingFrameF0PostFromJSONTyped(json, false); +} + +export function BodySingFrameF0SingFrameF0PostFromJSONTyped(json: any, ignoreDiscriminator: boolean): BodySingFrameF0SingFrameF0Post { + if ((json === undefined) || (json === null)) { + return json; + } + return { + + 'score': ScoreFromJSON(json['score']), + 'frameAudioQuery': FrameAudioQueryFromJSON(json['frame_audio_query']), + }; +} + +export function BodySingFrameF0SingFrameF0PostToJSON(value?: BodySingFrameF0SingFrameF0Post | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + + 'score': ScoreToJSON(value.score), + 'frame_audio_query': FrameAudioQueryToJSON(value.frameAudioQuery), + }; +} + diff --git a/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts b/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts index 8b683c2abf..d4d5e6d644 100644 --- a/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts +++ b/src/openapi/models/BodySingFrameVolumeSingFrameVolumePost.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/CorsPolicyMode.ts b/src/openapi/models/CorsPolicyMode.ts index 73d3943c7f..701e15a556 100644 --- a/src/openapi/models/CorsPolicyMode.ts +++ b/src/openapi/models/CorsPolicyMode.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/DownloadableLibraryInfo.ts b/src/openapi/models/DownloadableLibraryInfo.ts index 07b88a16f3..4332804a89 100644 --- a/src/openapi/models/DownloadableLibraryInfo.ts +++ b/src/openapi/models/DownloadableLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/EngineManifest.ts b/src/openapi/models/EngineManifest.ts index 7781b7a22b..476fe45437 100644 --- a/src/openapi/models/EngineManifest.ts +++ b/src/openapi/models/EngineManifest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -111,7 +111,7 @@ export interface EngineManifest { */ supportedVvlibManifestVersion?: string; /** - * + * エンジンが持つ機能 * @type {SupportedFeatures} * @memberof EngineManifest */ diff --git a/src/openapi/models/FrameAudioQuery.ts b/src/openapi/models/FrameAudioQuery.ts index afa54d884d..eacc49b6a3 100644 --- a/src/openapi/models/FrameAudioQuery.ts +++ b/src/openapi/models/FrameAudioQuery.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/FramePhoneme.ts b/src/openapi/models/FramePhoneme.ts index 8606c28972..adbbefb736 100644 --- a/src/openapi/models/FramePhoneme.ts +++ b/src/openapi/models/FramePhoneme.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/HTTPValidationError.ts b/src/openapi/models/HTTPValidationError.ts index 29ec8b62fe..d7d9a5e0aa 100644 --- a/src/openapi/models/HTTPValidationError.ts +++ b/src/openapi/models/HTTPValidationError.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/InstalledLibraryInfo.ts b/src/openapi/models/InstalledLibraryInfo.ts index 81563a1fa5..9e25766419 100644 --- a/src/openapi/models/InstalledLibraryInfo.ts +++ b/src/openapi/models/InstalledLibraryInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/LibrarySpeaker.ts b/src/openapi/models/LibrarySpeaker.ts index a24abf6443..b99b7b3481 100644 --- a/src/openapi/models/LibrarySpeaker.ts +++ b/src/openapi/models/LibrarySpeaker.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/LicenseInfo.ts b/src/openapi/models/LicenseInfo.ts index 99827e1029..0908112211 100644 --- a/src/openapi/models/LicenseInfo.ts +++ b/src/openapi/models/LicenseInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Mora.ts b/src/openapi/models/Mora.ts index 8a2699dcee..e00dc18727 100644 --- a/src/openapi/models/Mora.ts +++ b/src/openapi/models/Mora.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/MorphableTargetInfo.ts b/src/openapi/models/MorphableTargetInfo.ts index e743f8da15..da9cc66c81 100644 --- a/src/openapi/models/MorphableTargetInfo.ts +++ b/src/openapi/models/MorphableTargetInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Note.ts b/src/openapi/models/Note.ts index 1763b2eaf8..7dd33baa78 100644 --- a/src/openapi/models/Note.ts +++ b/src/openapi/models/Note.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ParseKanaBadRequest.ts b/src/openapi/models/ParseKanaBadRequest.ts index 0d6e14557d..2373532ae4 100644 --- a/src/openapi/models/ParseKanaBadRequest.ts +++ b/src/openapi/models/ParseKanaBadRequest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Preset.ts b/src/openapi/models/Preset.ts index 78af2e078d..8a7a958c0d 100644 --- a/src/openapi/models/Preset.ts +++ b/src/openapi/models/Preset.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Score.ts b/src/openapi/models/Score.ts index 0769a8585c..e9168cf080 100644 --- a/src/openapi/models/Score.ts +++ b/src/openapi/models/Score.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/Speaker.ts b/src/openapi/models/Speaker.ts index 9a5678b11e..840289e65c 100644 --- a/src/openapi/models/Speaker.ts +++ b/src/openapi/models/Speaker.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * @@ -57,7 +57,7 @@ export interface Speaker { */ version: string; /** - * + * キャラクターの対応機能 * @type {SpeakerSupportedFeatures} * @memberof Speaker */ diff --git a/src/openapi/models/SpeakerInfo.ts b/src/openapi/models/SpeakerInfo.ts index ec0da115cd..93cbcb0536 100644 --- a/src/openapi/models/SpeakerInfo.ts +++ b/src/openapi/models/SpeakerInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SpeakerStyle.ts b/src/openapi/models/SpeakerStyle.ts index dd46eb6fa8..377b5ecc74 100644 --- a/src/openapi/models/SpeakerStyle.ts +++ b/src/openapi/models/SpeakerStyle.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SpeakerSupportedFeatures.ts b/src/openapi/models/SpeakerSupportedFeatures.ts index 63c35f021a..2f91cc8fd8 100644 --- a/src/openapi/models/SpeakerSupportedFeatures.ts +++ b/src/openapi/models/SpeakerSupportedFeatures.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/StyleInfo.ts b/src/openapi/models/StyleInfo.ts index 8333aec0f5..73cc19bd32 100644 --- a/src/openapi/models/StyleInfo.ts +++ b/src/openapi/models/StyleInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SupportedDevicesInfo.ts b/src/openapi/models/SupportedDevicesInfo.ts index db15302c39..c0facee0a2 100644 --- a/src/openapi/models/SupportedDevicesInfo.ts +++ b/src/openapi/models/SupportedDevicesInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/SupportedFeatures.ts b/src/openapi/models/SupportedFeatures.ts index d68d63d050..5718d85838 100644 --- a/src/openapi/models/SupportedFeatures.ts +++ b/src/openapi/models/SupportedFeatures.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/UpdateInfo.ts b/src/openapi/models/UpdateInfo.ts index 8949a09e3d..3e39392379 100644 --- a/src/openapi/models/UpdateInfo.ts +++ b/src/openapi/models/UpdateInfo.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/UserDictWord.ts b/src/openapi/models/UserDictWord.ts index a08d572975..dcbb4299d6 100644 --- a/src/openapi/models/UserDictWord.ts +++ b/src/openapi/models/UserDictWord.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ValidationError.ts b/src/openapi/models/ValidationError.ts index a08053b021..e893a2d602 100644 --- a/src/openapi/models/ValidationError.ts +++ b/src/openapi/models/ValidationError.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/ValidationErrorLocInner.ts b/src/openapi/models/ValidationErrorLocInner.ts index db6095c134..18a47618e5 100644 --- a/src/openapi/models/ValidationErrorLocInner.ts +++ b/src/openapi/models/ValidationErrorLocInner.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/VvlibManifest.ts b/src/openapi/models/VvlibManifest.ts index 4ed628ebc8..c65a160629 100644 --- a/src/openapi/models/VvlibManifest.ts +++ b/src/openapi/models/VvlibManifest.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/WordTypes.ts b/src/openapi/models/WordTypes.ts index 78a3d901e8..4d026652a7 100644 --- a/src/openapi/models/WordTypes.ts +++ b/src/openapi/models/WordTypes.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/openapi/models/index.ts b/src/openapi/models/index.ts index 5ed82691a4..19164338dc 100644 --- a/src/openapi/models/index.ts +++ b/src/openapi/models/index.ts @@ -3,6 +3,7 @@ export * from './AccentPhrase'; export * from './AudioQuery'; export * from './BaseLibraryInfo'; +export * from './BodySingFrameF0SingFrameF0Post'; export * from './BodySingFrameVolumeSingFrameVolumePost'; export * from './CorsPolicyMode'; export * from './DownloadableLibraryInfo'; diff --git a/src/openapi/runtime.ts b/src/openapi/runtime.ts index f982393c8d..84b3765c73 100644 --- a/src/openapi/runtime.ts +++ b/src/openapi/runtime.ts @@ -1,8 +1,8 @@ /* tslint:disable */ /* eslint-disable */ /** - * VOICEVOX Engine - * VOICEVOX の音声合成エンジンです。 + * DUMMY Engine + * DUMMY の音声合成エンジンです。 * * The version of the OpenAPI document: latest * diff --git a/src/plugins/ipcMessageReceiverPlugin.ts b/src/plugins/ipcMessageReceiverPlugin.ts index 1ae8393ca6..78313ee5f3 100644 --- a/src/plugins/ipcMessageReceiverPlugin.ts +++ b/src/plugins/ipcMessageReceiverPlugin.ts @@ -9,8 +9,11 @@ export const ipcMessageReceiver: Plugin = { options: { store: Store }, ) => { window.backend.onReceivedIPCMsg({ - LOAD_PROJECT_FILE: (_, { filePath } = {}) => - void options.store.actions.LOAD_PROJECT_FILE({ filePath }), + LOAD_PROJECT_FILE: (_, { filePath }) => + void options.store.actions.LOAD_PROJECT_FILE({ + type: "path", + filePath, + }), DETECT_MAXIMIZED: () => options.store.actions.DETECT_MAXIMIZED(), diff --git a/src/sing/utaformatixProject/utils.ts b/src/sing/utaformatixProject/utils.ts new file mode 100644 index 0000000000..dec64e1248 --- /dev/null +++ b/src/sing/utaformatixProject/utils.ts @@ -0,0 +1,56 @@ +import { Project as UfProject } from "@sevenc-nanashi/utaformatix-ts"; +import { ExhaustiveError } from "@/type/utility"; + +export const singleFileProjectFormats = ["smf", "ufdata"] as const; +export type SingleFileProjectFormat = (typeof singleFileProjectFormats)[number]; +export const multiFileProjectFormats = ["ust", "musicxml"] as const; +export type MultiFileProjectFormat = (typeof multiFileProjectFormats)[number]; +export type ProjectFormat = SingleFileProjectFormat | MultiFileProjectFormat; + +export const isSingleFileProjectFormat = ( + format: ProjectFormat, +): format is SingleFileProjectFormat => + singleFileProjectFormats.includes(format as SingleFileProjectFormat); + +export const isMultiFileProjectFormat = ( + format: ProjectFormat, +): format is MultiFileProjectFormat => + multiFileProjectFormats.includes(format as MultiFileProjectFormat); + +export const projectFileExtensions: Record< + SingleFileProjectFormat | MultiFileProjectFormat, + string +> = { + smf: "mid", + ufdata: "ufdata", + ust: "ust", + musicxml: "xml", +}; + +export const ufProjectToSingleFile = async ( + project: UfProject, + format: SingleFileProjectFormat, +): Promise => { + switch (format) { + case "smf": + return await project.toStandardMid(); + case "ufdata": + return await project.toUfData(); + default: + throw new ExhaustiveError(format); + } +}; + +export const ufProjectToMultiFile = async ( + project: UfProject, + format: MultiFileProjectFormat, +): Promise => { + switch (format) { + case "ust": + return await project.toUst(); + case "musicxml": + return await project.toMusicXml(); + default: + throw new ExhaustiveError(format); + } +}; diff --git a/src/store/audio.ts b/src/store/audio.ts index bea3d9425f..371b0616f7 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -1370,9 +1370,11 @@ export const audioStore = createPartialStore({ defaultAudioFileName, ); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を保存", defaultPath: defaultAudioFileName, + extensionName: "WAV ファイル", + extensions: ["wav"], }); } @@ -1517,9 +1519,11 @@ export const audioStore = createPartialStore({ defaultFileName, ); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を全て繋げて保存", defaultPath: defaultFileName, + extensionName: "WAV ファイル", + extensions: ["wav"], }); } @@ -1660,9 +1664,11 @@ export const audioStore = createPartialStore({ defaultFileName, ); } else { - filePath ??= await window.backend.showTextSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "文章を全て繋げてテキストファイルに保存", defaultPath: defaultFileName, + extensionName: "テキストファイル", + extensions: ["txt"], }); } @@ -2880,24 +2886,36 @@ export const audioCommandStore = transformCommandStore( prevAudioKey: undefined, }); }, + /** + * セリフテキストファイルを読み込む。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 + */ action: createUILockAction( - async ( - { state, mutations, actions, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { + async ({ state, mutations, actions, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { filePath = await window.backend.showImportFileDialog({ title: "セリフ読み込み", }); if (!filePath) return; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let body = new TextDecoder("utf-8").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } + + let body = new TextDecoder("utf-8").decode(buf); if (body.includes("\ufffd")) { - body = new TextDecoder("shift-jis").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + body = new TextDecoder("shift-jis").decode(buf); } const audioItems: AudioItem[] = []; let baseAudioItem: AudioItem | undefined = undefined; diff --git a/src/store/command.ts b/src/store/command.ts index ecb5b37656..3d5ee0cf57 100644 --- a/src/store/command.ts +++ b/src/store/command.ts @@ -11,6 +11,7 @@ import { } from "@/store/vuex"; import { CommandId, EditorType } from "@/type/preload"; import { uuid4 } from "@/helpers/random"; +import { objectEntries, objectFromEntries } from "@/helpers/typedEntries"; enablePatches(); enableMapSet(); @@ -32,10 +33,10 @@ export const createCommandMutationTree = ( payloadRecipeTree: PayloadRecipeTree, editor: EditorType, ): MutationTree => - Object.fromEntries( - Object.entries(payloadRecipeTree).map(([key, val]) => [ + objectFromEntries( + objectEntries(payloadRecipeTree).map(([key, val]) => [ key, - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + // @ts-expect-error とりあえず動くので無視 createCommandMutation(val, editor), ]), ) as MutationTree; diff --git a/src/store/project.ts b/src/store/project.ts index 48da368714..3e5a0862d4 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -21,7 +21,7 @@ import { DEFAULT_TPQN, } from "@/sing/domain"; import { EditorType } from "@/type/preload"; -import { IsEqual } from "@/type/utility"; +import { IsEqual, UnreachableError } from "@/type/utility"; import { showAlertDialog, showMessageDialog, @@ -166,15 +166,13 @@ export const projectStore = createPartialStore({ LOAD_PROJECT_FILE: { /** * プロジェクトファイルを読み込む。読み込めたかの成否が返る。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 * エラー発生時はダイアログが表示される。 */ action: createUILockAction( - async ( - { actions, mutations, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { - // Select and load a project File. + async ({ actions, mutations, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { const ret = await window.backend.showProjectLoadDialog({ title: "プロジェクトファイルの選択", }); @@ -182,17 +180,25 @@ export const projectStore = createPartialStore({ return false; } filePath = ret[0]; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let buf: ArrayBuffer; try { - buf = await window.backend - .readFile({ filePath }) - .then(getValueOrThrow); + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); - await actions.APPEND_RECENTLY_USED_PROJECT({ - filePath, - }); + await actions.APPEND_RECENTLY_USED_PROJECT({ + filePath, + }); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } const text = new TextDecoder("utf-8").decode(buf).trim(); const parsedProjectData = await actions.PARSE_PROJECT_FILE({ diff --git a/src/store/proxy.ts b/src/store/proxy.ts index 283e2e9fd2..0873c974a1 100644 --- a/src/store/proxy.ts +++ b/src/store/proxy.ts @@ -1,8 +1,10 @@ import { ProxyStoreState, ProxyStoreTypes, EditorAudioQuery } from "./type"; import { createPartialStore } from "./vuex"; import { createEngineUrl } from "@/domain/url"; +import { isElectron, isProduction } from "@/helpers/platform"; import { IEngineConnectorFactory, + OpenAPIEngineAndMockConnectorFactory, OpenAPIEngineConnectorFactory, } from "@/infrastructures/EngineConnector"; import { AudioQuery } from "@/openapi"; @@ -69,4 +71,11 @@ export const convertAudioQueryFromEngineToEditor = ( }; }; -export const proxyStore = proxyStoreCreator(OpenAPIEngineConnectorFactory); +// 製品PC版は通常エンジンのみを、それ以外はモックエンジンも使えるようする +const getConnectorFactory = () => { + if (isElectron && isProduction) { + return OpenAPIEngineConnectorFactory; + } + return OpenAPIEngineAndMockConnectorFactory; +}; +export const proxyStore = proxyStoreCreator(getConnectorFactory()); diff --git a/src/store/setting.ts b/src/store/setting.ts index 6f14d6c9af..e000302461 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -218,15 +218,15 @@ export const settingStore = createPartialStore({ SET_ROOT_MISC_SETTING: { mutation(state, { key, value }) { - // Vuexの型処理でUnionが解かれてしまうのを迂回している + // @ts-expect-error Vuexの型処理でUnionが解かれてしまうのを迂回している // FIXME: このワークアラウンドをなくす - state[key as never] = value; + state[key] = value; }, action({ mutations }, { key, value }) { void window.backend.setSetting(key, value); - // Vuexの型処理でUnionが解かれてしまうのを迂回している + // @ts-expect-error Vuexの型処理でUnionが解かれてしまうのを迂回している // FIXME: このワークアラウンドをなくす - mutations.SET_ROOT_MISC_SETTING({ key: key as never, value }); + mutations.SET_ROOT_MISC_SETTING({ key, value }); }, }, diff --git a/src/store/singing.ts b/src/store/singing.ts index 65da791f3e..ed15a1759c 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -114,7 +114,15 @@ import { generateWriteErrorMessage } from "@/helpers/fileHelper"; import { generateWavFileData } from "@/helpers/fileDataGenerator"; import path from "@/helpers/path"; import { showAlertDialog } from "@/components/Dialog/Dialog"; +import { ufProjectFromVoicevox } from "@/sing/utaformatixProject/fromVoicevox"; import { generateUniqueFilePath } from "@/sing/fileUtils"; +import { + isMultiFileProjectFormat, + isSingleFileProjectFormat, + projectFileExtensions, + ufProjectToMultiFile, + ufProjectToSingleFile, +} from "@/sing/utaformatixProject/utils"; const logger = createLogger("store/singing"); @@ -1544,18 +1552,6 @@ export const singingStore = createPartialStore({ }, }, - SET_IS_DRAG: { - mutation(state, { isDrag }: { isDrag: boolean }) { - // FIXME: state.isDragが無くなっているので修正する - state.isDrag = isDrag; - }, - async action({ mutations }, { isDrag }) { - mutations.SET_IS_DRAG({ - isDrag, - }); - }, - }, - SET_START_RENDERING_REQUESTED: { mutation(state, { startRenderingRequested }) { state.startRenderingRequested = startRenderingRequested; @@ -2760,9 +2756,11 @@ export const singingStore = createPartialStore({ if (state.savingSetting.fixedExportEnabled) { filePath = path.join(state.savingSetting.fixedExportDir, fileName); } else { - filePath ??= await window.backend.showAudioSaveDialog({ + filePath ??= await window.backend.showExportFileDialog({ title: "音声を保存", defaultPath: fileName, + extensions: ["wav"], + extensionName: "WAV ファイル", }); } if (!filePath) { @@ -3449,6 +3447,101 @@ export const singingStore = createPartialStore({ return Math.max(1, lastNoteEndTime + 1); }, }, + + EXPORT_SONG_PROJECT: { + action: createUILockAction( + async ( + { state, getters, actions }, + { fileType, fileTypeLabel }, + ): Promise => { + const fileBaseName = generateDefaultSongFileBaseName( + getters.PROJECT_NAME, + getters.SELECTED_TRACK, + getters.CHARACTER_INFO, + ); + const project = ufProjectFromVoicevox( + { + tempos: state.tempos, + timeSignatures: state.timeSignatures, + tpqn: state.tpqn, + tracks: state.trackOrder.map((trackId) => + getOrThrow(state.tracks, trackId), + ), + }, + fileBaseName, + ); + + // 複数トラックかつ複数ファイルの形式はディレクトリに書き出す + if (state.trackOrder.length > 1 && isMultiFileProjectFormat(fileType)) { + const dirPath = await window.backend.showSaveDirectoryDialog({ + title: "プロジェクトを書き出し", + }); + if (!dirPath) { + return { result: "CANCELED", path: "" }; + } + + const extension = projectFileExtensions[fileType]; + const tracksBytes = await ufProjectToMultiFile(project, fileType); + + let firstFilePath; + for (const [i, trackBytes] of tracksBytes.entries()) { + const filePath = await actions.GENERATE_FILE_PATH_FOR_TRACK_EXPORT({ + trackId: state.trackOrder[i], + directoryPath: dirPath, + extension, + }); + if (i === 0) { + firstFilePath = filePath; + } + + const result = await actions.EXPORT_FILE({ + filePath, + content: trackBytes, + }); + if (result.result !== "SUCCESS") { + return result; + } + } + if (firstFilePath == undefined) { + throw new Error("firstFilePath is undefined."); + } + + return { result: "SUCCESS", path: firstFilePath }; + } + + // それ以外の場合は単一ファイルの形式を選択する + else { + let buffer: Uint8Array; + const extension = projectFileExtensions[fileType]; + if (isSingleFileProjectFormat(fileType)) { + buffer = await ufProjectToSingleFile(project, fileType); + } else { + buffer = (await ufProjectToMultiFile(project, fileType))[0]; + } + + let filePath = await window.backend.showExportFileDialog({ + title: "プロジェクトを書き出し", + defaultPath: fileBaseName, + extensionName: fileTypeLabel, + extensions: [extension], + }); + if (!filePath) { + return { result: "CANCELED", path: "" }; + } + filePath = await generateUniqueFilePath( + // 拡張子を除いたファイル名を取得 + filePath.slice(0, -(extension.length + 1)), + extension, + ); + + return await actions.EXPORT_FILE({ + filePath, + content: buffer, + }); + } + }, + ), + }, }); export const singingCommandStoreState: SingingCommandStoreState = {}; diff --git a/src/store/type.ts b/src/store/type.ts index 0a956c255c..50fa7d307a 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -72,6 +72,10 @@ import { trackSchema, } from "@/domain/project/schema"; import { HotkeySettingType } from "@/domain/hotkeyAction"; +import { + MultiFileProjectFormat, + SingleFileProjectFormat, +} from "@/sing/utaformatixProject/utils"; /** * エディタ用のAudioQuery @@ -686,7 +690,12 @@ export type AudioCommandStoreTypes = { mutation: { audioKeyItemPairs: { audioItem: AudioItem; audioKey: AudioKey }[]; }; - action(payload: { filePath?: string }): void; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): void; }; COMMAND_PUT_TEXTS: { @@ -847,6 +856,11 @@ export type NoteEditTool = "SELECT_FIRST" | "EDIT_FIRST"; // ピッチ編集ツール export type PitchEditTool = "DRAW" | "ERASE"; +// プロジェクトの書き出しに使えるファイル形式 +export type ExportSongProjectFileType = + | SingleFileProjectFormat + | MultiFileProjectFormat; + export type TrackParameters = { gain: boolean; pan: boolean; @@ -1142,11 +1156,6 @@ export type SingingStoreTypes = { action(payload: { sequencerPitchTool: PitchEditTool }): void; }; - SET_IS_DRAG: { - mutation: { isDrag: boolean }; - action(payload: { isDrag: boolean }): void; - }; - EXPORT_LABEL_FILES: { action(payload: { dirPath?: string }): SaveResultObject[]; }; @@ -1371,6 +1380,13 @@ export type SingingStoreTypes = { APPLY_DEVICE_ID_TO_AUDIO_CONTEXT: { action(payload: { device: string }): void; }; + + EXPORT_SONG_PROJECT: { + action(payload: { + fileType: ExportSongProjectFileType; + fileTypeLabel: string; + }): Promise; + }; }; export type SingingCommandStoreState = { @@ -1826,7 +1842,12 @@ export type ProjectStoreTypes = { }; LOAD_PROJECT_FILE: { - action(payload: { filePath?: string }): boolean; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): boolean; }; SAVE_PROJECT_FILE: { @@ -2257,7 +2278,7 @@ export type PresetStoreTypes = { * Dictionary Store Types */ -export type DictionaryStoreState = Record; +export type DictionaryStoreState = Record; export type DictionaryStoreTypes = { LOAD_USER_DICT: { @@ -2297,7 +2318,7 @@ export type DictionaryStoreTypes = { * Setting Store Types */ -export type ProxyStoreState = Record; +export type ProxyStoreState = Record; export type IEngineConnectorFactoryActions = ReturnType< IEngineConnectorFactory["instance"] diff --git a/src/store/ui.ts b/src/store/ui.ts index 26e6931d3b..e5ebf9d6e5 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -30,6 +30,7 @@ import { showNotifyAndNotShowAgainButton, showWarningDialog, } from "@/components/Dialog/Dialog"; +import { objectEntries } from "@/helpers/typedEntries"; export function createUILockAction( action: ( @@ -178,15 +179,18 @@ export const uiStore = createPartialStore({ SET_DIALOG_OPEN: { mutation(state, dialogState) { - for (const [key, value] of Object.entries(dialogState)) { + for (const [key, value] of objectEntries(dialogState)) { if (!(key in state)) { throw new Error(`Unknown dialog state: ${key}`); } + if (value == undefined) { + throw new Error(`Invalid dialog state: ${key}`); + } state[key] = value; } }, async action({ state, mutations }, dialogState) { - for (const [key, value] of Object.entries(dialogState)) { + for (const [key, value] of objectEntries(dialogState)) { if (state[key] === value) continue; if (value) { diff --git a/src/type/ipc.ts b/src/type/ipc.ts index d71685f5ac..8c35a089f5 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -33,21 +33,6 @@ export type IpcIHData = { return: AltPortInfos; }; - SHOW_AUDIO_SAVE_DIALOG: { - args: [ - obj: { - title: string; - defaultPath?: string; - }, - ]; - return?: string; - }; - - SHOW_TEXT_SAVE_DIALOG: { - args: [obj: { title: string; defaultPath?: string }]; - return?: string; - }; - SHOW_SAVE_DIRECTORY_DIALOG: { args: [obj: { title: string }]; return?: string; @@ -98,6 +83,18 @@ export type IpcIHData = { return: MessageBoxReturnValue; }; + SHOW_EXPORT_FILE_DIALOG: { + args: [ + obj: { + title: string; + defaultPath?: string; + extensionName: string; + extensions: string[]; + }, + ]; + return?: string; + }; + IS_AVAILABLE_GPU_MODE: { args: []; return: boolean; @@ -244,7 +241,7 @@ export type IpcIHData = { */ export type IpcSOData = { LOAD_PROJECT_FILE: { - args: [obj: { filePath?: string }]; + args: [obj: { filePath: string }]; return: void; }; diff --git a/src/type/preload.ts b/src/type/preload.ts index cd1e6cf9e4..a144d2c64d 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -73,14 +73,6 @@ export interface Sandbox { getAppInfos(): Promise; getTextAsset(textType: K): Promise; getAltPortInfos(): Promise; - showAudioSaveDialog(obj: { - title: string; - defaultPath?: string; - }): Promise; - showTextSaveDialog(obj: { - title: string; - defaultPath?: string; - }): Promise; showSaveDirectoryDialog(obj: { title: string }): Promise; showVvppOpenDialog(obj: { title: string; @@ -97,6 +89,12 @@ export interface Sandbox { name?: string; extensions?: string[]; }): Promise; + showExportFileDialog(obj: { + title: string; + defaultPath?: string; + extensionName: string; + extensions: string[]; + }): Promise; writeFile(obj: { filePath: string; buffer: ArrayBuffer | Uint8Array; @@ -143,6 +141,7 @@ export interface Sandbox { uninstallVvppEngine(engineId: EngineId): Promise; validateEngineDir(engineDir: string): Promise; reloadApp(obj: { isMultiEngineOffMode?: boolean }): Promise; + getPathForFile(file: File): string; } export type AppInfos = { @@ -404,7 +403,7 @@ export const rootMiscSettingSchema = z.object({ }); export type RootMiscSettingType = z.infer; -export function getConfigSchema(isMac: boolean) { +export function getConfigSchema({ isMac }: { isMac: boolean }) { return z .object({ inheritAudioInfo: z.boolean().default(true), @@ -427,7 +426,7 @@ export function getConfigSchema(isMac: boolean) { .default({}), hotkeySettings: hotkeySettingSchema .array() - .default(getDefaultHotkeySettings(isMac)), + .default(getDefaultHotkeySettings({ isMac })), toolbarSetting: toolbarSettingSchema .array() .default(defaultToolbarButtonSetting), diff --git "a/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" "b/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" index b18d149670..02a611fee7 100644 --- "a/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" +++ "b/tests/e2e/browser/\343\202\242\343\202\257\343\202\273\343\203\263\343\203\210.spec.ts" @@ -30,7 +30,6 @@ test("アクセントの読み部分をクリックすると読みを変更で await expect(page.locator(".text-cell").first()).toBeVisible(); await page.locator(".text-cell").first().click(); const input = page.getByLabel("1番目のアクセント区間の読み"); - await input.evaluate((node) => console.log(node.outerHTML)); expect(await input.inputValue()).toBe("テストデス"); await input.fill("テストテスト"); await input.press("Enter"); diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" index 485eb7fb66..37196f334a 100644 --- "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" +++ "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts" @@ -1,130 +1,6 @@ -import path from "path"; -import fs from "fs/promises"; import { test, expect } from "@playwright/test"; import { gotoHome, navigateToMain } from "../navigators"; -import { - Speaker, - SpeakerFromJSON, - SpeakerInfo, - SpeakerInfoFromJSON, - SpeakerInfoToJSON, - SpeakerToJSON, -} from "@/openapi"; -let speakerImages: - | { - portrait: string; - icon: string; - }[] - | undefined = undefined; - -/** - * 差し替え用の立ち絵・アイコンを取得する。 - * TODO: エンジンモックを使ってこのコードを削除する。 - */ -async function getSpeakerImages(): Promise< - { - portrait: string; - icon: string; - }[] -> { - if (!speakerImages) { - const assetsPath = path.resolve( - __dirname, - "../../../src/mock/engineMock/assets", - ); - const images = await fs.readdir(assetsPath); - const icons = images.filter((image) => image.startsWith("icon")); - icons.sort( - (a, b) => - parseInt(a.split(".")[0].split("_")[1]) - - parseInt(b.split(".")[0].split("_")[1]), - ); - speakerImages = await Promise.all( - icons.map(async (iconPath) => { - const portraitPath = iconPath.replace("icon_", "portrait_"); - const portrait = await fs.readFile( - path.join(assetsPath, portraitPath), - "base64", - ); - const icon = await fs.readFile( - path.join(assetsPath, iconPath), - "base64", - ); - - return { portrait, icon }; - }), - ); - } - return speakerImages; -} - -test.beforeEach(async ({ page }) => { - let speakers: Speaker[]; - const speakerImages = await getSpeakerImages(); - // Voicevox Nemo EngineでもVoicevox Engineでも同じ結果が選られるように、 - // GET /speakers、GET /speaker_infoの話者名、スタイル名、画像を差し替える。 - await page.route(/\/speakers$/, async (route) => { - const response = await route.fetch(); - const json: Speaker[] = await response - .json() - .then((json: unknown[]) => json.map(SpeakerFromJSON)); - let i = 0; - for (const speaker of json) { - i++; - speaker.name = `Speaker ${i}`; - let j = 0; - for (const style of speaker.styles) { - j++; - style.name = `Style ${i}-${j}`; - } - } - speakers = json; - await route.fulfill({ - status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - body: JSON.stringify(json.map(SpeakerToJSON)), - }); - }); - await page.route(/\/speaker_info\?/, async (route) => { - if (!speakers) { - // Unreachableのはず - throw new Error("speakers is not initialized"); - } - const url = new URL(route.request().url()); - const speakerUuid = url.searchParams.get("speaker_uuid"); - if (!speakerUuid) { - throw new Error("speaker_uuid is not set"); - } - const response = await route.fetch(); - const json: SpeakerInfo = await response.json().then(SpeakerInfoFromJSON); - const speakerIndex = speakers.findIndex( - (speaker) => speaker.speakerUuid === speakerUuid, - ); - if (speakerIndex === -1) { - throw new Error(`speaker_uuid=${speakerUuid} is not found`); - } - const image = speakerImages[speakerIndex % speakerImages.length]; - json.portrait = image.portrait; - for (const style of json.styleInfos) { - style.icon = image.icon; - if ("portrait" in style) { - delete style.portrait; - } - } - await route.fulfill({ - status: 200, - headers: { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - body: JSON.stringify(SpeakerInfoToJSON(json)), - }); - }); -}); test.beforeEach(gotoHome); test("メイン画面の表示", async ({ page }) => { diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" index 32da40c0dd..c5e5a5971d 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\202\275\343\203\263\343\202\260\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" index 9ba500101c..54332e013d 100644 Binary files "a/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" and "b/tests/e2e/browser/\343\202\271\343\202\257\343\203\252\343\203\274\343\203\263\343\202\267\343\203\247\343\203\203\343\203\210.spec.ts-snapshots/\343\203\210\343\203\274\343\202\257\347\224\273\351\235\242-browser-win32.png" differ diff --git "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" index 8ab1694115..bfd2e9b0db 100644 --- "a/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" +++ "b/tests/e2e/browser/\350\276\236\346\233\270\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" @@ -4,29 +4,27 @@ import { getNewestQuasarDialog } from "../locators"; test.beforeEach(gotoHome); -// 読み方を確認する。 -// エンジン起動直後など、たまに読みが反映されないことがあるので、 -// 一度空にする -> テキストが消えたことを確認(消えてなかったらもう一度Enter)-> -// 再度入力する -> 読み方が表示されたことを確認(表示されてなかったらもう一度Enter) -// という流れで読み方を確認する。 +/** + * 最後のテキスト欄にテキストを入力し、その読みを取得する。 + * 確実に読みを反映させるために、一度空にしてから入力する。 + */ async function getYomi(page: Page, inputText: string): Promise { - const audioCellInput = page.locator(".audio-cell input").last(); + const audioCellInput = page.getByRole("textbox", { name: "行目" }).last(); + const accentPhrase = page.locator(".accent-phrase"); + + // 空にする + await audioCellInput.click(); await audioCellInput.fill(""); - let text = ""; - do { - await page.waitForTimeout(100); - await audioCellInput.press("Enter"); - text = (await page.locator(".text-cell").allInnerTexts()).join(""); - } while (text.length > 0); + await audioCellInput.press("Enter"); + await expect(accentPhrase).not.toBeVisible(); + // 入力する + await audioCellInput.click(); await audioCellInput.fill(inputText); - do { - await page.waitForTimeout(100); - await audioCellInput.press("Enter"); - text = (await page.locator(".text-cell").allInnerTexts()).join(""); - } while (text.length === 0); + await audioCellInput.press("Enter"); + await expect(accentPhrase).not.toHaveCount(0); - return text; + return (await accentPhrase.allTextContents()).join(""); } async function openDictDialog(page: Page): Promise { @@ -52,17 +50,12 @@ async function validateInputTag( test("「設定」→「読み方&アクセント辞書」で「読み方&アクセント辞書」ページが表示される", async ({ page, }) => { - test.skip(!process.env.CI, "環境変数CIが未設定のためスキップします"); await navigateToMain(page); - // テスト用にランダムな文字列を生成 - const randomString = Math.random().toString(36).slice(-8); - const zenkakuRandomString = randomString.replace(/[\u0021-\u007e]/g, (s) => { - return String.fromCharCode(s.charCodeAt(0) + 0xfee0); - }); + const targetString = "あいうえお"; // 文字列を入力して読み方を記憶する - const yomi = await getYomi(page, randomString); + const yomi = await getYomi(page, targetString); // 読み方の設定画面を開く await openDictDialog(page); @@ -76,9 +69,9 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& await wordInputTag.evaluate((e: HTMLInputElement, rs: string) => { e.value = rs; e.dispatchEvent(new Event("input")); - }, randomString); + }, targetString); await page.waitForTimeout(100); - await validateInputTag(page, wordInputTag, zenkakuRandomString); + await validateInputTag(page, wordInputTag, targetString); const yomiInputTag = page .locator(".word-editor .row") @@ -103,21 +96,14 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& // 辞書が登録されているかどうかを確認 await page.getByRole("button").filter({ hasText: "add" }).click(); await page.waitForTimeout(100); - const yomi2 = await getYomi(page, randomString); + const yomi2 = await getYomi(page, targetString); expect(yomi2).toBe("テスト"); - // もう一度設定を開き辞書からabsを削除 + // もう一度設定を開き辞書から削除 await openDictDialog(page); - await page - .getByRole("listitem") - .filter({ hasText: zenkakuRandomString }) - .click(); - await page.waitForTimeout(100); - await page - .getByRole("listitem") - .filter({ hasText: zenkakuRandomString }) - .getByText("delete") - .click(); + const wordItem = page.getByRole("listitem").filter({ hasText: targetString }); + await wordItem.hover(); + await wordItem.getByText("delete").click(); await page.waitForTimeout(100); await getNewestQuasarDialog(page) .getByRole("button") @@ -136,6 +122,6 @@ test("「設定」→「読み方&アクセント辞書」で「読み方& // (=最初の読み方と同じになっていることを確認) await page.getByRole("button").filter({ hasText: "add" }).click(); await page.waitForTimeout(100); - const yomi3 = await getYomi(page, randomString); + const yomi3 = await getYomi(page, targetString); expect(yomi3).toBe(yomi); }); diff --git "a/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" "b/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" index dc8533309e..57b873104b 100644 --- "a/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" +++ "b/tests/e2e/browser/\351\237\263\345\243\260\350\251\263\347\264\260.spec.ts" @@ -14,7 +14,7 @@ test("単体アクセント句の読み変更", async ({ page }) => { const textField = page.getByRole("textbox", { name: "1行目" }); await textField.click(); - await textField.fill("1234"); + await textField.fill("あれもこれもそれもどれも"); await textField.press("Enter"); const inputs = Array.from({ length: 4 }, (_, i) => @@ -22,32 +22,32 @@ test("単体アクセント句の読み変更", async ({ page }) => { ); // 読点を追加 - await page.getByText("セ", { exact: true }).click(); - await inputs[0].fill("セン、"); + await page.getByText("ア", { exact: true }).click(); + await inputs[0].fill("アレモ、"); await inputs[0].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("セン、")).toBeVisible(); + await expect(page.getByText("アレモ、")).toBeVisible(); // 「,」が読点に変換される - await page.getByText("ヒャ", { exact: true }).click(); - await inputs[1].fill("ニヒャク,"); + await page.getByText("コ", { exact: true }).click(); + await inputs[1].fill("コレモ,"); await inputs[1].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("ニヒャク、")).toBeVisible(); + await expect(page.getByText("コレモ、")).toBeVisible(); // 連続する読点を追加すると1つに集約される - await page.getByText("ジュ", { exact: true }).click(); - await inputs[2].fill("サンジュウ,、,、"); + await page.getByText("ソ", { exact: true }).click(); + await inputs[2].fill("ソレモ,、,、"); await inputs[2].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("サンジュウ、")).toBeVisible(); + await expect(page.getByText("ソレモ、")).toBeVisible(); // 最後のアクセント区間に読点をつけても無視される - await page.getByText("ヨ", { exact: true }).click(); - await inputs[3].fill("ヨン,、,、"); + await page.getByText("ド", { exact: true }).click(); + await inputs[3].fill("ドレモ,、,、"); await inputs[3].press("Enter"); await page.waitForTimeout(100); - await expect(page.getByText("ヨン、")).not.toBeVisible(); + await expect(page.getByText("ドレモ、")).not.toBeVisible(); }); test("詳細調整欄のコンテキストメニュー", async ({ page }) => { @@ -56,9 +56,11 @@ test("詳細調整欄のコンテキストメニュー", async ({ page }) => { // 削除 await page.getByRole("textbox", { name: "1行目" }).click(); - await page.getByRole("textbox", { name: "1行目" }).fill("1234"); + await page + .getByRole("textbox", { name: "1行目" }) + .fill("あれもこれもそれもどれも"); await page.getByRole("textbox", { name: "1行目" }).press("Enter"); - await page.getByText("サンジュウ").click({ + await page.getByText("ソレモ").click({ button: "right", }); await page @@ -66,7 +68,7 @@ test("詳細調整欄のコンテキストメニュー", async ({ page }) => { .filter({ has: page.getByText("削除") }) .click(); await page.waitForTimeout(100); - await expect(page.getByText("サンジュウ")).not.toBeVisible(); - await expect(page.getByText("ニヒャク")).toBeVisible(); - await expect(page.getByText("ヨン")).toBeVisible(); + await expect(page.getByText("ソレモ")).not.toBeVisible(); + await expect(page.getByText("コレモ")).toBeVisible(); + await expect(page.getByText("ドレモ")).toBeVisible(); }); diff --git a/tests/e2e/electron/example.spec.ts b/tests/e2e/electron/example.spec.ts index c2616c1f2c..0795b258ef 100644 --- a/tests/e2e/electron/example.spec.ts +++ b/tests/e2e/electron/example.spec.ts @@ -3,10 +3,9 @@ import os from "os"; import path from "path"; import { _electron as electron, test } from "@playwright/test"; import dotenv from "dotenv"; +import { MessageBoxSyncOptions } from "electron"; test.beforeAll(async () => { - dotenv.config(); // FIXME: エンジンの設定直読み - console.log("Waiting for main.js to be built..."); while (true) { try { @@ -19,40 +18,79 @@ test.beforeAll(async () => { console.log("main.js is built."); }); -// キャッシュなどでテスト結果が変化しないように、appDataをテスト起動時に毎回消去する。 -// cf: https://www.electronjs.org/ja/docs/latest/api/app#appgetpathname -const appDataMap: Partial> = { - win32: process.env.APPDATA, - darwin: os.homedir() + "/Library/Application Support", - linux: process.env.XDG_CONFIG_HOME || os.homedir() + "/.config", -} as const; +test.beforeEach(async () => { + // キャッシュなどでテスト結果が変化しないように、appDataをテスト起動時に毎回消去する。 + // cf: https://www.electronjs.org/ja/docs/latest/api/app#appgetpathname + const appDataMap: Partial> = { + win32: process.env.APPDATA, + darwin: os.homedir() + "/Library/Application Support", + linux: process.env.XDG_CONFIG_HOME || os.homedir() + "/.config", + } as const; -const appData = appDataMap[process.platform]; -if (!appData) { - throw new Error("Unsupported platform"); -} -const userDir = path.resolve(appData, `${process.env.VITE_APP_NAME}-test`); + const appData = appDataMap[process.platform]; + if (!appData) { + throw new Error("Unsupported platform"); + } + const userDir = path.resolve(appData, `${process.env.VITE_APP_NAME}-test`); -test.beforeEach(async () => { await fs.rm(userDir, { recursive: true, force: true, }); }); -test("起動したら「利用規約に関するお知らせ」が表示される", async () => { - const app = await electron.launch({ - args: ["--no-sandbox", "."], // NOTE: --no-sandbox はUbuntu 24.04で動かすのに必要 - timeout: process.env.CI ? 0 : 60000, - }); +[ + { + envName: ".env環境", + envPath: ".env", + }, + { + envName: "VVPPデフォルトエンジン", + envPath: "tests/env/.env.test-electron-default-vvpp", + }, +].forEach(({ envName, envPath }) => { + test.describe(`${envName}`, () => { + test.beforeEach(() => { + dotenv.config({ path: envPath, override: true }); + }); - const sut = await app.firstWindow({ - timeout: process.env.CI ? 60000 : 30000, - }); + test("起動したら「利用規約に関するお知らせ」が表示される", async () => { + const app = await electron.launch({ + args: ["--no-sandbox", "."], // NOTE: --no-sandbox はUbuntu 24.04で動かすのに必要 + timeout: process.env.CI ? 0 : 60000, + }); + + // ダイアログのモック + await app.evaluate((electron) => { + // @ts-expect-error 2種のオーバーロードを無視する + electron.dialog.showMessageBoxSync = ( + options: MessageBoxSyncOptions, + ) => { + // デフォルトエンジンのインストールの確認ダイアログ + if ( + options.title == "デフォルトエンジンのインストール" && + options.buttons?.[0] == "インストール" + ) { + return 0; + } + + throw new Error(`Unexpected dialog: ${JSON.stringify(options)}`); + }; + }); + + // ログを表示 + app.on("console", (msg) => { + console.log(msg.text()); + }); - // エンジンが起動し「利用規約に関するお知らせ」が表示されるのを待つ - await sut.waitForSelector("text=利用規約に関するお知らせ", { - timeout: 60000, + const sut = await app.firstWindow({ + timeout: process.env.CI ? 60000 : 30000, + }); + // エンジンが起動し「利用規約に関するお知らせ」が表示されるのを待つ + await sut.waitForSelector("text=利用規約に関するお知らせ", { + timeout: 60000, + }); + await app.close(); + }); }); - await app.close(); }); diff --git a/tests/env/.env.test-electron b/tests/env/.env.test-electron new file mode 100644 index 0000000000..bb5b8b782e --- /dev/null +++ b/tests/env/.env.test-electron @@ -0,0 +1,16 @@ +# CI環境でのelectronテスト用の.envファイル。CI時に値が上書きされる。 + +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ + { + "name": "VOICEVOX Nemo Engine", + "uuid": "208cf94d-43d2-4cf5-abc0-9783cac36d29", + "executionEnabled": true, + "executionFilePath": "path/to/engine", + "executionArgs": ["--port=random_port"], + "host": "http://127.0.0.1:random_port" + } +]` +VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ +VITE_LATEST_UPDATE_INFOS_URL=https://voicevox.hiroshiba.jp/updateInfos.json +VITE_GTM_CONTAINER_ID=GTM-DUMMY diff --git a/tests/env/.env.test-electron-default-vvpp b/tests/env/.env.test-electron-default-vvpp new file mode 100644 index 0000000000..94255249d3 --- /dev/null +++ b/tests/env/.env.test-electron-default-vvpp @@ -0,0 +1,17 @@ +# VVPPデフォルトエンジンでのテスト用の.envファイル。 + +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ + { + "type": "downloadVvpp", + "name": "VOICEVOX Nemo Engine", + "uuid": "208cf94d-43d2-4cf5-abc0-9783cac36d29", + "executionEnabled": true, + "executionArgs": [], + "host": "http://127.0.0.1:50121", + "latestUrl": "https://voicevox.hiroshiba.jp/nemoLatestDefaultEngineInfos.json" + } +]` +VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/ +VITE_LATEST_UPDATE_INFOS_URL=https://voicevox.hiroshiba.jp/updateInfos.json +VITE_GTM_CONTAINER_ID=GTM-DUMMY diff --git a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap index 27c1b4d8f6..4da839ee6a 100644 --- a/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap +++ b/tests/unit/backend/common/__snapshots__/configManager.spec.ts.snap @@ -19,7 +19,7 @@ exports[`0.13.0からマイグレーションできる 1`] = ` "enablePreset": false, "enableRubyNotation": false, "engineSettings": { - "074fc39e-678b-4c13-8916-ffca8d505d1d": { + "00000000-0000-0000-0000-000000000000": { "outputSamplingRate": "engineDefault", "useGpu": false, }, diff --git a/tests/unit/backend/common/configManager.spec.ts b/tests/unit/backend/common/configManager.spec.ts index eed9aa0aaa..09945dd08a 100644 --- a/tests/unit/backend/common/configManager.spec.ts +++ b/tests/unit/backend/common/configManager.spec.ts @@ -4,7 +4,7 @@ import { BaseConfigManager } from "@/backend/common/ConfigManager"; import { getConfigSchema } from "@/type/preload"; const configBase = { - ...getConfigSchema(false).parse({}), + ...getConfigSchema({ isMac: false }).parse({}), __internal__: { migrations: { version: "999.999.999", @@ -13,6 +13,10 @@ const configBase = { }; class TestConfigManager extends BaseConfigManager { + constructor() { + super({ isMac: false }); + } + getAppVersion() { return "999.999.999"; } @@ -49,7 +53,7 @@ it("新規作成できる", async () => { async () => undefined, ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager).toBeTruthy(); }); @@ -62,7 +66,7 @@ it("バージョンが保存される", async () => { .spyOn(TestConfigManager.prototype, "save") .mockImplementation(async () => undefined); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); await configManager.ensureSaved(); expect(saveSpy).toHaveBeenCalled(); @@ -82,7 +86,7 @@ for (const [version, data] of pastConfigs) { async () => data, ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager).toBeTruthy(); @@ -122,7 +126,7 @@ it("0.19.1からのマイグレーション時にハミング・ソングスタ ).map((key) => getStyleIdFromVoiceId(key)); // マイグレーション - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); const presets = configManager.get("presets"); const defaultPresetKeys = configManager.get("defaultPresetKeys"); @@ -164,7 +168,7 @@ it("getできる", async () => { }), ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); expect(configManager.get("inheritAudioInfo")).toBe(false); }); @@ -183,7 +187,7 @@ it("setできる", async () => { }), ); - const configManager = new TestConfigManager(false); + const configManager = new TestConfigManager(); await configManager.initialize(); configManager.set("inheritAudioInfo", true); expect(configManager.get("inheritAudioInfo")).toBe(true); diff --git a/tests/unit/domain/hotkeyAction.spec.ts b/tests/unit/domain/hotkeyAction.spec.ts index 9d1f90e397..5124fa140e 100644 --- a/tests/unit/domain/hotkeyAction.spec.ts +++ b/tests/unit/domain/hotkeyAction.spec.ts @@ -4,7 +4,7 @@ import { } from "@/domain/hotkeyAction"; test("すべてのホットキーに初期値が設定されている", async () => { - const defaultHotkeySettings = getDefaultHotkeySettings(false); + const defaultHotkeySettings = getDefaultHotkeySettings({ isMac: false }); const allActionNames = new Set(hotkeyActionNameSchema.options); const defaultHotkeyActionsNames = new Set( defaultHotkeySettings.map((setting) => setting.action), diff --git a/vite.config.mts b/vite.config.mts index c3834795a7..e4da29c038 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -8,13 +8,18 @@ import vue from "@vitejs/plugin-vue"; import checker from "vite-plugin-checker"; import { BuildOptions, defineConfig, loadEnv, Plugin } from "vite"; import { quasar } from "@quasar/vite-plugin"; +import { z } from "zod"; const isElectron = process.env.VITE_TARGET === "electron"; const isBrowser = process.env.VITE_TARGET === "browser"; export default defineConfig((options) => { + const mode = z + .enum(["development", "test", "production"]) + .parse(options.mode); + const packageName = process.env.npm_package_name; - const env = loadEnv(options.mode, import.meta.dirname); + const env = loadEnv(mode, import.meta.dirname); if (!packageName?.startsWith(env.VITE_APP_NAME)) { throw new Error( `"package.json"の"name":"${packageName}"は"VITE_APP_NAME":"${env.VITE_APP_NAME}"から始まっている必要があります`, @@ -32,19 +37,19 @@ export default defineConfig((options) => { throw new Error(`Unsupported platform: ${process.platform}`); } process.env.VITE_7Z_BIN_NAME = - (options.mode === "development" + (mode !== "production" ? path.join(import.meta.dirname, "vendored", "7z") + path.sep : "") + sevenZipBinName; process.env.VITE_APP_VERSION = process.env.npm_package_version; - const shouldEmitSourcemap = ["development", "test"].includes(options.mode); + const shouldEmitSourcemap = ["development", "test"].includes(mode); const sourcemap: BuildOptions["sourcemap"] = shouldEmitSourcemap ? "inline" : false; // ref: electronの起動をスキップしてデバッグ起動を軽くする const skipLahnchElectron = - options.mode === "test" || process.env.SKIP_LAUNCH_ELECTRON === "1"; + mode === "test" || process.env.SKIP_LAUNCH_ELECTRON === "1"; return { root: path.resolve(import.meta.dirname, "src"), @@ -71,7 +76,7 @@ export default defineConfig((options) => { plugins: [ vue(), quasar({ autoImportComponentCase: "pascal" }), - options.mode !== "test" && + mode !== "test" && checker({ overlay: false, eslint: {