diff --git a/.env.production b/.env.production index 56146c0a3d..1fe00d6ff2 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,5 @@ -DEFAULT_ENGINE_INFOS=`[ +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ { "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", "name": "VOICEVOX Engine", @@ -9,4 +10,3 @@ DEFAULT_ENGINE_INFOS=`[ } ]` VITE_GTM_CONTAINER_ID=GTM-DUMMY -VV_OUTPUT_LOG_UTF8=1 diff --git a/.env.test b/.env.test index f02c0b0239..a2c27972a3 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,5 @@ -DEFAULT_ENGINE_INFOS=`[ +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ { "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", "name": "VOICEVOX Engine", @@ -9,4 +10,3 @@ DEFAULT_ENGINE_INFOS=`[ } ]` VITE_GTM_CONTAINER_ID=GTM-DUMMY -VV_OUTPUT_LOG_UTF8=1 diff --git a/.eslintrc.js b/.eslintrc.js index 26947de566..3abaecc545 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,6 +56,33 @@ module.exports = { }, ], "import/order": "error", + "no-restricted-syntax": [ + "warn", + { + selector: + "BinaryExpression[operator='==='][right.type='Literal'][right.value=null]", + message: + "'=== null'ではなく'== null'を使用してください。詳細: https://github.com/VOICEVOX/voicevox/issues/1513", + }, + { + selector: + "BinaryExpression[operator='!=='][right.type='Literal'][right.value=null]", + message: + "'!== null'ではなく'!= null'を使用してください。詳細: https://github.com/VOICEVOX/voicevox/issues/1513", + }, + { + selector: + "BinaryExpression[operator='==='][right.type='Identifier'][right.name=undefined]", + message: + "'=== undefined'ではなく'== undefined'を使用してください。詳細: https://github.com/VOICEVOX/voicevox/issues/1513", + }, + { + selector: + "BinaryExpression[operator='!=='][right.type='Identifier'][right.name=undefined]", + message: + "'!== undefined'ではなく'!= undefined'を使用してください。詳細: https://github.com/VOICEVOX/voicevox/issues/1513", + }, + ], }, overrides: [ { diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 1a2aba9e14..ad9b7611f1 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,6 +1,6 @@ --- name: Question -about: 質問 (既存のIssueや一般事例を良く調べてからしてください) +about: 質問(既存のIssueや一般事例を良く調べてからしてください) labels: question --- diff --git a/.github/actions/download-engine/action.yml b/.github/actions/download-engine/action.yml index 4c61fcd9d0..73d087bdc9 100644 --- a/.github/actions/download-engine/action.yml +++ b/.github/actions/download-engine/action.yml @@ -2,6 +2,10 @@ name: "Download VOICEVOX ENGINE" description: | VOICEVOX ENGINEをダウンロードし、指定したディレクトリに展開する。 inputs: + repo: + description: "リポジトリ名。デフォルトはVOICEVOX/voicevox_engine。" + required: false + default: "VOICEVOX/voicevox_engine" version: description: "VOICEVOX ENGINEのバージョン。latest(デフォルト)、prerelease-latest、バージョン番号(例:0.14.4)で指定できる。" required: false @@ -37,7 +41,7 @@ runs: - name: Get version shell: bash run: | - curl -s https://api.github.com/repos/voicevox/voicevox_engine/releases \ + curl -s https://api.github.com/repos/${{ inputs.repo }}/releases \ -H 'authorization: Bearer ${{ github.token }}' \ -H 'content-type: application/json' > $TEMPDIR/releases.json @@ -52,8 +56,8 @@ runs: shell: bash run: | if [ "${{ inputs.target }}" = "" ]; then - OS="${{ runner.os }}" - TARGET="${OS,,}-cpu" # 小文字にする + OS=$(echo "${{ runner.os }}" | tr "[:upper:]" "[:lower:]") # 小文字にする + TARGET="$OS-cpu" else TARGET="${{ inputs.target }}" fi @@ -93,4 +97,5 @@ runs: else echo "run_path=$DEST/run" >> $GITHUB_OUTPUT fi - cat $TEMPDIR/target.json | jq -r '.tag_name' | sed -e 's_^_version=_' >> $GITHUB_OUTPUT + echo "version=$(jq -r '.tag_name' $TEMPDIR/target.json)" >> $GITHUB_OUTPUT + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60652ea503..8a3402e146 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,7 @@ on: code_signing: description: "コード署名する" type: boolean + default: false upload_artifact: description: "デバッグ用に成果物をartifactにアップロードするか" type: boolean @@ -26,8 +27,8 @@ on: env: VOICEVOX_ENGINE_REPO_URL: "https://github.com/VOICEVOX/voicevox_engine" - VOICEVOX_ENGINE_VERSION: 0.14.5 - VOICEVOX_RESOURCE_VERSION: 0.14.3 + VOICEVOX_ENGINE_VERSION: 0.14.6 + VOICEVOX_RESOURCE_VERSION: 0.14.4 VOICEVOX_EDITOR_VERSION: |- # releaseタグ名か、workflow_dispatchでのバージョン名か、999.999.999-developが入る ${{ github.event.release.tag_name || github.event.inputs.version || '999.999.999-develop' }} @@ -207,17 +208,23 @@ jobs: - name: Generate public/licenses.json run: npm run license:generate -- -o public/licenses.json - # build electronでコード署名するには環境変数を指定が必要だけど、 - # コード署名しない場合に環境変数を定義するとエラーになるので、動的に環境変数を足す - name: Define Code Signing Envs if: startsWith(matrix.os, 'windows-') && github.event.inputs.code_signing == 'true' run: | - # 複数行の文字列を環境変数に代入 - echo 'CSC_LINK<> $GITHUB_ENV - echo "${{ secrets.CERT_BASE64 }}" >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV - - echo 'CSC_KEY_PASSWORD=${{ secrets.CERT_PASSWORD }}' >> $GITHUB_ENV + bash build/codesign_setup.bash + THUMBPRINT="$(head -n 1 $THUMBPRINT_PATH)" + SIGNTOOL_PATH="$(head -n 1 $SIGNTOOL_PATH_PATH)" + echo "::add-mask::$THUMBPRINT" + + echo "WIN_CERTIFICATE_SHA1=$THUMBPRINT" >> $GITHUB_ENV + echo 'WIN_SIGNING_HASH_ALGORITHMS=["sha256"]' >> $GITHUB_ENV + echo "SIGNTOOL_PATH=$SIGNTOOL_PATH" >> $GITHUB_ENV + env: + ESIGNERCKA_USERNAME: ${{ secrets.ESIGNERCKA_USERNAME }} + ESIGNERCKA_PASSWORD: ${{ secrets.ESIGNERCKA_PASSWORD }} + ESIGNERCKA_TOTP_SECRET: ${{ secrets.ESIGNERCKA_TOTP_SECRET }} + THUMBPRINT_PATH: /tmp/esignercka_thumbprint.txt + SIGNTOOL_PATH_PATH: /tmp/signtool_path.txt # Build result will be exported to ${{ matrix.artifact_path }} - name: Build Electron @@ -234,8 +241,12 @@ jobs: - name: Reset Code Signing Envs if: startsWith(matrix.os, 'windows-') && github.event.inputs.code_signing == 'true' run: | - echo 'CSC_LINK=' >> $GITHUB_ENV - echo 'CSC_KEY_PASSWORD=' >> $GITHUB_ENV + bash build/codesign_cleanup.bash + echo 'WIN_CERTIFICATE_SHA1=' >> $GITHUB_ENV + echo 'WIN_SIGNING_HASH_ALGORITHMS=' >> $GITHUB_ENV + echo 'SIGNTOOL_PATH=' >> $GITHUB_ENV + env: + THUMBPRINT_PATH: /tmp/esignercka_thumbprint.txt - name: Rename NoEngine Prepackage run: | @@ -360,17 +371,23 @@ jobs: run: | rm ${{ matrix.compressed_artifact_name }}-${{ env.VOICEVOX_EDITOR_VERSION }}.zip - # build electronでコード署名するには環境変数を指定が必要だけど、 - # コード署名しない場合に環境変数を定義するとエラーになるので、動的に環境変数を足す - name: Define Code Signing Envs if: startsWith(matrix.os, 'windows-') && github.event.inputs.code_signing == 'true' run: | - # 複数行の文字列を環境変数に代入 - echo 'CSC_LINK<> $GITHUB_ENV - echo "${{ secrets.CERT_BASE64 }}" >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV - - echo 'CSC_KEY_PASSWORD=${{ secrets.CERT_PASSWORD }}' >> $GITHUB_ENV + bash build/codesign_setup.bash + THUMBPRINT="$(head -n 1 $THUMBPRINT_PATH)" + SIGNTOOL_PATH="$(head -n 1 $SIGNTOOL_PATH_PATH)" + echo "::add-mask::$THUMBPRINT" + + echo "WIN_CERTIFICATE_SHA1=$THUMBPRINT" >> $GITHUB_ENV + echo 'WIN_SIGNING_HASH_ALGORITHMS=["sha256"]' >> $GITHUB_ENV + echo "SIGNTOOL_PATH=$SIGNTOOL_PATH" >> $GITHUB_ENV + env: + ESIGNERCKA_USERNAME: ${{ secrets.ESIGNERCKA_USERNAME }} + ESIGNERCKA_PASSWORD: ${{ secrets.ESIGNERCKA_PASSWORD }} + ESIGNERCKA_TOTP_SECRET: ${{ secrets.ESIGNERCKA_TOTP_SECRET }} + THUMBPRINT_PATH: /tmp/esignercka_thumbprint.txt + SIGNTOOL_PATH_PATH: /tmp/signtool_path.txt # NOTE: prepackage can be removed before splitting nsis-web archive - name: Build Electron @@ -392,8 +409,12 @@ jobs: - name: Reset Code Signing Envs if: startsWith(matrix.os, 'windows-') && github.event.inputs.code_signing == 'true' run: | - echo 'CSC_LINK=' >> $GITHUB_ENV - echo 'CSC_KEY_PASSWORD=' >> $GITHUB_ENV + bash build/codesign_cleanup.bash + echo 'WIN_CERTIFICATE_SHA1=' >> $GITHUB_ENV + echo 'WIN_SIGNING_HASH_ALGORITHMS=' >> $GITHUB_ENV + echo 'SIGNTOOL_PATH=' >> $GITHUB_ENV + env: + THUMBPRINT_PATH: /tmp/esignercka_thumbprint.txt - name: Create Linux AppImage split if: endsWith(matrix.installer_artifact_name, '-appimage') diff --git a/.github/workflows/release_latest_dev.yml b/.github/workflows/release_latest_dev.yml new file mode 100644 index 0000000000..5c86fe9fe9 --- /dev/null +++ b/.github/workflows/release_latest_dev.yml @@ -0,0 +1,43 @@ +name: Release latest dev build + +# mainブランチが更新されるたびに開発版をビルドしてデプロイする。 +# バージョン(タグ)は最新リリースのバージョンを`X.Y.Z`としたときの`X.Y+1.0-dev`。 + +on: + push: + branches: + - main + paths-ignore: + - "docs/**" + - "tests/**" + +jobs: + latest-dev-build: + runs-on: ubuntu-latest + if: github.repository_owner == 'VOICEVOX' + steps: + - name: Trigger workflow_dispatch + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const latest_release = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + const split_version = latest_release.data.tag_name.split('.'); + const dev_version = `${split_version[0]}.${parseInt(split_version[1]) + 1}.0-dev`; + + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'build.yml', + ref: 'main', + inputs: { + version: dev_version, + prerelease: true + } + }) + + console.log(`Triggered workflow_dispatch for ${dev_version}`); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42a3daa9fa..631676390e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,13 +8,15 @@ on: workflow_dispatch: env: - VOICEVOX_ENGINE_VERSION: 0.14.4 + VOICEVOX_ENGINE_REPO: "VOICEVOX/voicevox_nemo_engine" # 軽いのでNemoを使う + VOICEVOX_ENGINE_VERSION: "0.14.0" defaults: run: shell: bash - + jobs: + # ビルドのテスト build-test: runs-on: windows-latest steps: @@ -23,7 +25,8 @@ jobs: uses: ./.github/actions/setup-environment - run: npm run electron:build_pnever - test: + # unit テスト + unit-test: runs-on: windows-latest steps: - uses: actions/checkout@v3 @@ -32,23 +35,82 @@ jobs: - run: npm run test:unit + # e2e テスト + e2e-test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + voicevox_engine_asset_name: linux-cpu + - os: macos-latest + voicevox_engine_asset_name: macos-x64 + - os: windows-latest + voicevox_engine_asset_name: windows-cpu + steps: + - uses: actions/checkout@v3 + - name: Setup environment + uses: ./.github/actions/setup-environment + + - name: Install xvfb and x11-xserver-utils + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -y xvfb x11-xserver-utils # for electron + sudo apt-get install -y libsndfile1 # for engine + - name: Download VOICEVOX ENGINE id: download-engine uses: ./.github/actions/download-engine with: + repo: ${{ env.VOICEVOX_ENGINE_REPO }} version: ${{ env.VOICEVOX_ENGINE_VERSION }} dest: ${{ github.workspace }}/voicevox_engine + target: ${{ matrix.voicevox_engine_asset_name }} - - name: Run npm run test:e2e + - name: Setup run: | + # playwright + npx playwright install + + # run.exe + chmod +x ${{ steps.download-engine.outputs.run_path }} + + # .env cp .env.test .env sed -i -e 's|"../voicevox_engine/run.exe"|"${{ steps.download-engine.outputs.run_path }}"|' .env + sed -i -e 's|"executionArgs": \[\],|"executionArgs": ["--port=50021"],|' .env + + - name: Run npm run test:browser-e2e + run: | + if [ -n "${{ runner.debug }}" ]; then + export DEBUG="pw:browser*" + fi + if [[ ${{ matrix.os }} == ubuntu-* ]]; then + xvfb-run --auto-servernum npm run test:browser-e2e + else + npm run test:browser-e2e + fi + + - name: Run npm run test:electron-e2e + run: | if [ -n "${{ runner.debug }}" ]; then - DEBUG=pw:browser* npm run test:e2e + export DEBUG="pw:browser*" + fi + if [[ ${{ matrix.os }} == ubuntu-* ]]; then + xvfb-run --auto-servernum npm run test:electron-e2e else - npm run test:e2e + npm run test:electron-e2e fi + - name: Upload playwright report to artifact + if: failure() + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: playwright-report + lint: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 44b68a9ab7..c3d792f470 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules /tests/e2e/videos/ /tests/e2e/screenshots/ +/test-results/* +/playwright-report/ # local env files .env.local diff --git a/Dockerfile b/Dockerfile index 1026939f4d..ec007cb85b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.16.0 +FROM node:18.13.0 WORKDIR /opt RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup_install.sh @@ -11,4 +11,4 @@ WORKDIR /work RUN npm ci EXPOSE 3000 -CMD ["/bin/sh"] \ No newline at end of file +CMD ["/bin/sh"] diff --git a/README.md b/README.md index 42f9a96182..5bc838bab9 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Issue 側で取り組み始めたことを伝えるか、最初に Draft プル ## 環境構築 [.node-version](.node-version) に記載されているバージョンの Node.js をインストールしてください。 -Node.js の管理ツール ([nvs](https://github.com/jasongin/nvs)や[Volta](https://volta.sh)など)を利用すると簡単にインストールでき、Node.js の自動切り替えもできます。 +Node.js の管理ツール([nvs](https://github.com/jasongin/nvs)や[Volta](https://volta.sh)など)を利用すると簡単にインストールでき、Node.js の自動切り替えもできます。 Node.js をインストール後、[このリポジトリ](https://github.com/VOICEVOX/voicevox.git) を Fork して `git clone` し、次のコマンドを実行してください。 @@ -118,7 +118,7 @@ npm ci ## 実行 -`.env.production`をコピーして`.env`を作成し、`DEFAULT_ENGINE_INFOS`内の`executionFilePath`に`voicevox_engine`のフルパスを指定します。 +`.env.production`をコピーして`.env`を作成し、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に`voicevox_engine`のフルパスを指定します。 [製品版 VOICEVOX](https://voicevox.hiroshiba.jp/) のディレクトリのパスを指定すれば動きます。 @@ -132,7 +132,7 @@ AppImage 版の場合は`$ /path/to/VOICEVOX.AppImage --appimage-mount`でファ VOICEVOX エディタの実行とは別にエンジン API のサーバを立てている場合は`executionFilePath`を指定する必要はありません。 これは製品版 VOICEVOX を起動している場合もあてはまります。 -また、エンジン API の宛先エンドポイントを変更する場合は`DEFAULT_ENGINE_INFOS`内の`host`を変更してください。 +また、エンジン API の宛先エンドポイントを変更する場合は`VITE_DEFAULT_ENGINE_INFOS`内の`host`を変更してください。 ```bash npm run electron:serve @@ -159,16 +159,41 @@ npm run electron:build ## テスト +### 単体テスト + ```bash npm run test:unit -npm run test:e2e +npm run test-watch:unit # 監視モード ``` -### 監視モード +### ブラウザ End to End テスト + +Electron の機能が不要な、UI や音声合成などの End to End テストを実行します。 +> **Note** +> 一部のエンジンの設定を書き換えるテストは、CI(Github Actions)上でのみ実行されるようになっています。 ```bash -npm run test-watch:unit -npm run test-watch:e2e +npm run test:browser-e2e +npm run test-watch:browser-e2e # 監視モード +npm run test-watch:browser-e2e -- --headed # テスト中の UI を表示 +``` + +Playwright を使用しているためテストパターンを生成することもできます。 +ブラウザ版を起動している状態で以下のコマンドを実行してください。 + +```bash +npx playwright codegen http://localhost:5173/#/home --viewport-size=800,600 +``` + +詳細は [Playwright ドキュメントの Test generator](https://playwright.dev/docs/codegen-intro) を参照してください。 + +### Electron End to End テスト + +Electron の機能が必要な、エンジン起動・終了などを含めた End to End テストを実行します。 + +```bash +npm run test:electron-e2e +npm run test-watch:electron-e2e # 監視モード ``` ## 依存ライブラリのライセンス情報の生成 @@ -204,15 +229,9 @@ typos ## 型チェック TypeScript の型チェックを行います。 -※ 現在チェック方法は 2 種類ありますが、将来的に 1 つになります。 ```bash -# .tsのみ型チェック npm run typecheck - -# .vueも含めて型チェック -# ※ 現状、大量にエラーが検出されます。 -npm run typecheck:vue-tsc ``` ## Markdownlint @@ -234,9 +253,7 @@ shellcheck ./build/*.sh ## OpenAPI generator -音声合成エンジンが起動している状態で以下のコマンドを実行してください。 -なお、2023/07/02 現在、openapi-generator の最新版に[パッチ](https://github.com/OpenAPITools/openapi-generator/pull/15943)を当てたものを使わないと更新できない状態になっています。 -詳細は[こちら](https://github.com/VOICEVOX/voicevox/pull/1361) +音声合成エンジンが起動している状態で以下のコマンドを実行してください。 ```bash curl http://127.0.0.1:50021/openapi.json >openapi.json diff --git a/android/app/src/main/cpp/voicevox_core_wrapper/main.cpp b/android/app/src/main/cpp/voicevox_core_wrapper/main.cpp index 607bea1355..533d418884 100644 --- a/android/app/src/main/cpp/voicevox_core_wrapper/main.cpp +++ b/android/app/src/main/cpp/voicevox_core_wrapper/main.cpp @@ -11,6 +11,7 @@ VoicevoxCore *voicevoxCore; bool assertCoreLoaded(JNIEnv *env) { if (!voicevoxCore) { + __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "voicevoxCore is not loaded"); jclass jExceptionClass = env->FindClass("java/lang/RuntimeException"); env->ThrowNew(jExceptionClass, "voicevoxCore is not loaded"); return false; diff --git a/build/codesign_cleanup.bash b/build/codesign_cleanup.bash new file mode 100644 index 0000000000..c3e8bb02e3 --- /dev/null +++ b/build/codesign_cleanup.bash @@ -0,0 +1,20 @@ +# !!! コードサイニング証明書を取り扱うので取り扱い注意 !!! + +# eSignerCKAで読み込んだコードサイニング証明書を破棄する + +set -eu + +if [ ! -v THUMBPRINT_PATH ]; then # THUMBPRINTの出力先 + echo "THUMBPRINT_PATHが未定義です" + exit 1 +fi + +if [ ! -v ESIGNERCKA_INSTALL_DIR ]; then # eSignerCKAのインストール先 + ESIGNERCKA_INSTALL_DIR='..\eSignerCKA' +fi + +# 証明書を破棄 +powershell "& '$ESIGNERCKA_INSTALL_DIR\eSignerCKATool.exe' unload" + +# THUMBPRINTを削除 +rm "$THUMBPRINT_PATH" diff --git a/build/codesign_setup.bash b/build/codesign_setup.bash new file mode 100644 index 0000000000..8ad1e06144 --- /dev/null +++ b/build/codesign_setup.bash @@ -0,0 +1,61 @@ +# !!! コードサイニング証明書を取り扱うので取り扱い注意 !!! + +# eSignerCKAを使ってコードサイニング証明書を読み込む +# electronから利用するためにTHUMBPRINTとsigntoolのパスを出力する + +set -eu + +if [ ! -v ESIGNERCKA_USERNAME ]; then # eSignerCKAのユーザー名 + echo "ESIGNERCKA_USERNAMEが未定義です" + exit 1 +fi +if [ ! -v ESIGNERCKA_PASSWORD ]; then # eSignerCKAのパスワード + echo "ESIGNERCKA_PASSWORDが未定義です" + exit 1 +fi +if [ ! -v ESIGNERCKA_TOTP_SECRET ]; then # eSignerCKAのTOTP Secret + echo "ESIGNERCKA_TOTP_SECRETが未定義です" + exit 1 +fi +if [ ! -v THUMBPRINT_PATH ]; then # THUMBPRINTの出力先 + echo "THUMBPRINT_PATHが未定義です" + exit 1 +fi +if [ ! -v SIGNTOOL_PATH_PATH ]; then # 対応しているsigntoolのパスの出力先 + echo "SIGNTOOL_PATH_PATHが未定義です" + exit 1 +fi + +if [ ! -v ESIGNERCKA_INSTALL_DIR ]; then + ESIGNERCKA_INSTALL_DIR='..\eSignerCKA' +fi + +# eSignerCKAのセットアップ +if [ ! -d "$ESIGNERCKA_INSTALL_DIR" ]; then + curl -LO "https://github.com/SSLcom/eSignerCKA/releases/download/v1.0.6/SSL.COM-eSigner-CKA_1.0.6.zip" + unzip -o SSL.COM-eSigner-CKA_1.0.6.zip + mv *eSigner*CKA_*.exe eSigner_CKA_Installer.exe + powershell " + & ./eSigner_CKA_Installer.exe /CURRENTUSER /VERYSILENT /SUPPRESSMSGBOXES /DIR="$ESIGNERCKA_INSTALL_DIR" | Out-Null + & '$ESIGNERCKA_INSTALL_DIR\eSignerCKATool.exe' config -mode product -user '$ESIGNERCKA_USERNAME' -pass '$ESIGNERCKA_PASSWORD' -totp '$ESIGNERCKA_TOTP_SECRET' -key '$ESIGNERCKA_INSTALL_DIR\master.key' -r + & '$ESIGNERCKA_INSTALL_DIR\eSignerCKATool.exe' unload + " + rm SSL.COM-eSigner-CKA_1.0.6.zip eSigner_CKA_Installer.exe +fi + +# 証明書を読み込む +powershell "& '$ESIGNERCKA_INSTALL_DIR\eSignerCKATool.exe' load" + +THUMBPRINT=$( + powershell ' + $CodeSigningCert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1 + echo "$($CodeSigningCert.Thumbprint)" + ' +) + +# THUMBPRINTを出力 +echo "$THUMBPRINT" >"$THUMBPRINT_PATH" + +# 対応しているsigntoolのパスを出力 +SIGNTOOL_PATH=$(ls "C:/Program Files (x86)/Windows Kits/"10/bin/*/x86/signtool.exe | sort -V | tail -n 1) # なぜかこれじゃないと動かない +echo "$SIGNTOOL_PATH" >"$SIGNTOOL_PATH_PATH" diff --git a/build/downloadMobileAssets.mts b/build/downloadMobileAssets.mts index 52a45a788f..16076f0644 100644 --- a/build/downloadMobileAssets.mts +++ b/build/downloadMobileAssets.mts @@ -54,11 +54,16 @@ const downloadAndCompressModel = async () => { "git", "clone", "https://github.com/VOICEVOX/voicevox_core.git", - __dirname + "/vendored/voicevox_core", - "--depth", - "1" + __dirname + "/vendored/voicevox_core" ); } + await runCommand( + "git", + "-C", + __dirname + "/vendored/voicevox_core", + "checkout", + "b8c1b316203a0963ce3d3aca787fd392cceba930" + ); await runCommand( sevenZip, "a", diff --git a/build/installer.nsh b/build/installer.nsh index 58589d1555..267b951a4e 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -799,3 +799,154 @@ FunctionEnd ${endif} FunctionEnd !macroend + +; "%VITE_APP_NAME%"が空の状態でビルドすると他のソフトのファイルを消してしまうためビルドエラーにする。 +!define DOLLAR "$" +!if "$%VITE_APP_NAME%" == "${DOLLAR}%VITE_APP_NAME%" + !error 'The environment variable "%VITE_APP_NAME%" is undefined.' +!endif +!if "$%VITE_APP_NAME%" == "" + !error 'The environment variable "%VITE_APP_NAME%" is empty.' +!endif + +!macro locateVvppTmp callbacks + ${Locate} "$APPDATA\$%VITE_APP_NAME%\vvpp-engines\.tmp" "/L=D /M=????????????? /G=0" ${callbacks} +!macroend + +!macro locateVvppEngines callbacks + ${Locate} "$APPDATA\$%VITE_APP_NAME%\vvpp-engines" "/L=D /M=*+????????-????-????-????-???????????? /G=0" ${callbacks} +!macroend + +!macro customUninstallPage + ; エンジンディレクトリが存在する場合は、消去するかのチェックボックスを案内する + ; 存在しない場合はそのまま終了する + UninstPage custom un.removeUserDataPage un.removeUserDataPageLeave + + Function un.removeUserDataPage + Push $0 + + Var /GLOBAL isExistEngine + StrCpy $isExistEngine "0" + + ${If} $installMode == "all" + SetShellVarContext current + ${EndIf} + + Push $R0 + + StrCpy $R0 "0" + !insertmacro locateVvppTmp un.isExistVvppTmp + ${If} $R0 == "1" + StrCpy $isExistEngine "1" + ${Else} + RMDir "$APPDATA\$%VITE_APP_NAME%\vvpp-engines\.tmp" + ${EndIf} + ClearErrors + + ${If} $isExistEngine == "0" + StrCpy $R0 "0" + !insertmacro locateVvppEngines un.isExistVvppEngines + ${If} $R0 == "1" + StrCpy $isExistEngine "1" + ${Else} + RMDir "$APPDATA\$%VITE_APP_NAME%\vvpp-engines" + ${EndIf} + ClearErrors + ${EndIf} + + Pop $R0 + + ${If} $installMode == "all" + SetShellVarContext all + ${EndIf} + + ${If} $isExistEngine == "0" + Pop $0 + Abort + ${EndIf} + + nsDialogs::Create 1018 + Pop $0 + + ${If} $0 == "error" + Pop $0 + Abort + ${EndIf} + + ; 既にアンインストールは完了してしまっているためキャンセルボタンは無効化する + GetDlgItem $0 $HWNDPARENT 2 + EnableWindow $0 0 + + ${NSD_CreateCheckBox} 0 0 100% 12u "追加エンジンを削除する" + Var /GLOBAL removeAdditionalEngineCheckBox + Pop $removeAdditionalEngineCheckBox + + nsDialogs::Show + + Pop $0 + FunctionEnd + + Function un.removeUserDataPageLeave + Push $0 + ; 削除の処理 + ${NSD_GetState} $removeAdditionalEngineCheckBox $0 + + ${If} $0 == ${BST_CHECKED} + ${If} $installMode == "all" + SetShellVarContext current + ${EndIf} + + !insertmacro locateVvppTmp un.removeVvppTmp + RMDir "$APPDATA\$%VITE_APP_NAME%\vvpp-engines\.tmp" + !insertmacro locateVvppEngines un.removeVvppEngines + RMDir "$APPDATA\$%VITE_APP_NAME%\vvpp-engines" + ; 未知のファイルが残っている場合削除されずにエラーフラグが立つのでクリアする + ClearErrors + + ${If} $installMode == "all" + SetShellVarContext all + ${EndIf} + ${EndIf} + Pop $0 + FunctionEnd + + Function un.isExistVvppTmp + ; 実行された場合は"$R0"に"1"を代入する。 + StrCpy $R0 "1" + Push "StopLocate" + FunctionEnd + + Function un.removeVvppTmp + RMDir /r "$R9" + Push "" + FunctionEnd + + Function un.isExistVvppEngines + ; "engine_manifest.json"がある場合"$R0"に"1"を代入する。 + ${If} ${FileExists} "$R9\engine_manifest.json" + StrCpy $R0 "1" + Push "StopLocate" + ${Else} + Push "" + ${EndIf} + FunctionEnd + + Function un.removeVvppEngines + ; "engine_manifest.json"があるか確認してから削除する。 + ${If} ${FileExists} "$R9\engine_manifest.json" + RMDir /r "$R9" + ClearErrors + ${EndIf} + Push "" + FunctionEnd + + ; MUI_UNPAGE_FINISHの戻るボタンを無効化する + !define MUI_PAGE_CUSTOMFUNCTION_SHOW un.disableBack + + Function un.disableBack + Push $0 + GetDlgItem $0 $HWNDPARENT 3 + EnableWindow $0 0 + Pop $0 + FunctionEnd +!macroend diff --git a/docker-compose.yml b/docker-compose.yml index d0ab9641c8..c7215ca192 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,6 @@ services: ports: - 3000:3000 volumes: - - .:/work:cache + - .:/work:cached tty: true stdin_open: true diff --git "a/docs/UI\345\220\215\347\247\260\343\201\250Vue\343\203\225\343\202\241\343\202\244\343\203\253\345\220\215\343\201\256\345\257\276\345\277\234\346\227\251\350\246\213\350\241\250.md" "b/docs/UI\345\220\215\347\247\260\343\201\250Vue\343\203\225\343\202\241\343\202\244\343\203\253\345\220\215\343\201\256\345\257\276\345\277\234\346\227\251\350\246\213\350\241\250.md" new file mode 100644 index 0000000000..56b96d2d57 --- /dev/null +++ "b/docs/UI\345\220\215\347\247\260\343\201\250Vue\343\203\225\343\202\241\343\202\244\343\203\253\345\220\215\343\201\256\345\257\276\345\277\234\346\227\251\350\246\213\350\241\250.md" @@ -0,0 +1,59 @@ +# UI 名称と Vue ファイル名の対応早見表 + +## 注意事項など + +このファイルは更新漏れなどが発生する可能性が高めです。実際のファイルも併せてご確認ください。 + +各 UI の名称が分からない場合は、[VOICEVOX 専用 UI の名称](./UX・UIデザインの方針.md#voicevox-専用-ui-の名称) をご覧ください。 + +## 対応早見 + +全ファイル共通の拡張子`.vue`は省略しています。 + +### views ディレクトリ + +- メイン画面全体 ・・・ [EditorHome](../src/views/EditorHome.vue) + +### compornents ディレクトリ + +- 最上部のバー(メニュー含む) ・・・ [MenuBar](../src/components/MenuBar.vue) + - メニュー + - メニューの項目リスト ・・・ [MenuItem](../src/components/MenuItem.vue) + - メニューのボタン ・・・ [MenuButton](../src/components/MenuButton.vue) + - エンジン + - エンジンの管理 ・・・ [EngineManageDialog](../src/components/EngineManageDialog.vue) + - 設定 + - キー割り当て ・・・ [HotkeySettingDialog](../src/components/HotkeySettingDialog.vue) + - ツールバーのカスタマイズ ・・・ [HeaderBarCustomDialog](../src/components/HeaderBarCustomDialog.vue) + - キャラクター並び替え・試聴 ・・・ [CharacterOrderDialog](../src/components/CharacterOrderDialog.vue) + - サンプルボイス一覧の各キャラクター ・・・ [CharacterTryListenCard](../src/components/CharacterTryListenCard.vue) + - デフォルトスタイル ・・・ [DefaultStyleListDialog](../src/components/DefaultStyleListDialog.vue) + - 個別選択 ・・・ [DefaultStyleSelectDialog](../src/components/DefaultStyleSelectDialog.vue) + - 読み方&アクセント辞書 ・・・ [DictionaryManageDialog](../src/components/DictionaryManageDialog.vue) + - オプション ・・・ [SettingDialog](../src/components/SettingDialog.vue) + - 書き出しファイル名パターン ・・・ [FileNamePatternDialog](../src/components/FileNamePatternDialog.vue) + - ヘルプ ・・・ `help`ディレクトリ + - [HelpDialog](../src/components/help/HelpDialog.vue) の`pagedata`の`components`をご参照ください。 + - ウィンドウ右上のボタン群(ピンボタン含む) ・・・ [TitleBarButtons](../src/components/TitleBarButtons.vue) + - ピンボタン以外のボタン ・・・ [MinMaxCloseButtons](../src/components/MinMaxCloseButtons.vue) +- ツールバー ・・・ [HeaderBar](../src/components/HeaderBar.vue) +- キャラクター表示欄 ・・・ [CharacterPortrait](../src/components/CharacterPortrait.vue) +- 台本欄(テキスト欄追加ボタンを含む) ・・・ [views/EditorHome](../src/views/EditorHome.vue) に含まれる + - レーン(行番号・テキスト欄含む) ・・・ [AudioCell](../src/components/AudioCell.vue) + - キャラクターアイコン ・・・ [CharacterButton](../src/components/CharacterButton.vue) + - コンテキスト(右クリック)メニュー ・・・ [ContextMenu](../src/components/ContextMenu.vue) +- パラメータ調整欄 ・・・ [AudioInfo](../src/components/AudioInfo.vue) + - プリセット管理 ・・・ [PresetManageDialog](../src/components/PresetManageDialog.vue) +- 詳細調整欄(各項目・再生ボタンを含む) ・・・ [AudioDetail](../src/components/AudioDetail.vue) + - アクセント項目のうち、文字以外の部分の UI ・・・ [AudioAccent](../src/components/AudioAccent.vue) + - イントネーション・長さ項目のスライダー [AudioParameter](../src/components/AudioParameter.vue) +- その他 + - 初回起動時に表示される画面 + - 利用規約 ・・・ [AcceptTermsDialog](../src/components/AcceptTermsDialog.vue) + - データ収集とプライバシーポリシー ・・・ [AcceptRetrieveTelemetryDialog](../src/components/AcceptRetrieveTelemetryDialog.vue) + - 起動時に表示される画面 + - 追加キャラクターの紹介 ・・・ [CharacterOrderDialog](../src/components/CharacterOrderDialog.vue)(設定 / キャラクター並び替え・試聴 と共通) + - 「音声書き出し」時の成否の通知 ・・・ [SaveAllResultDialog](../src/components/SaveAllResultDialog.vue) + - 一度のみ表示されるヒント ・・・ [ToolTip](../src/components/ToolTip.vue) + - 音声生成中の進捗表示 ・・・ [ProgressDialog](../src/components/ProgressDialog.vue) + - エラー記録用(UI には影響なし) ・・・ [ErrorBoundary](../src/components/ErrorBoundary.vue) diff --git "a/docs/UX\343\203\273UI\343\203\207\343\202\266\343\202\244\343\203\263\343\201\256\346\226\271\351\207\235.md" "b/docs/UX\343\203\273UI\343\203\207\343\202\266\343\202\244\343\203\263\343\201\256\346\226\271\351\207\235.md" index ac88e9abca..fe22d824bd 100644 --- "a/docs/UX\343\203\273UI\343\203\207\343\202\266\343\202\244\343\203\263\343\201\256\346\226\271\351\207\235.md" +++ "b/docs/UX\343\203\273UI\343\203\207\343\202\266\343\202\244\343\203\263\343\201\256\346\226\271\351\207\235.md" @@ -1,9 +1,9 @@ # UI/UX デザイン方針 -## 基本方針 - メモ程度ですが、VOICEVOX の UX や UI のデザインの設計方針を共有します。 +## 基本方針 + - 最も重視していることは、「ユーザーが作ろうとしたコンテンツが完成までたどり着くこと」です - この思想から、以下のことを意識してデザインしています - 使いたくなるように感じること @@ -30,6 +30,12 @@ - 音声の品質を手軽に高められること - 音声を調声できることがわかるようなファーストビューにしています +### ミッションとソフトウェアの方針の対応 + +- [ミッション](./ミッション・バリュー・ビジョン.md#ミッション)達成には UGC(ユーザー生成コンテンツ)が最重要だと考えています + - ユーザーが作ったコンテンツが界隈を広げるという考え方です +- この思想から、より良いコンテンツがより多く作られることをソフトウェアの方針としています + ## デザインの意思決定のプロセス まだ試行錯誤を重ねていますが、だいたいこのような流れで進めています。 @@ -39,6 +45,7 @@ - テキストでも手書きの図でもサンプル実装でもなんでも OK です 2. 実際に実装してプルリクエストを作成 - UI/UX デザインは触ってみないとわからない部分が多いので、実装をお願いする形になります + - デザインの変更はコード量が少なくても内容がかさばるので、+-合計80程度の差分に収めておくとマージまでがスムーズです 3. 問題箇所を洗い出す 4. マージ 5. 全体のデザインを見て必要であればメンテナーが微調整 @@ -46,10 +53,50 @@ ## 細かいデザインガイドライン - キャンセルボタンは左 +- ダイアログとトーストの使い分け + - ユーザーの操作に起因し、かつエラーや頻度が低いフィードバックにはダイアログを表示(`SHOW_DIALOG_`系) + - 例: [エンジンの再起動が必要です]エンジンが停止しているため、音声を生成できません。エンジンを再起動しますか?[再起動しない/再起動する] + - それ以外はトーストで通知(`SHOW_NOTIFY_`系) + - 例: 音声を書き出しました。 + - 例: エンジンが異常終了しました。エンジンの再起動が必要です。 +- ダイアログの warning と confirm の使い分け + - 続行することが望まれそうな場合は confirm + - キャンセルすることが望まれそうな場合は warning ## 文章など -### 名称 +### VOICEVOX 専用 UI の名称 + +🚷 のマークがついてるものはまだユーザー向けの名称ではないもの(案内で使わない) + +具体的にどのファイルと対応しているかについては、[UI 名称と Vue ファイル名の対応早見表](UI名称とVueファイル名の対応早見表.md) もご参照ください。 + +- メニュー ・・・ 最上段のバー、ファイルや設定などの項目がある + - ピンボタン 🚷 ・・・ 閉じるボタン辺りの横にある最前面固定機能 +- ツールバー ・・・ メニューの下にあるバー、カスタマイズが可能 +- キャラクター表示欄 🚷 ・・・ 画面左にあるパネル +- 台本欄 🚷 ・・・ テキスト欄などがあるパネル + - レーン 🚷 ・・・ テキスト1行が含まれる区画、ユーザー向けにはこれを指して「テキスト欄」と呼ぶこともある + - 行番号 ・・・ レーンの番号、レーンがユーザー向けになったら「レーン番号」へ + - キャラクターアイコン ・・・ キャラクターやスタイルを変更できるボタン + - テキスト欄 ・・・ 音声合成するためのテキストを入力する区画 + - テキスト欄追加ボタン 🚷 ・・・ レーンを追加するボタン +- パラメータ調整欄 🚷 ・・・ 画面右にあるパラメータなどを調整できるパネル +- 詳細調整欄 🚷 ・・・ 画面下にあるイントネーションなどを調整できるパネル + - 項目 ・・・ 詳細調整欄のイントネーションやアクセントなどの選択ボタン + - 再生ボタン ・・・ 音声を再生できるボタン + - アクセント区間 ・・・ 1つのアクセント句を表示する区画(より良い名称を求む) + - 文字ごとの区間 🚷 ・・・ アクセント区画の1文字の表示区画、ユーザー向けには「文字ごと」と呼ぶこともある + - 読み ・・・ 読み仮名の表示区画 + +#### その他の UI パーツ + +- 欄 ・・・ 視覚的に区切られている場所のこと +- パラメータの直接入力 🚷 ・・・ パラメータをクリックして直接入力できる区画、普通のテキスト表示と何ら変わらないので名称が無い +- スライダー ・・・ アクセントや話速などを調整できる +- 選択中のレーン・アクセント区間 🚷 ・・・ アクティブなレーン・アクセント区間のこと + +### その他の概念の名称 - ユーザーに提示する場合は「ディレクトリ」ではなく「フォルダ」 - マルチエンジン周り @@ -63,8 +110,9 @@ - アルファベットは半角 - 括弧は全角 + - ただし、中身が全て半角文字の場合に限り、括弧も半角 - です/ます調 -- 項目名は名詞で終わる(体言止め) +- 項目名は名詞で終わる(体言止め) - button の中身は「~する」 ### tooltip diff --git "a/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" "b/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" index 06f91c9bb9..cb7761e068 100644 --- "a/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" +++ "b/docs/res/\347\265\202\344\272\206\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" @@ -1,46 +1,57 @@ # 終了シーケンス図 ```mermaid -flowchart - node_1>"アプリ実行中"] -->|"アプリを再起動"| node_3["back.RESTART_APP"] +flowchart TD + node_1>"アプリ実行中"] -.->|"MacのCmd+Q"| subgraph_2["app.before_quit"] style node_1 fill:#ffbbbb,stroke:#ff0000 - node_3 --> node_4(["willRestart=true"]) - node_1 -.->|"MacのCmd+Q"| subgraph_2["app.before_quit"] node_21{{"winがclose済みか"}} -.->|"NO"| subgraph_1["win.close"] - node_1 -->|"アプリ起動直後のVVPPインストール後の再起動"| 132024(["willRestart=true"]) - 132024 --> 512010["app.quit"] - 512010 -.-> subgraph_2 - 438128["win.close"] -.-> subgraph_1 - node_1 -.->|"ウィンドウを閉じる"| subgraph_1 - node_4 --> 438128 - 442878["event.preventDefault"] --> 571782["Vuex.CHECK_EDITED_AND_NOT_WAVE"] - node_9["event.preventDefault"] --> 571782 + node_1 -.->|"MacのCmd+W・WinのAlt+F4"| subgraph_1 node_15["app.quit"] -.-> subgraph_2 - a{{"willQuit"}} -.-> subgraph_2 - node_8["win.destroy"] -.-> subgraph_1 - 701221{{"willRestart"}} -->|"false"| node_21 + a{{"willQuit"}} -.->|"true"| subgraph_2 node_21 -.->|"YES"| node_23>"アプリ終了"] style node_23 fill:#bbbbff,stroke:#0000ff - subgraph 571782["Vuex.CHECK_EDITED_AND_NOT_WAVE"] - node_5["back.CLOSE_WINDOW"] --> node_7(["willQuit=true"]) - node_7 --> node_8 - 453066{" "} -->|"キャンセル"| node_6>"アプリ実行中に戻る"] - 453066 --> node_5 + 846215{" "} -->|"reload"| 186768["RELOAD_APP"] + node_1 -->|"×ボタン"| 295190(["close"]) + node_1 -->|"アプリを再読み込み"| 929152(["reload"]) + 846215 -->|"close"| 208965["back.CLOSE_WINDOW"] + node_9["event.preventDefault"] --> 295190 + 295190 --> 571782["Vuex.CHECK_EDITED_AND_NOT_SAVE"] + 929152 --> 571782 + 177756{{"alreadyCompleted?"}} -->|"true"| node_21 + 442878["event.preventDefault"] --> 295190 + node_8["win.destroy"] -.-> subgraph_2 + 705785[["cleanupEngines"]] ~~~ 793927["cleanupEngines"] + 198115["win.loadURL"] --> 562861>"UIの描画"] + 454139[["cleanupEngines"]] ~~~ 793927 + subgraph 186768["RELOAD_APP"] + 705785 --> 700765{{"alreadyCompleted?"}} + 700765 -->|"false"| 462289["await"] + 700765 -->|"true"| 464405[["launchEngines"]] + 462289 --> 464405 + 464405 --> 198115 + 494918["win.loadURL(dummy)"] --> 705785 end - subgraph subgraph_2["app.before_quit"] - c["willQuit"] -->|"false"| node_9 - node_13["event.preventDefault"] --> node_14["全エンジンkill待機"] - node_19(["willRestart=false, willQuit=false"]) --> node_20>"back.start"] + subgraph 208965["back.CLOSE_WINDOW"] + node_7(["willQuit=true"]) --> node_8 + end + subgraph 571782["Vuex.CHECK_EDITED_AND_NOT_SAVE"] + 846215 -->|"キャンセル"| node_6>"アプリ実行中に戻る"] + end + subgraph 793927["cleanupEngines"] node_12["engine.killEngineAll"] --> 889691{{"numLivingEngineProcess"}} - 889691 -->|">0"| 701221 - 701221 -->|"true"| node_17["event.preventDefault"] - c -->|"true"| node_12 889691 -->|"0"| 916552{{"hasMarkedEngineDirs"}} - 916552 -->|"false"| 701221 - 916552 -->|"true"| node_13 - node_17 --> node_19 - node_14 --> node_18["vvpp.handleMarkedEngineDirs"] - node_18 --> node_15 + node_14["全エンジンkill待機"] --> node_18["vvpp.handleMarkedEngineDirs"] + 889691 -->|">0"| node_14 + 916552 -->|"false"| node_14 + 916552 -->|"true"| 655722["何もしない"] + end + subgraph subgraph_2["app.before_quit"] + c["willQuit"] -->|"false"| node_9 + c -->|"true"| 454139 + 454139 --> 177756 + 177756 -->|"false"| node_13["event.preventDefault"] + node_13 --> 322763["await"] + 322763 --> node_15 end subgraph subgraph_1["win.close"] a -->|"false"| 442878 diff --git "a/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" "b/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" index 561956e4a8..191115bc70 100644 --- "a/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" +++ "b/docs/res/\350\265\267\345\213\225\343\202\267\343\203\274\343\202\261\343\203\263\343\202\271\345\233\263.md" @@ -3,20 +3,17 @@ ```mermaid flowchart 174170["back.installVvppEngineWithWarning"] --> 786961["back.installVvppEngine"] - 786961 --> 409576{{"再起動するか"}} - 409576 -->|"する"| 173803>"アプリ起動直後のVVPPインストール後の再起動へ"] 764022["(画面読み込み)"] --> 698565["App.vue"] 764022 --> 332024["EditorHome.vue"] - 389651["back.start"] --> 733212["back.createWindow"] - 733212 -.-> 764022 + 733212["back.createWindow"] -.-> 764022 448821>"アプリ停止中"] -.-> 430173["app.ready"] style 448821 fill:#ffbbbb,stroke:#ff0000 430173 -->|"ある"| 174170 - 409576 -->|"しない"| 389651 - 430173 -->|"ない"| 389651 + 430173 -->|"ない"| 389651["back.start"] 698565 -.-> 704891>"アプリ実行中"] style 704891 fill:#bbbbff,stroke:#0000ff 332024 -.-> 704891 + 786961 --> 389651 subgraph 332024["EditorHome.vue"] 709863["Vuex.GET_ENGINE_INFOS"] --> 773040["Vuex.POST_ENGINE_START"] subgraph 773040["Vuex.POST_ENGINE_START"] @@ -31,13 +28,19 @@ flowchart end end subgraph 389651["back.start"] - 250263["store.get engineSettings"] --> 222321["store.set engineSettings"] - 870482["store.get registeredEngineDirs"] --> 250263 - 222321 --> 967432["engine.runEngineAll"] - 656570["engine.fetchEngineInfos"] --> 870482 - 110954["engine.initializeEngineInfosAndAltPortInfo"] --> 656570 - subgraph 656570["engine.fetchEngineInfos"] - 267019["engine.fetchAdditionalEngineInfos"] + 967432["engine.runEngineAll"] --> 733212 + subgraph 733212["back.createWindow"] + 613440["win.loadURL"] + end + subgraph 548965["launchEngines"] + 250263["store.get engineSettings"] --> 222321["store.set engineSettings"] + 870482["store.get registeredEngineDirs"] --> 250263 + 222321 --> 967432 + 656570["engine.fetchEngineInfos"] --> 870482 + 110954["engine.initializeEngineInfosAndAltPortInfo"] --> 656570 + subgraph 656570["engine.fetchEngineInfos"] + 267019["engine.fetchAdditionalEngineInfos"] + end end end subgraph 430173["app.ready"] @@ -49,7 +52,4 @@ flowchart 225701["win.show"] end end - subgraph 733212["back.createWindow"] - 613440["win.loadURL"] - end ``` diff --git "a/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" "b/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" index 0ed42e2a27..9bfa7de10e 100644 --- "a/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" +++ "b/docs/\343\202\263\343\203\274\343\203\211\343\201\256\346\255\251\343\201\215\346\226\271.md" @@ -80,6 +80,7 @@ TODO - App.vue ・・・ Vue のルートになるコンポーネント。他の全てのコンポーネントの親。 - views ディレクトリ ・・・ 画面全体を覆うような Vue コンポーネントのディレクトリ。 - components ディレクトリ ・・・ UI のパーツになる Vue コンポーネントディレクトリ。 + - 具体的にどの UI と対応しているかについては、[UI 名称と Vue ファイル名の対応早見表](UI名称とVueファイル名の対応早見表.md) もご参照ください。 - store ディレクトリ ・・・ Vuex のストアのディレクトリ。アプリのロジックの大半はここに書かれる。 - electron ディレクトリ ・・・ Electron の ipc 通信などのコードが置かれるディレクトリ。 - browser ディレクトリ ・・・ WIP @@ -112,12 +113,17 @@ flowchart LR 945949 -->|"条件2"| 871294["受動的な処理は点線\n(イベント発生など)"] 366994 --> 617606["大きい関数(subgraph)"] 871294 -.->|"発生条件"| 617606 - 617606 --> 314405>"なんらかの状態"] - 617606 --> 230853>"シーケンスの終了"] + 572391[["外に定義した関数"]] --> 314405>"なんらかの状態"] + 572391 --> 230853>"シーケンスの終了"] style 230853 fill:#bbbbff,stroke:#0000ff + 617606 --> 572391 + 572391 ~~~|"透明な線で結んでもよし"| 513532["外に定義した関数"] subgraph 617606["大きい関数(subgraph)"] 776462["処理開始"] --> 992786["処理終了"] end + subgraph 513532["外に定義した関数"] + 327013["処理"] + end ``` 処理の prefix 一覧 diff --git "a/docs/\343\203\225\343\202\251\343\203\263\343\203\210\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/\343\203\225\343\202\251\343\203\263\343\203\210\343\201\253\343\201\244\343\201\204\343\201\246.md" index 6ed601cd9e..1d4c57a480 100644 --- "a/docs/\343\203\225\343\202\251\343\203\263\343\203\210\343\201\253\343\201\244\343\201\204\343\201\246.md" +++ "b/docs/\343\203\225\343\202\251\343\203\263\343\203\210\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -13,7 +13,7 @@ VOICEVOX では改変したRounded M+ 1pを使用しています。 Rounded M+をダウンロードします。 2. [ttfautohint](https://freetype.org/ttfautohint/)をインストールします。[^longnote]:[ttfautohint-py](https://pypi.org/project/ttfautohint-py/)を使用しました。 3. [woff2](https://github.com/google/woff2)をビルドします。 -4. `rounded-mplus-20150529.7z`から`rounded-mplus-1p-(ウェイト).ttf`を全て`src/fonts`に解凍します。 +4. `rounded-mplus-20150529.7z`から`rounded-mplus-1p-(ウェイト).ttf`を全て`src/fonts`に解凍します。 5. ttfautohintを使用して、Rounded M+のヒント情報を削除します。名前は`Unhinted Rounded M+ 1p`とします。 6. woff2を使用して、ttfからwoff2へ変換します。 diff --git "a/docs/\345\205\250\344\275\223\346\247\213\346\210\220.md" "b/docs/\345\205\250\344\275\223\346\247\213\346\210\220.md" index 3868803f24..41dd5175e5 100644 --- "a/docs/\345\205\250\344\275\223\346\247\213\346\210\220.md" +++ "b/docs/\345\205\250\344\275\223\346\247\213\346\210\220.md" @@ -12,8 +12,9 @@ [The Open Source Definition](https://opensource.org/osd) そのため VOICEVOX では、キャラクターの要素(声や見た目)を含まない OSS 版と、OSS 版をもとに構築してキャラクターの要素を含めた製品版を分けて開発・配布しています。 + - + ## 構成 diff --git "a/docs/\350\211\262\343\201\253\343\201\244\343\201\204\343\201\246.md" "b/docs/\350\211\262\343\201\253\343\201\244\343\201\204\343\201\246.md" index 510bf8e463..4a644c7121 100644 --- "a/docs/\350\211\262\343\201\253\343\201\244\343\201\204\343\201\246.md" +++ "b/docs/\350\211\262\343\201\253\343\201\244\343\201\204\343\201\246.md" @@ -9,7 +9,6 @@ | 変数名 | 役割 | | ---- | ---- | | primary | プライマリーカラー | -| primary-light | 明るめのプライマリーカラー | | display | 情報を表示するための色。文字色とか | | display-on-primary | プライマリーカラー上の情報を表示するための色 | | display-hyperlink | リンクを表示するための色 | diff --git a/electron-builder.config.js b/electron-builder.config.js index b3fede70ac..8fa391c643 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -1,6 +1,10 @@ // @ts-check const path = require("path"); const fs = require("fs"); +const dotenv = require("dotenv"); + +const dotenvPath = path.join(process.cwd(), ".env.production"); +dotenv.config({ path: dotenvPath }); const VOICEVOX_ENGINE_DIR = process.env.VOICEVOX_ENGINE_DIR ?? "../voicevox_engine/run.dist/"; @@ -17,6 +21,12 @@ const LINUX_EXECUTABLE_NAME = process.env.LINUX_EXECUTABLE_NAME; // ${productName}-${version}.${ext} const MACOS_ARTIFACT_NAME = process.env.MACOS_ARTIFACT_NAME; +// コード署名証明書 +const WIN_CERTIFICATE_SHA1 = process.env.WIN_CERTIFICATE_SHA1; +const WIN_SIGNING_HASH_ALGORITHMS = process.env.WIN_SIGNING_HASH_ALGORITHMS + ? JSON.parse(process.env.WIN_SIGNING_HASH_ALGORITHMS) + : undefined; + const isMac = process.platform === "darwin"; // electron-builderのextraFilesは、ファイルのコピー先としてVOICEVOX.app/Contents/を使用する。 @@ -78,10 +88,6 @@ const builderOptions = { from: "build/README.txt", to: extraFilePrefix + "README.txt", }, - { - from: ".env.production", - to: extraFilePrefix + ".env", - }, { from: VOICEVOX_ENGINE_DIR, to: extraFilePrefix, @@ -108,6 +114,12 @@ const builderOptions = { arch: ["x64"], }, ], + certificateSha1: + WIN_CERTIFICATE_SHA1 !== "" ? WIN_CERTIFICATE_SHA1 : undefined, + signingHashAlgorithms: + WIN_SIGNING_HASH_ALGORITHMS !== "" + ? WIN_SIGNING_HASH_ALGORITHMS + : undefined, }, nsisWeb: { artifactName: diff --git a/openapitools.json b/openapitools.json index 3b40e47a45..06ac150787 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,6 @@ "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "5.3.0" + "version": "7.0.0" } } diff --git a/package-lock.json b/package-lock.json index b9e745e661..d5c53cff6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,13 @@ "@capacitor/splash-screen": "5.0.2", "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "async-lock": "1.4.0", "buffer": "6.0.3", "clone-deep": "4.0.1", "core-js": "3.12.1", "crypto-js": "4.1.1", "dayjs": "1.10.7", - "dotenv": "16.0.0", + "diff": "5.1.0", "electron-log": "4.4.8", "electron-store": "8.1.0", "electron-window-state": "5.0.3", @@ -48,8 +49,10 @@ "@openapitools/openapi-generator-cli": "2.3.3", "@playwright/test": "1.32.1", "@quasar/vite-plugin": "1.3.0", + "@types/async-lock": "1.4.0", "@types/clone-deep": "4.0.1", "@types/crypto-js": "4.1.1", + "@types/diff": "5.0.3", "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", "@types/glob": "8.0.0", @@ -68,7 +71,8 @@ "@vue/test-utils": "2.3.0", "cross-env": "7.0.3", "cross-var": "1.1.0", - "electron": "23.3.10", + "dotenv": "16.0.0", + "electron": "25.3.2", "electron-builder": "24.0.0-alpha.12", "electron-devtools-installer": "3.2.0", "eslint": "8.24.0", @@ -1986,6 +1990,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==", + "dev": true + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2034,6 +2044,12 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.3.tgz", + "integrity": "sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==", + "dev": true + }, "node_modules/@types/electron-devtools-installer": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.2.tgz", @@ -2149,9 +2165,9 @@ } }, "node_modules/@types/node": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.3.0.tgz", - "integrity": "sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ==", + "version": "18.18.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.4.tgz", + "integrity": "sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ==", "devOptional": true }, "node_modules/@types/normalize-package-data": { @@ -2712,15 +2728,6 @@ "pretty-format": "^27.5.1" } }, - "node_modules/@vitest/utils/node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/@volar/language-core": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.4.0.tgz", @@ -3794,6 +3801,11 @@ "node": ">=0.12.0" } }, + "node_modules/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -6917,6 +6929,14 @@ "wrappy": "1" } }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -7025,6 +7045,7 @@ "version": "16.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "dev": true, "engines": { "node": ">=12" } @@ -7119,14 +7140,14 @@ } }, "node_modules/electron": { - "version": "23.3.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-23.3.10.tgz", - "integrity": "sha512-PcEQo8letcJYUAP3x+GN4Qf4atS65EVxe3VhKrQUnSI6GA5+K1zrs3ur88iHXD4a3mJaH/491Y4pBTLxFqwXnA==", + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.2.tgz", + "integrity": "sha512-xiktJvXraaE/ARf2OVHFyTze1TksSbsbJgOaBtdIiBvUduez6ipATEPIec8Msz1n6eQ+xqYb6YF8tDuIZtJSPw==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^16.11.26", + "@types/node": "^18.11.18", "extract-zip": "^2.0.1" }, "bin": { @@ -7436,12 +7457,6 @@ "node": ">=8.0.0" } }, - "node_modules/electron/node_modules/@types/node": { - "version": "16.11.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz", - "integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==", - "dev": true - }, "node_modules/elementtree": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", @@ -18761,6 +18776,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-2+rYSaWrpdbQG3SA0LmMT6YxWLrI81AqpMlSkw3QtFc2HGDufkweQSn30Eiev7x9LL0oyFrBqk1PXOnB9IEgKg==", + "dev": true + }, "@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -18809,6 +18830,12 @@ "@types/ms": "*" } }, + "@types/diff": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.3.tgz", + "integrity": "sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==", + "dev": true + }, "@types/electron-devtools-installer": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.2.tgz", @@ -18924,9 +18951,9 @@ } }, "@types/node": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.3.0.tgz", - "integrity": "sha512-8/bnjSZD86ZfpBsDlCIkNXIvm+h6wi9g7IqL+kmFkQ+Wvu3JrasgLElfiPgoo8V8vVfnEi0QVS12gbl94h9YsQ==", + "version": "18.18.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.4.tgz", + "integrity": "sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ==", "devOptional": true }, "@types/normalize-package-data": { @@ -19325,14 +19352,6 @@ "loupe": "^2.3.6", "picocolors": "^1.0.0", "pretty-format": "^27.5.1" - }, - "dependencies": { - "diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true - } } }, "@volar/language-core": { @@ -20243,6 +20262,11 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "async-lock": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", + "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -22872,6 +22896,11 @@ "wrappy": "1" } }, + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -22962,7 +22991,8 @@ "dotenv": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", - "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==" + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "dev": true }, "dotenv-expand": { "version": "5.1.0", @@ -23044,22 +23074,14 @@ } }, "electron": { - "version": "23.3.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-23.3.10.tgz", - "integrity": "sha512-PcEQo8letcJYUAP3x+GN4Qf4atS65EVxe3VhKrQUnSI6GA5+K1zrs3ur88iHXD4a3mJaH/491Y4pBTLxFqwXnA==", + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.3.2.tgz", + "integrity": "sha512-xiktJvXraaE/ARf2OVHFyTze1TksSbsbJgOaBtdIiBvUduez6ipATEPIec8Msz1n6eQ+xqYb6YF8tDuIZtJSPw==", "dev": true, "requires": { "@electron/get": "^2.0.0", - "@types/node": "^16.11.26", + "@types/node": "^18.11.18", "extract-zip": "^2.0.1" - }, - "dependencies": { - "@types/node": { - "version": "16.11.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz", - "integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==", - "dev": true - } } }, "electron-builder": { diff --git a/package.json b/package.json index 61b85eb7d3..cb9500c493 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,14 @@ "scripts": { "test:unit": "vitest --run", "test-watch:unit": "vitest --watch", - "test:e2e": "playwright test", - "test-watch:e2e": "cross-env PWTEST_WATCH=1 playwright test", + "test:electron-e2e": "cross-env VITE_TARGET=electron playwright test", + "test-watch:electron-e2e": "cross-env PWTEST_WATCH=1 VITE_TARGET=electron playwright test", + "test:browser-e2e": "cross-env VITE_TARGET=browser playwright test --reporter=html", + "test-watch:browser-e2e": "cross-env PWTEST_WATCH=1 VITE_TARGET=browser playwright test --reporter=html", "lint": "eslint --ext .js,.vue,.ts *.config.* src tests build", "fmt": "eslint --ext .js,.vue,.ts *.config.* src tests build --fix", "markdownlint": "markdownlint --ignore node_modules/ --ignore dist/ --ignore dist_electron/ --ignore build/vendored/voicevox_engine --ignore build/vendored/voicevox_core ./", "typecheck": "tsc --noEmit", - "typecheck:vue-tsc": "vue-tsc --noEmit", "electron:build": "cross-env VITE_TARGET=electron vite build && electron-builder --config electron-builder.config.js", "electron:build_dir": "cross-env VITE_TARGET=electron vite build && electron-builder --config electron-builder.config.js --dir", "electron:build_pnever": "cross-env VITE_TARGET=electron vite build && electron-builder --config electron-builder.config.js --publish never", @@ -46,12 +47,13 @@ "@capacitor/splash-screen": "5.0.2", "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "async-lock": "1.4.0", "buffer": "6.0.3", "clone-deep": "4.0.1", "core-js": "3.12.1", "crypto-js": "4.1.1", "dayjs": "1.10.7", - "dotenv": "16.0.0", + "diff": "5.1.0", "electron-log": "4.4.8", "electron-store": "8.1.0", "electron-window-state": "5.0.3", @@ -83,8 +85,10 @@ "@openapitools/openapi-generator-cli": "2.3.3", "@playwright/test": "1.32.1", "@quasar/vite-plugin": "1.3.0", + "@types/async-lock": "1.4.0", "@types/clone-deep": "4.0.1", "@types/crypto-js": "4.1.1", + "@types/diff": "5.0.3", "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", "@types/glob": "8.0.0", @@ -103,7 +107,8 @@ "@vue/test-utils": "2.3.0", "cross-env": "7.0.3", "cross-var": "1.1.0", - "electron": "23.3.10", + "dotenv": "16.0.0", + "electron": "25.3.2", "electron-builder": "24.0.0-alpha.12", "electron-devtools-installer": "3.2.0", "eslint": "8.24.0", diff --git a/playwright.config.ts b/playwright.config.ts index 19a3fa6583..9865ea2294 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,44 @@ -import type { PlaywrightTestConfig } from "@playwright/test"; +import type { PlaywrightTestConfig, Project } from "@playwright/test"; +import { z } from "zod"; + +import dotenv from "dotenv"; +dotenv.config({ override: true }); + +let project: Project; +const additionalWebServer: PlaywrightTestConfig["webServer"] = []; + +if (process.env.VITE_TARGET === "electron") { + project = { name: "electron", testDir: "./tests/e2e/electron" }; +} else if (process.env.VITE_TARGET === "browser") { + project = { name: "browser", testDir: "./tests/e2e/browser" }; + + // エンジンの起動が必要 + 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()), + }) + .passthrough() + .array(); + const engineInfos = envSchema.parse(JSON.parse(defaultEngineInfosEnv)); + + engineInfos.forEach((info) => { + additionalWebServer.push({ + command: `${info.executionFilePath} ${info.executionArgs.join(" ")}`, + url: `${info.host}/version`, + reuseExistingServer: !process.env.CI, + }); + }); +} else { + throw new Error(`VITE_TARGETの指定が不正です。${process.env.VITE_TARGET}`); +} /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -// require('dotenv').config(); /** * See https://playwright.dev/docs/test-configuration. @@ -18,12 +52,20 @@ const config: PlaywrightTestConfig = { * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000, + timeout: 30 * 1000, }, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, + reporter: [ + [ + "html", + { + open: process.env.CI ? "never" : "on-failure", + }, + ], + ], /* Retry on CI only */ retries: process.env.CI ? 2 : 0, use: { @@ -31,19 +73,25 @@ const config: PlaywrightTestConfig = { actionTimeout: 0, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", + video: { + mode: "retain-on-failure", + }, }, /* Configure projects for major browsers */ - projects: [{ name: "electron" }], + projects: [project], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ // outputDir: 'test-results/', - webServer: { - command: "cross-env VITE_TARGET=electron vite --mode test", - port: 5173, - reuseExistingServer: !process.env.CI, - }, + webServer: [ + { + command: "vite --mode test", + port: 5173, + reuseExistingServer: !process.env.CI, + }, + ...additionalWebServer, + ], }; export default config; diff --git a/public/contact.md b/public/contact.md index 595da69dca..131378d7ff 100644 --- a/public/contact.md +++ b/public/contact.md @@ -13,6 +13,11 @@ - OS の種類・VOICEVOX のバージョン - 解決のために試されたこと - ウイルス対策ソフトなどの有無(関係がありそうであれば) +- エラーログ + +エラーログは VOICEVOX ソフトウェア内のヘルプ → お問い合わせ → 「ログフォルダを開く」ボタンから確認できます。 + +エラーログには PC ユーザー名などのシステム情報が含まれる場合があります。頂いたデータは問題解決のためのみに使用し、プライバシーは厳重に保護します。 開発者の方は GitHub Issue にてご報告・ご連絡ください。 diff --git a/public/howtouse.md b/public/howtouse.md index 96dc755b96..2ab9b59016 100644 --- a/public/howtouse.md +++ b/public/howtouse.md @@ -11,7 +11,7 @@ ### Windows 版 -起動しようとすると「Windows によって PC が保護されました」というダイアログが表示されるかもしれません。その際は「詳細情報」をクリックし、「実行」を選んでください。 +起動しようとすると「Windows によって PC が保護されました」というダイアログが表示されるかもしれません。その際は「詳細情報」をクリックし、発行元が「Kazuyuki Hiroshiba」であることを確認してから「実行」を選んでください。 「Windows によって PC が保護されました」というダイアログ「詳細情報」をクリックし、「実行」を選んでいる様子。 @@ -111,7 +111,7 @@ GPU をお持ちの方は、音声の生成がずっと速い GPU モードを イントネーション調整スペースを縦に広くした様子 -マウスホイールで調整することもできます。Ctrl キー (Mac 版では Command キー) を押しながらマウスホイールを使うと更に細かく調整できます。Alt キー (Mac 版では Option キー) を押しながらイントネーションや長さを調整することで、同じアクセント区間内を同時に調整できます。 +マウスホイールで調整することもできます。Ctrl キー(Mac 版では Command キー)を押しながらマウスホイールを使うと更に細かく調整できます。Alt キー(Mac 版では Option キー)を押しながらイントネーションや長さを調整することで、同じアクセント区間内を同時に調整できます。 また、「キ」や「ツ」や「ス」などが無声化されている場合、バーが灰色になっています。イントネーション欄のテキストをクリックすることで無声化を解くことができます。 @@ -158,7 +158,7 @@ GPU をお持ちの方は、音声の生成がずっと速い GPU モードを ```txt 四国めたん,おはようございます,こんにちは ずんだもん,こんばんは -四国めたん(あまあま),さようなら +四国めたん(あまあま),さようなら ``` このように読み込まれます。 @@ -179,7 +179,7 @@ GPU をお持ちの方は、音声の生成がずっと速い GPU モードを ## ショートカットキー 「設定」の「キー割り当て」で変更することができます。 -(Mac 版をご利用の場合は Ctrl を Command に、Alt を Option に読み替えてください。) +(Mac 版をご利用の場合は Ctrl を Command に、Alt を Option に読み替えてください。) - 上下キー - 上下のテキスト欄に移動 diff --git a/public/qAndA.md b/public/qAndA.md index d7c35dea48..d38987779b 100644 --- a/public/qAndA.md +++ b/public/qAndA.md @@ -14,7 +14,7 @@ Windows/Mac/Linux 搭載の PC に対応しています。 ### GPU 版 -GPU 搭載の Windows PC と、Nvidia 製 GPU 搭載の Linux PC に対応しています。 +DirectML 対応の GPU を搭載した Windows PC と、Nvidia 製 GPU 搭載の Linux PC に対応しています。 ## インストールに関する質問 @@ -47,11 +47,11 @@ GPU 搭載の Windows PC と、Nvidia 製 GPU 搭載の Linux PC に対応して #### Windows 版のインストール先 -`C:\Users\(ユーザー名)\AppData\Local\Programs\VOICEVOX` +`C:\Users\(ユーザー名)\AppData\Local\Programs\VOICEVOX` #### Mac 版のインストール先 -`/Applications/VOICEVOX` もしくは `/Users/(ユーザー名)/Applications/VOICEVOX` +`/Applications/VOICEVOX` もしくは `/Users/(ユーザー名)/Applications/VOICEVOX` ### Q. アップデート方法を教えてください @@ -89,11 +89,11 @@ VOICEVOX Twitter アカウント [@voicevox_pj](https://twitter.com/voicevox_pj) #### Windows 版の設定ファイル -`C:\Users\(ユーザー名)\AppData\Roaming\voicevox\config.json` +`C:\Users\(ユーザー名)\AppData\Roaming\voicevox\config.json` #### Mac 版の設定ファイル -`/Users/(ユーザー名)/Library/Application Support/voicevox/config.json` +`/Users/(ユーザー名)/Library/Application Support/voicevox/config.json` ### Q. エンジンの起動が失敗したというエラーが表示されます @@ -129,15 +129,15 @@ VOICEVOX Twitter アカウント [@voicevox_pj](https://twitter.com/voicevox_pj) ### Q. エラーログはどこで確認できますか? -以下のフォルダに保存されています。 +ヘルプ → お問い合わせ → ログフォルダを開くボタン から確認できます。具体的には以下のフォルダに保存されています。 #### Windows 版 -`C:\Users\(ユーザー名)\AppData\Roaming\voicevox\logs` +`C:\Users\(ユーザー名)\AppData\Roaming\voicevox\logs` #### Mac 版 -`/Users/(ユーザー名)/Library/Application Support/voicevox/logs` +`/Users/(ユーザー名)/Library/Logs/voicevox-cpu` ### Q. Ubuntu 22.04 で動きません @@ -168,7 +168,7 @@ sudo apt install libfuse2 ### Q. 音楽配信サービスなどで公開する場合のクレジット表記はどうすれば良いですか? 概要欄の無い楽曲投稿系プラットフォームの場合、タイトルやアーティストなどの欄にクレジットを記載するか、楽曲中に音声クレジットを挿入してください。 -タイトルの例「タイトル (VOICEVOX:キャラクター名)」「タイトル feat. キャラクター名(VOICEVOX)」など +タイトルの例「タイトル(VOICEVOX:キャラクター名)」「タイトル feat. キャラクター名(VOICEVOX)」など アーティストの例「キャラクター名(VOICEVOX)」「VOICEVOX:キャラクター名」など ### Q. スピーカーでの音声案内など、機械で音声を流したい場合のクレジット記載はどうすれば良いですか? diff --git a/public/res/image14.png b/public/res/image14.png index 6ec096e41c..3c1f901f9e 100644 Binary files a/public/res/image14.png and b/public/res/image14.png differ diff --git a/public/res/image15.png b/public/res/image15.png index 829c9ac5a2..72f574d98a 100644 Binary files a/public/res/image15.png and b/public/res/image15.png differ diff --git a/public/themes/dark.json b/public/themes/dark.json index ac1d71b333..a3bef2b9bf 100644 --- a/public/themes/dark.json +++ b/public/themes/dark.json @@ -5,7 +5,6 @@ "isDark": true, "colors": { "primary": "#86C591", - "primary-light": "#86C591", "display": "#E1E1E1", "display-on-primary": "#1F1F1F", "display-hyperlink": "#58A6FF", diff --git a/public/themes/default.json b/public/themes/default.json index 4dc4277f7a..9d04773fd8 100644 --- a/public/themes/default.json +++ b/public/themes/default.json @@ -5,7 +5,6 @@ "isDark": false, "colors": { "primary": "#A5D4AD", - "primary-light": "#A5D4AD", "display": "#121212", "display-on-primary": "#121212", "display-hyperlink": "#0969DA", diff --git a/public/updateInfos.json b/public/updateInfos.json index dcfcfe976a..a1495513ad 100644 --- a/public/updateInfos.json +++ b/public/updateInfos.json @@ -1,4 +1,11 @@ [ + { + "version": "0.14.8", + "descriptions": [ + "キャラクター「栗田まろん」「あいえるたん」「満別花丸」「琴詠ニア」を追加" + ], + "contributors": [] + }, { "version": "0.14.7", "descriptions": [ diff --git a/src/background.ts b/src/background.ts index 20834bde3a..6b03994913 100644 --- a/src/background.ts +++ b/src/background.ts @@ -11,25 +11,19 @@ import { Menu, shell, nativeTheme, + net, } from "electron"; import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; -import Store, { Schema } from "electron-store"; -import dotenv from "dotenv"; import log from "electron-log"; import dayjs from "dayjs"; import windowStateKeeper from "electron-window-state"; -import zodToJsonSchema from "zod-to-json-schema"; import { hasSupportedGpu } from "./electron/device"; -import { textEditContextMenu } from "./electron/contextMenu"; import { HotkeySetting, ThemeConf, - AcceptTermsStatus, EngineInfo, - ElectronStoreType, SystemError, - electronStoreSchema, defaultHotkeySettings, isMac, defaultToolbarButtonSetting, @@ -52,6 +46,7 @@ import VvppManager, { isVvppFile } from "./background/vvppManager"; import configMigration014 from "./background/configMigration014"; import { failure, success } from "./type/result"; import { ipcMainHandle, ipcMainSend } from "@/electron/ipc"; +import { getStoreWithError } from "@/background/electronStore"; type SingleInstanceLockData = { filePath: string | undefined; @@ -70,11 +65,17 @@ if (isTest) { } else if (isDevelopment) { suffix = "-dev"; } -console.log(`Environment: ${import.meta.env.MODE}, appData: voicevox${suffix}`); +const appName = import.meta.env.VITE_APP_NAME + suffix; +console.log(`Environment: ${import.meta.env.MODE}, appData: ${appName}`); + +// バージョン0.14より前の設定ファイルの保存場所 +const beforeUserDataDir = app.getPath("userData"); // マイグレーション用 + +// appnameをvoicevoxとしてsetする +app.setName(appName); // Electronの設定ファイルの保存場所を変更 -const beforeUserDataDir = app.getPath("userData"); // 設定ファイルのマイグレーション用 -const fixedUserDataDir = path.join(app.getPath("appData"), `voicevox${suffix}`); +const fixedUserDataDir = path.join(app.getPath("appData"), appName); if (!fs.existsSync(fixedUserDataDir)) { fs.mkdirSync(fixedUserDataDir); } @@ -107,24 +108,15 @@ process.on("unhandledRejection", (reason) => { log.error(reason); }); -// .envから設定をprocess.envに読み込み let appDirPath: string; let __static: string; -// NOTE: 開発版では、カレントディレクトリにある .env ファイルを読み込む。 -// 一方、配布パッケージ版では .env ファイルが実行ファイルと同じディレクトリに配置されているが、 -// Linux・macOS ではそのディレクトリはカレントディレクトリとはならないため、.env ファイルの -// パスを明示的に指定する必要がある。Windows の配布パッケージ版でもこの設定で起動できるため、 -// 全 OS で共通の条件分岐とした。 if (isDevelopment) { // __dirnameはdist_electronを指しているので、一つ上のディレクトリに移動する appDirPath = path.resolve(__dirname, ".."); - dotenv.config({ override: true }); __static = path.join(appDirPath, "public"); } else { appDirPath = path.dirname(app.getPath("exe")); - const envPath = path.join(appDirPath, ".env"); - dotenv.config({ path: envPath }); process.chdir(appDirPath); __static = __dirname; } @@ -133,74 +125,10 @@ protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); -// 設定ファイル -const electronStoreJsonSchema = zodToJsonSchema(electronStoreSchema); -if (!("properties" in electronStoreJsonSchema)) { - throw new Error("electronStoreJsonSchema must be object"); -} -let store: Store; -try { - store = new Store({ - schema: electronStoreJsonSchema.properties as Schema, - migrations: { - ">=0.13": (store) => { - // acceptTems -> acceptTerms - const prevIdentifier = "acceptTems"; - const prevValue = store.get(prevIdentifier, undefined) as - | AcceptTermsStatus - | undefined; - if (prevValue) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - store.delete(prevIdentifier as any); - store.set("acceptTerms", prevValue); - } - }, - ">=0.14": (store) => { - // FIXME: できるならEngineManagerからEnginIDを取得したい - if (process.env.DEFAULT_ENGINE_INFOS == undefined) - throw new Error("DEFAULT_ENGINE_INFOS == undefined"); - const engineId = EngineId( - JSON.parse(process.env.DEFAULT_ENGINE_INFOS)[0].uuid - ); - if (engineId == undefined) - throw new Error("DEFAULT_ENGINE_INFOS[0].uuid == undefined"); - const prevDefaultStyleIds = store.get("defaultStyleIds"); - store.set( - "defaultStyleIds", - prevDefaultStyleIds.map((defaultStyle) => ({ - engineId, - speakerUuid: defaultStyle.speakerUuid, - defaultStyleId: defaultStyle.defaultStyleId, - })) - ); - - const outputSamplingRate: number = - // @ts-expect-error 削除されたパラメータ。 - store.get("savingSetting").outputSamplingRate; - store.set(`engineSettings.${engineId}`, { - useGpu: store.get("useGpu"), - outputSamplingRate: - outputSamplingRate === 24000 ? "engineDefault" : outputSamplingRate, - }); - // @ts-expect-error 削除されたパラメータ。 - store.delete("savingSetting.outputSamplingRate"); - // @ts-expect-error 削除されたパラメータ。 - store.delete("useGpu"); - }, - }, - }); -} catch (e) { - log.error(e); - dialog.showErrorBox( - "設定ファイルの読み込みに失敗しました。", - `${app.getPath( - "userData" - )} にある config.json の名前を変えることで解決することがあります(ただし設定がすべてリセットされます)。` - ); - app.exit(1); - throw e; -} +const firstUrl = process.env.VITE_DEV_SERVER_URL ?? "app://./index.html"; +// 設定ファイル +const store = getStoreWithError(); // engine const vvppEngineDir = path.join(app.getPath("userData"), "vvpp-engines"); @@ -208,10 +136,26 @@ if (!fs.existsSync(vvppEngineDir)) { fs.mkdirSync(vvppEngineDir); } +const onEngineProcessError = (engineInfo: EngineInfo, error: Error) => { + const engineId = engineInfo.uuid; + log.error(`ENGINE ${engineId} ERROR: ${error}`); + + // winが作られる前にエラーが発生した場合はwinへの通知を諦める + // FIXME: winが作られた後にエンジンを起動させる + if (win != undefined) { + ipcMainSend(win, "DETECTED_ENGINE_ERROR", { engineId }); + } else { + log.error(`onEngineProcessError: win is undefined`); + } + + dialog.showErrorBox("音声合成エンジンエラー", error.message); +}; + const engineManager = new EngineManager({ store, defaultEngineDir: appDirPath, vvppEngineDir, + onEngineProcessError, }); const vvppManager = new VvppManager({ vvppEngineDir }); @@ -247,10 +191,10 @@ async function installVvppEngine(vvppPath: string) { */ async function installVvppEngineWithWarning({ vvppPath, - restartNeeded, + reloadNeeded, }: { vvppPath: string; - restartNeeded: boolean; + reloadNeeded: boolean; }) { const result = dialog.showMessageBoxSync(win, { type: "warning", @@ -266,21 +210,22 @@ async function installVvppEngineWithWarning({ await installVvppEngine(vvppPath); - if (restartNeeded) { + if (reloadNeeded) { dialog .showMessageBox(win, { type: "info", - title: "再起動が必要です", + title: "再読み込みが必要です", message: - "VVPPファイルを読み込みました。反映には再起動が必要です。今すぐ再起動しますか?", - buttons: ["再起動", "キャンセル"], + "VVPPファイルを読み込みました。反映には再読み込みが必要です。今すぐ再読み込みしますか?", + buttons: ["再読み込み", "キャンセル"], noLink: true, cancelId: 1, }) .then((result) => { if (result.response === 0) { - appState.willRestart = true; - app.quit(); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "reload", + }); } }); } @@ -335,6 +280,18 @@ async function uninstallVvppEngine(engineId: EngineId) { } } +// テーマの読み込み +const themes = readThemeFiles(); +function readThemeFiles() { + const themes: ThemeConf[] = []; + const dir = path.join(__static, "themes"); + for (const file of fs.readdirSync(dir)) { + const theme = JSON.parse(fs.readFileSync(path.join(dir, file)).toString()); + themes.push(theme); + } + return themes; +} + // 使い方テキストの読み込み const howToUseText = fs.readFileSync( path.join(__static, HowToUseTextFileName), @@ -428,8 +385,6 @@ migrateHotkeySettings(); const appState = { willQuit: false, - willRestart: false, - isMultiEngineOffMode: false, }; let filePathOnMac: string | undefined = undefined; // create window @@ -439,6 +394,10 @@ async function createWindow() { defaultHeight: 600, }); + const currentTheme = store.get("currentTheme"); + const backgroundColor = themes.find((value) => value.name == currentTheme) + ?.colors.background; + win = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, @@ -449,6 +408,7 @@ async function createWindow() { trafficLightPosition: { x: 6, y: 4 }, minWidth: 320, show: false, + backgroundColor, webPreferences: { preload: path.join(__dirname, "preload.js"), nodeIntegration: false, @@ -458,7 +418,7 @@ async function createWindow() { icon: path.join(__static, "icon.png"), }); - let projectFilePath: string | undefined = ""; + let projectFilePath = ""; if (isMac) { if (filePathOnMac) { if (filePathOnMac.endsWith(".vvproj")) { @@ -479,21 +439,16 @@ async function createWindow() { } } - const parameter = - "#/?isMultiEngineOffMode=" + - appState.isMultiEngineOffMode + - "&projectFilePath=" + - projectFilePath; - - if (process.env.VITE_DEV_SERVER_URL) { - await win.loadURL(process.env.VITE_DEV_SERVER_URL + parameter); - } else { - protocol.registerFileProtocol("app", (request, callback) => { - const filePath = new URL(request.url).pathname; - callback(path.join(__dirname, filePath)); + // ソフトウェア起動時はプロトコルを app にする + if (process.env.VITE_DEV_SERVER_URL == undefined) { + protocol.handle("app", (request) => { + const filePath = path.join(__dirname, new URL(request.url).pathname); + return net.fetch(`file://${filePath}`); }); - win.loadURL("app://./index.html" + parameter); } + + await loadUrl({ projectFilePath }); + if (isDevelopment && !isTest) win.webContents.openDevTools(); win.on("maximize", () => win.webContents.send("DETECT_MAXIMIZED")); @@ -512,7 +467,9 @@ async function createWindow() { win.on("close", (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE"); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "close", + }); return; } }); @@ -528,8 +485,31 @@ async function createWindow() { mainWindowState.manage(win); } -// UI処理を開始。その他の準備が完了した後に呼ばれる。 +/** + * 画面の読み込みを開始する。 + * @param obj.isMultiEngineOffMode マルチエンジンオフモードにするかどうか。無指定時はfalse扱いになる。 + * @param obj.projectFilePath 初期化時に読み込むプロジェクトファイル。無指定時は何も読み込まない。 + * @returns ロードの完了を待つPromise。 + */ +async function loadUrl(obj: { + isMultiEngineOffMode?: boolean; + projectFilePath?: string; +}) { + const fragment = + "#/home" + + `?isMultiEngineOffMode=${obj?.isMultiEngineOffMode ?? false}` + + `&projectFilePath=${obj?.projectFilePath ?? ""}`; + return win.loadURL(`${firstUrl}${fragment}`); +} + +// 開始。その他の準備が完了した後に呼ばれる。 async function start() { + await launchEngines(); + await createWindow(); +} + +// エンジンの準備と起動 +async function launchEngines() { // エンジンの追加と削除を反映させるためEngineInfoとAltPortInfoを再生成する。 engineManager.initializeEngineInfosAndAltPortInfo(); const engineInfos = engineManager.fetchEngineInfos(); @@ -542,8 +522,51 @@ async function start() { } store.set("engineSettings", engineSettings); - await engineManager.runEngineAll(win); - await createWindow(); + await engineManager.runEngineAll(); +} + +/** + * エンジンの停止とエンジン終了後処理を行う。 + * 全処理が完了済みの場合 alreadyCompleted を返す。 + * そうでない場合は Promise を返す。 + */ +function cleanupEngines(): Promise | "alreadyCompleted" { + const killingProcessPromises = engineManager.killEngineAll(); + const numLivingEngineProcess = Object.entries(killingProcessPromises).length; + + // 前処理が完了している場合 + if (numLivingEngineProcess === 0 && !vvppManager.hasMarkedEngineDirs()) { + return "alreadyCompleted"; + } + + let numEngineProcessKilled = 0; + + // 非同期的にすべてのエンジンプロセスをキル + const waitingKilledPromises: Promise[] = Object.entries( + killingProcessPromises + ).map(([engineId, promise]) => { + return promise + .catch((error) => { + // TODO: 各エンジンプロセスキルの失敗をUIに通知する + log.error(`ENGINE ${engineId}: Error during killing process: ${error}`); + // エディタを終了するため、エラーが起きてもエンジンプロセスをキルできたとみなす + }) + .finally(() => { + numEngineProcessKilled++; + log.info( + `ENGINE ${engineId}: Process killed. ${numEngineProcessKilled} / ${numLivingEngineProcess} processes killed` + ); + }); + }); + + // すべてのエンジンプロセスキル処理が完了するまで待機 + return Promise.all(waitingKilledPromises).then(() => { + // エンジン終了後の処理を実行 + log.info( + "All ENGINE process kill operations done. Running post engine kill process" + ); + return vvppManager.handleMarkedEngineDirs(); + }); } const menuTemplateForMac: Electron.MenuItemConstructorOptions[] = [ @@ -736,10 +759,6 @@ ipcMainHandle("SHOW_IMPORT_FILE_DIALOG", (_, { title }) => { })?.[0]; }); -ipcMainHandle("OPEN_TEXT_EDIT_CONTEXT_MENU", () => { - textEditContextMenu.popup({ window: win }); -}); - ipcMainHandle("IS_AVAILABLE_GPU_MODE", () => { return hasSupportedGpu(process.platform); }); @@ -773,6 +792,10 @@ ipcMainHandle("LOG_INFO", (_, ...params) => { log.info(...params); }); +ipcMainHandle("OPEN_LOG_DIRECTORY", () => { + shell.openPath(app.getPath("logs")); +}); + ipcMainHandle("ENGINE_INFOS", () => { // エンジン情報を設定ファイルに保存しないためにstoreは使わない return engineManager.fetchEngineInfos(); @@ -783,7 +806,7 @@ ipcMainHandle("ENGINE_INFOS", () => { * エンジンの起動が開始したらresolve、起動が失敗したらreject。 */ ipcMainHandle("RESTART_ENGINE", async (_, { engineId }) => { - await engineManager.restartEngine(engineId, win); + await engineManager.restartEngine(engineId); }); ipcMainHandle("OPEN_ENGINE_DIRECTORY", async (_, { engineId }) => { @@ -809,14 +832,10 @@ ipcMainHandle("THEME", (_, { newData }) => { store.set("currentTheme", newData); return; } - const dir = path.join(__static, "themes"); - const themes: ThemeConf[] = []; - const files = fs.readdirSync(dir); - files.forEach((file) => { - const theme = JSON.parse(fs.readFileSync(path.join(dir, file)).toString()); - themes.push(theme); - }); - return { currentTheme: store.get("currentTheme"), availableThemes: themes }; + return { + currentTheme: store.get("currentTheme"), + availableThemes: themes, + }; }); ipcMainHandle("ON_VUEX_READY", () => { @@ -871,10 +890,25 @@ ipcMainHandle("VALIDATE_ENGINE_DIR", (_, { engineDir }) => { return engineManager.validateEngineDir(engineDir); }); -ipcMainHandle("RESTART_APP", async (_, { isMultiEngineOffMode }) => { - appState.willRestart = true; - appState.isMultiEngineOffMode = isMultiEngineOffMode; - win.close(); +ipcMainHandle("RELOAD_APP", async (_, { isMultiEngineOffMode }) => { + win.hide(); // FIXME: ダミーページ表示のほうが良い + + // FIXME: 同じようなURLだとスーパーリロードされないことがあるので一度ダミーページを読み込む + await win.loadURL(firstUrl + "dummypage"); + + log.info("Checking ENGINE status before reload app"); + const engineCleanupResult = cleanupEngines(); + + // エンジンの停止とエンジン終了後処理の待機 + if (engineCleanupResult != "alreadyCompleted") { + await engineCleanupResult; + } + log.info("Post engine kill process done. Now reloading app"); + + await launchEngines(); + + await loadUrl({ isMultiEngineOffMode: !!isMultiEngineOffMode }); + win.show(); }); ipcMainHandle("WRITE_FILE", (_, { filePath, buffer }) => { @@ -920,31 +954,16 @@ app.on("window-all-closed", () => { app.on("before-quit", async (event) => { if (!appState.willQuit) { event.preventDefault(); - ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE"); + ipcMainSend(win, "CHECK_EDITED_AND_NOT_SAVE", { closeOrReload: "close" }); return; } log.info("Checking ENGINE status before app quit"); + const engineCleanupResult = cleanupEngines(); - const killingProcessPromises = engineManager.killEngineAll(); - const numLivingEngineProcess = Object.entries(killingProcessPromises).length; - - // すべてのエンジンプロセスが停止している - if (numLivingEngineProcess === 0 && !vvppManager.hasMarkedEngineDirs()) { - if (appState.willRestart) { - // 再起動フラグが立っている場合はフラグを戻して再起動する - log.info( - "Post engine kill process done. Now restarting app because of willRestart flag" - ); - event.preventDefault(); - - appState.willRestart = false; - appState.willQuit = false; - - start(); - } else { - log.info("Post engine kill process done. Now quit app"); - } + // エンジンの停止とエンジン終了後処理が完了している + if (engineCleanupResult == "alreadyCompleted") { + log.info("Post engine kill process done. Now quit app"); return; } @@ -954,34 +973,7 @@ app.on("before-quit", async (event) => { log.info("Interrupt app quit to kill ENGINE processes"); event.preventDefault(); - let numEngineProcessKilled = 0; - - // 非同期的にすべてのエンジンプロセスをキル - const waitingKilledPromises: Array> = Object.entries( - killingProcessPromises - ).map(([engineId, promise]) => { - return promise - .catch((error) => { - // TODO: 各エンジンプロセスキルの失敗をUIに通知する - log.error(`ENGINE ${engineId}: Error during killing process: ${error}`); - // エディタを終了するため、エラーが起きてもエンジンプロセスをキルできたとみなす - }) - .finally(() => { - numEngineProcessKilled++; - log.info( - `ENGINE ${engineId}: Process killed. ${numEngineProcessKilled} / ${numLivingEngineProcess} processes killed` - ); - }); - }); - - // すべてのエンジンプロセスキル処理が完了するまで待機 - await Promise.all(waitingKilledPromises); - - // エンジン終了後の処理を実行 - log.info( - "All ENGINE process kill operations done. Running post engine kill process" - ); - await vvppManager.handleMarkedEngineDirs(); + await engineCleanupResult; // アプリケーションの終了を再試行する log.info("Attempting to quit app again"); @@ -1039,7 +1031,7 @@ app.on("ready", async () => { if (checkMultiEngineEnabled()) { await installVvppEngineWithWarning({ vvppPath: filePath, - restartNeeded: false, + reloadNeeded: false, }); } } @@ -1058,7 +1050,7 @@ app.on("second-instance", async (event, argv, workDir, rawData) => { if (checkMultiEngineEnabled()) { await installVvppEngineWithWarning({ vvppPath: data.filePath, - restartNeeded: true, + reloadNeeded: true, }); } } else if (data.filePath.endsWith(".vvproj")) { diff --git a/src/background/electronStore.ts b/src/background/electronStore.ts new file mode 100644 index 0000000000..f62f6e71ca --- /dev/null +++ b/src/background/electronStore.ts @@ -0,0 +1,100 @@ +import { app, dialog, shell } from "electron"; +import Store, { Schema } from "electron-store"; +import log from "electron-log"; +import zodToJsonSchema from "zod-to-json-schema"; +import { + AcceptTermsStatus, + ElectronStoreType, + EngineId, + electronStoreSchema, +} from "@/type/preload"; + +function getStore() { + const electronStoreJsonSchema = zodToJsonSchema(electronStoreSchema); + if (!("properties" in electronStoreJsonSchema)) { + throw new Error("electronStoreJsonSchema must be object"); + } + return new Store({ + schema: electronStoreJsonSchema.properties as Schema, + migrations: { + ">=0.13": (store) => { + // acceptTems -> acceptTerms + const prevIdentifier = "acceptTems"; + const prevValue = store.get(prevIdentifier, undefined) as + | AcceptTermsStatus + | undefined; + if (prevValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store.delete(prevIdentifier as any); + store.set("acceptTerms", prevValue); + } + }, + ">=0.14": (store) => { + // FIXME: できるならEngineManagerからEnginIDを取得したい + if (import.meta.env.VITE_DEFAULT_ENGINE_INFOS == undefined) + throw new Error("VITE_DEFAULT_ENGINE_INFOS == undefined"); + const engineId = EngineId( + JSON.parse(import.meta.env.VITE_DEFAULT_ENGINE_INFOS)[0].uuid + ); + if (engineId == undefined) + throw new Error("VITE_DEFAULT_ENGINE_INFOS[0].uuid == undefined"); + const prevDefaultStyleIds = store.get("defaultStyleIds"); + store.set( + "defaultStyleIds", + prevDefaultStyleIds.map((defaultStyle) => ({ + engineId, + speakerUuid: defaultStyle.speakerUuid, + defaultStyleId: defaultStyle.defaultStyleId, + })) + ); + + const outputSamplingRate: number = + // @ts-expect-error 削除されたパラメータ。 + store.get("savingSetting").outputSamplingRate; + store.set(`engineSettings.${engineId}`, { + useGpu: store.get("useGpu"), + outputSamplingRate: + outputSamplingRate === 24000 ? "engineDefault" : outputSamplingRate, + }); + // @ts-expect-error 削除されたパラメータ。 + store.delete("savingSetting.outputSamplingRate"); + // @ts-expect-error 削除されたパラメータ。 + store.delete("useGpu"); + }, + }, + }); +} + +export function getStoreWithError(): Store { + try { + return getStore(); + } catch (e) { + log.error(e); + app.whenReady().then(() => { + dialog + .showMessageBox({ + type: "error", + title: "設定ファイルの読み込みエラー", + message: `設定ファイルの読み込みに失敗しました。${app.getPath( + "userData" + )} にある config.json の名前を変えることで解決することがあります(ただし設定がすべてリセットされます)。設定ファイルがあるフォルダを開きますか?`, + buttons: ["いいえ", "はい"], + noLink: true, + cancelId: 0, + }) + .then(async ({ response }) => { + if (response === 1) { + await shell.openPath(app.getPath("userData")); + // 直後にexitするとフォルダが開かないため + await new Promise((resolve) => { + setTimeout(resolve, 500); + }); + } + }) + .finally(() => { + app.exit(1); + }); + }); + throw e; + } +} diff --git a/src/background/engineManager.ts b/src/background/engineManager.ts index 8a52cdcf04..d35f5e4eb7 100644 --- a/src/background/engineManager.ts +++ b/src/background/engineManager.ts @@ -5,7 +5,7 @@ import treeKill from "tree-kill"; import Store from "electron-store"; import shlex from "shlex"; -import { app, BrowserWindow, dialog } from "electron"; +import { app, dialog } from "electron"; // FIXME: ここでelectronをimportするのは良くない import log from "electron-log"; import { z } from "zod"; @@ -16,7 +16,6 @@ import { isAssignablePort, url2HostInfo, } from "./portManager"; -import { ipcMainSend } from "@/electron/ipc"; import { EngineInfo, @@ -39,7 +38,8 @@ type EngineProcessContainer = { */ function createDefaultEngineInfos(defaultEngineDir: string): EngineInfo[] { // TODO: envから直接ではなく、envに書いたengine_manifest.jsonから情報を得るようにする - const defaultEngineInfosEnv = process.env.DEFAULT_ENGINE_INFOS ?? "[]"; + const defaultEngineInfosEnv = + import.meta.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]"; const envSchema = z .object({ @@ -70,6 +70,7 @@ export class EngineManager { store: Store; defaultEngineDir: string; vvppEngineDir: string; + onEngineProcessError: (engineInfo: EngineInfo, error: Error) => void; defaultEngineInfos: EngineInfo[] = []; additionalEngineInfos: EngineInfo[] = []; @@ -81,14 +82,17 @@ export class EngineManager { store, defaultEngineDir, vvppEngineDir, + onEngineProcessError, }: { store: Store; defaultEngineDir: string; vvppEngineDir: string; + onEngineProcessError: (engineInfo: EngineInfo, error: Error) => void; }) { this.store = store; // FIXME: エンジンマネージャーがelectron-storeを持たなくても良いようにする this.defaultEngineDir = defaultEngineDir; this.vvppEngineDir = vvppEngineDir; + this.onEngineProcessError = onEngineProcessError; this.initializeEngineInfosAndAltPortInfo(); this.engineProcessContainers = {}; @@ -207,23 +211,21 @@ export class EngineManager { /** * 全てのエンジンを起動する。 - * FIXME: winを受け取らなくても良いようにする */ - async runEngineAll(win: BrowserWindow) { + async runEngineAll() { const engineInfos = this.fetchEngineInfos(); log.info(`Starting ${engineInfos.length} engine/s...`); for (const engineInfo of engineInfos) { log.info(`ENGINE ${engineInfo.uuid}: Start launching`); - await this.runEngine(engineInfo.uuid, win); + await this.runEngine(engineInfo.uuid); } } /** * エンジンを起動する。 - * FIXME: winを受け取らなくても良いようにする */ - async runEngine(engineId: EngineId, win: BrowserWindow) { + async runEngine(engineId: EngineId) { const engineInfos = this.fetchEngineInfos(); const engineInfo = engineInfos.find( (engineInfo) => engineInfo.uuid === engineId @@ -330,6 +332,7 @@ export class EngineManager { const engineProcess = spawn(enginePath, args, { cwd: path.dirname(enginePath), + env: { ...process.env, VV_OUTPUT_LOG_UTF8: "1" }, }); engineProcessContainer.engineProcess = engineProcess; @@ -341,14 +344,16 @@ export class EngineManager { log.error(`ENGINE ${engineId} STDERR: ${data.toString("utf-8")}`); }); + // onEngineProcessErrorを一度だけ呼ぶためのフラグ。"error"と"close"がどちらも呼ばれることがある。 + // 詳細 https://github.com/VOICEVOX/voicevox/pull/1053/files#r1051436950 + let errorNotified = false; + engineProcess.on("error", (err) => { log.error(`ENGINE ${engineId} ERROR: ${err}`); - // FIXME: "close"イベントでダイアログが表示されて2回表示されてしまうのを防ぐ - // 詳細 https://github.com/VOICEVOX/voicevox/pull/1053/files#r1051436950 - dialog.showErrorBox( - "音声合成エンジンエラー", - `音声合成エンジンが異常終了しました。${err}` - ); + if (!errorNotified) { + errorNotified = true; + this.onEngineProcessError(engineInfo, err); + } }); engineProcess.on("close", (code, signal) => { @@ -358,12 +363,14 @@ export class EngineManager { log.info(`ENGINE ${engineId}: Process exited with code ${code}`); if (!engineProcessContainer.willQuitEngine) { - ipcMainSend(win, "DETECTED_ENGINE_ERROR", { engineId }); - const dialogMessage = + const errorMessage = engineInfos.length === 1 ? "音声合成エンジンが異常終了しました。エンジンを再起動してください。" : `${engineInfo.name}が異常終了しました。エンジンを再起動してください。`; - dialog.showErrorBox("音声合成エンジンエラー", dialogMessage); + if (!errorNotified) { + errorNotified = true; + this.onEngineProcessError(engineInfo, new Error(errorMessage)); + } } }); } @@ -449,9 +456,8 @@ export class EngineManager { /** * エンジンを再起動する。 - * FIXME: winを受け取らなくても良いようにする */ - async restartEngine(engineId: EngineId, win: BrowserWindow) { + async restartEngine(engineId: EngineId) { // FIXME: killEngine関数を使い回すようにする await new Promise((resolve, reject) => { const engineProcessContainer: EngineProcessContainer | undefined = @@ -472,7 +478,7 @@ export class EngineManager { `ENGINE ${engineId}: Process is not started yet or already killed. Starting process...` ); - this.runEngine(engineId, win); + this.runEngine(engineId); resolve(); return; } @@ -485,12 +491,14 @@ export class EngineManager { const restartEngineOnProcessClosedCallback = () => { log.info(`ENGINE ${engineId}: Process killed. Restarting process...`); - this.runEngine(engineId, win); + this.runEngine(engineId); resolve(); }; if (engineProcess === undefined) throw Error("engineProcess === undefined"); + if (engineProcess.pid === undefined) + throw Error("engineProcess.pid === undefined"); engineProcess.once("close", restartEngineOnProcessClosedCallback); diff --git a/src/background/vvppManager.ts b/src/background/vvppManager.ts index bfc1725a2c..170a3eb1b4 100644 --- a/src/background/vvppManager.ts +++ b/src/background/vvppManager.ts @@ -6,6 +6,7 @@ import { moveFile } from "move-file"; import { app, dialog } from "electron"; import MultiStream from "multistream"; import glob, { glob as callbackGlob } from "glob"; +import AsyncLock from "async-lock"; import { EngineId, EngineInfo, @@ -41,18 +42,19 @@ export const isVvppFile = (filePath: string) => { ); }; +const lockKey = "lock-key-for-vvpp-manager"; + // # 軽い概要 // // フォルダ名:"エンジン名+UUID" // エンジン名にフォルダ名に使用できない文字が含まれている場合は"_"に置換する。連続する"_"は1つにする。 // 拡張子は".vvpp"または".vvppp"。".vvppp"は分割されているファイルであることを示す。 // engine.0.vvppp、engine.1.vvppp、engine.2.vvppp、...というように分割されている。 -// UUIDはengine_manifest.jsonのuuidを使用する +// UUIDはengine_manifest.jsonのuuidを使用し、同一エンジンの判定にはこれを使用する。 // // 追加: // * エンジンを仮フォルダ(vvpp-engines/.tmp/現在の時刻)に展開する // * エンジンが既に存在しているか確認する -// 最後のUUIDで比較する // - 存在していた場合:上書き処理を行う // - 存在していなかった場合:仮フォルダをvvpp-engines/エンジン名+UUIDに移動する // @@ -72,6 +74,8 @@ export class VvppManager { willDeleteEngineIds: Set; willReplaceEngineDirs: Array<{ from: string; to: string }>; + private lock = new AsyncLock(); + constructor({ vvppEngineDir }: { vvppEngineDir: string }) { this.vvppEngineDir = vvppEngineDir; this.willDeleteEngineIds = new Set(); @@ -115,7 +119,7 @@ export class VvppManager { return true; } - async extractVvpp( + private async extractVvpp( vvppLikeFilePath: string ): Promise<{ outputDir: string; manifest: MinimumEngineManifest }> { const nonce = new Date().getTime().toString(); @@ -192,7 +196,7 @@ export class VvppManager { const args = ["x", "-o" + outputDir, archiveFile, "-t" + format]; - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME as string; + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; if (!sevenZipPath) { throw new Error("7z path is not defined"); } @@ -257,7 +261,13 @@ export class VvppManager { } } + /** + * 追加 + */ async install(vvppPath: string) { + await this.lock.acquire(lockKey, () => this._install(vvppPath)); + } + private async _install(vvppPath: string) { const { outputDir, manifest } = await this.extractVvpp(vvppPath); const dirName = this.toValidDirName(manifest); const engineDirectory = path.join(this.vvppEngineDir, dirName); @@ -280,6 +290,9 @@ export class VvppManager { } async handleMarkedEngineDirs() { + await this.lock.acquire(lockKey, () => this._handleMarkedEngineDirs()); + } + private async _handleMarkedEngineDirs() { await Promise.all( [...this.willDeleteEngineIds].map(async (engineId) => { let deletingEngineDir: string | undefined = undefined; diff --git a/src/browser/fileImpl.ts b/src/browser/fileImpl.ts index 3f8efdc527..608891a452 100644 --- a/src/browser/fileImpl.ts +++ b/src/browser/fileImpl.ts @@ -113,7 +113,7 @@ export const writeFileImpl: typeof window[typeof SandboxKey]["writeFile"] = async (obj: { filePath: string; buffer: ArrayBuffer }) => { const path = obj.filePath; - if (path.indexOf(sep) === -1) { + if (!path.includes(sep)) { const aTag = document.createElement("a"); const blob = URL.createObjectURL(new Blob([obj.buffer])); aTag.href = blob; @@ -151,7 +151,7 @@ export const checkFileExistsImpl: typeof window[typeof SandboxKey]["checkFileExi async (file) => { const path = file; - if (path.indexOf(sep) === -1) { + if (!path.includes(sep)) { return Promise.resolve(false); } diff --git a/src/browser/sandbox.ts b/src/browser/sandbox.ts index 2353dc7656..d2688d7516 100644 --- a/src/browser/sandbox.ts +++ b/src/browser/sandbox.ts @@ -168,10 +168,6 @@ export const api: Sandbox = { "ブラウザ版では現在ファイルの読み込みをサポートしていません" ); }, - openTextEditContextMenu() { - // NOTE: ブラウザ版では不要 - return Promise.resolve(); - }, isAvailableGPUMode() { // TODO: WebAssembly版をサポートする時に実装する // FIXME: canvasでWebGLから調べたり、WebGPUがサポートされているかを調べたりで判断は出来そう @@ -213,6 +209,9 @@ export const api: Sandbox = { console.info(...params); return; }, + openLogDirectory() { + throw new Error(`Not supported on Browser version: openLogDirectory`); + }, /* eslint-enable no-console */ engineInfos() { return Promise.resolve([defaultEngine]); @@ -262,6 +261,8 @@ export const api: Sandbox = { await this.setSetting("currentTheme", newData); return; } + // NOTE: Electron版では起動時にテーマ情報が必要なので、 + // この実装とは違って起動時に読み込んだキャッシュを返すだけになっている。 return Promise.all( // FIXME: themeファイルのいい感じのパスの設定 ["/themes/default.json", "/themes/dark.json"].map((url) => @@ -309,7 +310,7 @@ export const api: Sandbox = { validateEngineDir(/* engineDir: string */) { throw new Error(`Not supported on Browser version: validateEngineDir`); }, - restartApp(/* obj: { isMultiEngineOffMode: boolean } */) { - throw new Error(`Not supported on Browser version: restartApp`); + reloadApp(/* obj: { isMultiEngineOffMode: boolean } */) { + throw new Error(`Not supported on Browser version: reloadApp`); }, }; diff --git a/src/browser/storeImpl.ts b/src/browser/storeImpl.ts index 08fb4aac49..436b79c633 100644 --- a/src/browser/storeImpl.ts +++ b/src/browser/storeImpl.ts @@ -7,7 +7,7 @@ import { engineSettingSchema, } from "@/type/preload"; -const dbName = "voicevox-web"; +const dbName = `${import.meta.env.VITE_APP_NAME}-web`; const settingStoreKey = "electronStore"; // FIXME: DBのschemaを変更したら、dbVersionを上げる // TODO: 気づけるようにしたい diff --git a/src/components/AccentPhrase.vue b/src/components/AccentPhrase.vue new file mode 100644 index 0000000000..822528d2d7 --- /dev/null +++ b/src/components/AccentPhrase.vue @@ -0,0 +1,458 @@ + + + + + diff --git a/src/components/AcceptTermsDialog.vue b/src/components/AcceptTermsDialog.vue index c7e96baa6a..6ad2047f28 100644 --- a/src/components/AcceptTermsDialog.vue +++ b/src/components/AcceptTermsDialog.vue @@ -86,7 +86,9 @@ const handler = (acceptTerms: boolean) => { store.dispatch("SET_ACCEPT_TERMS", { acceptTerms: acceptTerms ? "Accepted" : "Rejected", }); - !acceptTerms ? store.dispatch("CHECK_EDITED_AND_NOT_SAVE") : undefined; + !acceptTerms + ? store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { closeOrReload: "close" }) + : undefined; modelValueComputed.value = false; }; diff --git a/src/components/AudioAccent.vue b/src/components/AudioAccent.vue index a01f1eb365..ebc9208f3c 100644 --- a/src/components/AudioAccent.vue +++ b/src/components/AudioAccent.vue @@ -12,7 +12,7 @@ v-if="accentPhrase.moras.length > 1" snap dense - color="primary-light" + color="primary" track-size="2px" :min="previewAccentSlider.qSliderProps.min.value" :max="previewAccentSlider.qSliderProps.max.value" @@ -106,7 +106,9 @@ const accentLine = computed(() => { }); // クリックでアクセント句が選択されないように、@click.stopに渡す -const stopPropagation = undefined; +const stopPropagation = () => { + // fn is not a function エラーを回避するために何もしない関数を渡す +}; diff --git a/src/components/AudioDetail.vue b/src/components/AudioDetail.vue index f1a0cbab90..d5f12150ab 100644 --- a/src/components/AudioDetail.vue +++ b/src/components/AudioDetail.vue @@ -28,25 +28,23 @@
- + +
@@ -76,231 +74,44 @@ OptionAlt + ホイール: 一括調整 -
- - - -
- - -
- - -
+ ref="accentPhraseComponents" + :audio-key="activeAudioKey" + :accent-phrase="accentPhrase" + :index="accentPhraseIndex" + :is-last=" + accentPhrases !== undefined && + accentPhrases.length - 1 === accentPhraseIndex + " + :is-active="accentPhraseIndex === activePoint" + :selected-detail="selectedDetail" + :shift-key-flag="shiftKeyFlag" + :alt-key-flag="altKeyFlag" + @click="setPlayAndStartPoint" + /> diff --git a/src/components/AudioInfo.vue b/src/components/AudioInfo.vue index 8eb78391a1..4705401ecd 100644 --- a/src/components/AudioInfo.vue +++ b/src/components/AudioInfo.vue @@ -18,7 +18,7 @@ @@ -34,7 +34,7 @@ @@ -52,7 +52,7 @@ v-model="presetSelectModel" :options="selectablePresetList" class="col overflow-hidden" - color="primary-light" + color="primary" text-color="display-on-primary" outlined dense @@ -102,7 +102,7 @@ autofocus hide-selected label="タイトル" - color="primary-light" + color="primary" use-input input-debounce="0" :model-value="presetName" @@ -195,7 +195,7 @@ +
(() => [ step: () => 0.01, scrollStep: () => 0.1, scrollMinStep: () => 0.01, + onChange: (speedScale: number) => + store.dispatch("COMMAND_SET_AUDIO_SPEED_SCALE", { + audioKey: props.activeAudioKey, + speedScale, + }), }), action: "COMMAND_SET_AUDIO_SPEED_SCALE", key: "speedScale", @@ -381,6 +388,11 @@ const parameters = computed(() => [ min: () => -0.15, step: () => 0.01, scrollStep: () => 0.01, + onChange: (pitchScale: number) => + store.dispatch("COMMAND_SET_AUDIO_PITCH_SCALE", { + audioKey: props.activeAudioKey, + pitchScale, + }), }), action: "COMMAND_SET_AUDIO_PITCH_SCALE", key: "pitchScale", @@ -397,6 +409,11 @@ const parameters = computed(() => [ step: () => 0.01, scrollStep: () => 0.1, scrollMinStep: () => 0.01, + onChange: (intonationScale: number) => + store.dispatch("COMMAND_SET_AUDIO_INTONATION_SCALE", { + audioKey: props.activeAudioKey, + intonationScale, + }), }), action: "COMMAND_SET_AUDIO_INTONATION_SCALE", key: "intonationScale", @@ -412,6 +429,11 @@ const parameters = computed(() => [ step: () => 0.01, scrollStep: () => 0.1, scrollMinStep: () => 0.01, + onChange: (volumeScale: number) => + store.dispatch("COMMAND_SET_AUDIO_VOLUME_SCALE", { + audioKey: props.activeAudioKey, + volumeScale, + }), }), action: "COMMAND_SET_AUDIO_VOLUME_SCALE", key: "volumeScale", @@ -426,6 +448,11 @@ const parameters = computed(() => [ step: () => 0.01, scrollStep: () => 0.1, scrollMinStep: () => 0.01, + onChange: (prePhonemeLength: number) => + store.dispatch("COMMAND_SET_AUDIO_PRE_PHONEME_LENGTH", { + audioKey: props.activeAudioKey, + prePhonemeLength, + }), }), action: "COMMAND_SET_AUDIO_PRE_PHONEME_LENGTH", key: "prePhonemeLength", @@ -440,6 +467,11 @@ const parameters = computed(() => [ step: () => 0.01, scrollStep: () => 0.1, scrollMinStep: () => 0.01, + onChange: (postPhonemeLength: number) => + store.dispatch("COMMAND_SET_AUDIO_POST_PHONEME_LENGTH", { + audioKey: props.activeAudioKey, + postPhonemeLength, + }), }), action: "COMMAND_SET_AUDIO_POST_PHONEME_LENGTH", key: "postPhonemeLength", @@ -588,7 +620,7 @@ const setMorphingRate = (rate: number) => { if (info == undefined) { throw new Error("audioItem.value.morphingInfo == undefined"); } - store.dispatch("COMMAND_SET_MORPHING_INFO", { + return store.dispatch("COMMAND_SET_MORPHING_INFO", { audioKey: props.activeAudioKey, morphingInfo: { rate, @@ -666,12 +698,11 @@ type PresetSelectModelType = { }; // プリセットの変更 -const changePreset = (presetKey: PresetKey | undefined): void => { +const changePreset = (presetKey: PresetKey | undefined) => store.dispatch("COMMAND_SET_AUDIO_PRESET", { audioKey: props.activeAudioKey, presetKey, }); -}; const presetList = computed<{ label: string; key: PresetKey }[]>(() => presetKeys.value diff --git a/src/components/AudioParameter.vue b/src/components/AudioParameter.vue index bf49501e54..59a455cd7e 100644 --- a/src/components/AudioParameter.vue +++ b/src/components/AudioParameter.vue @@ -8,11 +8,11 @@ !disable && (valueLabel.visible || previewSlider.state.isPanning.value) " class="value-label" - color="primary-light" + color="primary" text-color="display-on-primary" > {{ - previewSlider.state.currentValue.value + previewSlider.state.currentValue.value != undefined ? previewSlider.state.currentValue.value.toFixed(precisionComputed) : undefined }} @@ -21,7 +21,7 @@ vertical reverse snap - color="primary-light" + color="primary" track-size="2.5px" :style="clipPathComputed" :min="previewSlider.qSliderProps.min.value" @@ -46,7 +46,6 @@ import { MoraDataType } from "@/type/preload"; const props = withDefaults( defineProps<{ value: number; - accentPhraseIndex: number; moraIndex: number; uiLocked: boolean; min?: number; @@ -72,23 +71,20 @@ const emit = defineEmits<{ ( e: "changeValue", - accentPhraseIndex: number, moraIndex: number, newValue: number, type: MoraDataType - ): void; + ): Promise; ( e: "mouseOver", isOver: boolean, type: MoraDataType, - accentPhraseIndex: number, moraIndex: number ): void; }>(); -const changeValue = (newValue: number, type: MoraDataType = props.type) => { - emit("changeValue", props.accentPhraseIndex, props.moraIndex, newValue, type); -}; +const changeValue = (newValue: number, type: MoraDataType = props.type) => + emit("changeValue", props.moraIndex, newValue, type); const previewSlider = previewSliderHelper({ modelValue: () => props.value, @@ -121,13 +117,7 @@ const clipPathComputed = computed((): string => { const handleMouseHover = (isOver: boolean) => { valueLabel.visible = isOver; if (props.type == "consonant" || props.type == "vowel") { - emit( - "mouseOver", - isOver, - props.type, - props.accentPhraseIndex, - props.moraIndex - ); + emit("mouseOver", isOver, props.type, props.moraIndex); } }; @@ -140,7 +130,9 @@ const precisionComputed = computed(() => { }); // クリックでアクセント句が選択されないように@click.stopに渡す -const stopPropagation = undefined; +const stopPropagation = () => { + // fn is not a function エラーを回避するために何もしない関数を渡す +}; diff --git a/src/components/SettingDialog.vue b/src/components/SettingDialog.vue index ac5271e8f7..c421b38c3f 100644 --- a/src/components/SettingDialog.vue +++ b/src/components/SettingDialog.vue @@ -299,7 +299,7 @@ v-if="isDefaultConfirmedTips && hasResetConfirmedTips" name="check" size="sm" - color="primary-light" + color="primary" style="margin-right: 8px" > @@ -857,6 +857,63 @@ > + +
複数選択
+
+ + + 複数のテキスト欄を選択できるようにします。 + + +
+ + + +
+ +
調整結果の保持
+
+ + ONの場合、テキスト変更時、同じ読みのアクセント区間内の調整結果を保持します。 + +
+ + + +
@@ -892,10 +949,10 @@