diff --git a/.npmrc b/.npmrc index d9ca8860b0..335b2f1803 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ engine-strict=true save-exact=true +@jsr:registry=https://npm.jsr.io diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..5a974e6fb4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,363 @@ +# 貢献者ガイドライン + +## 始めに + +まず初めに、VOICEVOXプロジェクトに関心を寄せて頂きありがとうございます。 +私たちは、あなたが積極的に参加してくれることを歓迎します。 + +実際に参加しようとすると、どんなコミュニティにもルールが存在し、そこを理解しないとハードルを高く感じてしまうことがあります。 + +このガイドラインは、その部分を出来るだけ分かりやすく文章として残し、コミュニティへ参画しやすい環境を提供するために執筆されました。なお、新たな貢献者が参入しやすいよう細かく解説しているため、慣れている方には不要な説明も含まれます。プロジェクトは意志ある貢献者を歓迎しておりますので、ドキュメントを読んで参画してみてください。 + +## 担当 + +| 役割 | 担当 | +| ------------------ | ---------------------------- | +| プロダクトオーナー | @Hiroshiba | +| メンテナー | @Hiroshiba、@y-chan、@qryxip | + +## 参加の心得 + +VOICEVOXプロジェクトは、いわゆる集団開発型のオープンソースソフトウェアにあたります。参加を望む方は、下記のことに注意して参加する必要があります。 + +- [VOICEVOXの目標](docs/ミッション・バリュー・ビジョン.md)に照らし合わせて提案を行うと会話がスムーズです。 + +- 実施する中身については、コミュニティ内で会話をしながら合意を取っていく必要があります。また、プロジェクト方針により採用が拒絶されるケースがあります。 + +- 集団開発では、会話をしながら物を作っていくことが1つの醍醐味でもあります。一人で作品を作る時に比べて丁寧なコミュニケーションが必要です。会話相手に対して常に敬意を払ってください。 + +- プロジェクトへの参画に当たっては、年齢、国籍、境遇、性別などは関係ありません。これらの差別を行うことをプロジェクトは容認しません。 + +- プロジェクトは著作者や著作物を尊重します。常に他者の権利やライセンスを順守するように意識しています。プロジェクトへの貢献にあたっては、盗用したプログラムの提出は行わないでください。 + +- コントリビューターとして提供したプログラムは、プロジェクトが定義するライセンスで取り扱われる事に注意してください。 + +- プライバシーに関わる実装や、コンピュータに危害を与える可能性がある実装に関しては慎重な議論が必要です。実装を先におこなうのではなく、必ず合意形成をしてください。 + +## 貢献の仕方 + +このドキュメントでは、主にプログラムの改良を手伝ってくださる方に向けた、「参加の仕方」をガイドします。 + +VOICEVOXには、下記のような貢献の仕方があります。 + +- ユーザとして使う +- 記事や動画を公開して広める +- プログラムの改良を手伝う +- ドキュメントなどを書く + +プログラムは3部構成に分かれているので、該当する部分に該当するプロジェクトに参加しましょう。 + +| 種類 | ページ | 役割 | +| --------------- | ------------------------------------------------------------ | ---------------------------------------- | +| VOICEVOX | [プロジェクト](https://github.com/VOICEVOX/voicevox/) | 主にユーザインタフェイス(エディタ)部分 | +| VOICEVOX_ENGINE | [プロジェクト](https://github.com/VOICEVOX/voicevox_engine/) | 主にWeb API実装部分 | +| VOICEVOX_CORE | [プロジェクト](https://github.com/VOICEVOX/voicevox_core/) | 主に音声合成・ライブラリ実装部分 | + +なお、全体構成を学びたい場合は、[こちら](docs/全体構成.md)が参考になることでしょう。 + +## 初心者歓迎タスク + +あなたがプログラム開発を学んだり、オープンソース開発コミュニティで活動することを実践したい場合は、既にコミュニティのIssuesで提案されている「初心者歓迎タスク」に参加することをお勧めします。 + +「初心者歓迎タスク」は、VOICEVOXプロジェクトとしては「難易度が比較的低い案件であるが、必要とされているもの」となっており、比較的一通りの工程をスマートに学びながら貢献することができます。 + +| 種類 | ページ | +| --------------- | ---------------------------------------------------------------------------------------------------------------------- | +| VOICEVOX | [初心者歓迎タスク](https://github.com/VOICEVOX/voicevox/issues?q=is%3Aissue+is%3Aopen+label%3A初心者歓迎タスク) | +| VOICEVOX_ENGINE | [初心者歓迎タスク](https://github.com/VOICEVOX/voicevox_engine/issues?q=is%3Aissue+is%3Aopen+label%3A初心者歓迎タスク) | +| VOICEVOX_CORE | [初心者歓迎タスク](https://github.com/VOICEVOX/voicevox_core/issues?q=is%3Aissue+is%3Aopen+label%3A初心者歓迎タスク) | + +## 事前準備 + +ここからはWindowsをお使いの方が、VOICEVOX(エディタ)の環境を作るケースを想定し、話を進めます。まず、テスト版VOICEVOXの環境を構築しましょう。 + +### 1. 製品版VOICEVOXを導入する + +- まず[VOICEVOXの製品版](https://voicevox.hiroshiba.jp/)を導入します。これによりすぐ使えるVOIECVOXエンジンを手に入れることができます。 + +### 2. 開発環境の構築 + +- 必須ツール + - [Node.js](https://nodejs.org/en/download/releases/)\ + [こちら](https://github.com/VOICEVOX/voicevox/blob/main/.node-version)に記載されているバージョンのインストーラを入手し、インストールします。 + +- 必要に応じて + - [Git](https://git-scm.com/downloads) + - [Visual Studio Code](https://code.visualstudio.com/) + - [GitHub CLI](https://github.com/cli/cli#installation) + - [typos](https://github.com/crate-ci/typos#install) (誤字チェックする場合) + - [Tortoise Git](https://tortoisegit.org/download/) + (エクスプローラ上で操作したい場合) + +### 3. フォークする + +- プロジェクトの複製をつくって自分のGitHubリポジトリにもってくる作業をフォークと言います。[こちら](https://github.com/VOICEVOX/voicevox/fork)を押して、フォークを実施します。 + +### 4. ソースコードを手に入れる(クローン) + +- 自分のGitHubリポジトリにあるソースコードをGitHubから作業用パソコンに持ってきます。 + +#### 4.1 コマンドラインで行う場合 + +- GitHub コマンド(GitHub CLI)を使う場合 + +```bash +gh repo clone https://github.com/(個人のGitHubアカウント名)/voicevox.git +``` + +- Git コマンド(Git CLI)を使う場合 + +```bash +git clone git@github.com:(個人のGitHubアカウント名)/voicevox.git +``` + +#### 4.2 GUIで行う場合 + +- Visual Studio CodeやTortoise Gitなどのツールを用いて入手します。 +- 指定するURLはツールによって異なりますが、`git@github.com:(個人のGitHubアカウント名)/voicevox.git`や`https://github.com/(個人のGitHubアカウント名)/voicevox.git`となります。 + +### 5. 必要なプログラムをダウンロードする + +- 手順4で手に入れたフォルダを開いて、コマンドプロンプトを開きます。 +- 環境を準備するコマンド `npm ci` + を実行してください。自動的にダウンロードされます。 +- ツールの組み合わせや実装に関する警告が表示されますが、開発環境を作るうえでは無視して差し支えありません。 + +### 6. エンジンを指定する + +- `.env.production`というファイルがありますので、コピーして、名前を`.env`にします。 +- ファイルをエディタでひらいて、`VITE_DEFAULT_ENGINE_INFOS`内の`executionFilePath`に手順1のフォルダ名をいれます。たとえば製品版をインストーラで導入し、インストール先を変更していない場合は、下記のように書き換えて保存します。 + +```ini +VITE_APP_NAME=voicevox +VITE_DEFAULT_ENGINE_INFOS=`[ + { + "uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d", + "name": "VOICEVOX Engine", + "executionEnabled": true, + "executionFilePath": "vv-engine/run.exe", + "executionArgs": [], + "host": "http://127.0.0.1:50021" + } +]` +``` + +- あなたがVOICEVOX製品版のインストール先を変更している場合は個別で指定します。たとえば、`D:\VOICEVOX0.14.1`に製品版をインストールしている場合は、下記のように書き換えて保存します。 + +```text +"executionFilePath": "D:/VOICEVOX0.14.1/vv-engine/run.exe", +``` + +### 7. 始動してみる + +- `npm run electron:serve`を実行します。 +- 設定が正しければ、開発環境が起動するはずです。 + +## プロジェクトへの貢献手順 + +### 1. 提案と調整 + +まず、下記のことがあれば、Issueとして登録をしましょう。 + +- プログラムの仕様を変更したい +- 新機能を追加したい +- バグを確認した + +#### 1.1 提案 + +その際、VOICEVOXのどの部分に関して提案をしたいのかを考えて提案しましょう。また、個人がわかる範囲で問題ないので、「改良されることで良くなる点」や「悪くなる点・影響を受ける点」を書いて登録します。 + +#### 1.2 相談 + +この段階では、関係者と実装に関しての制約や影響範囲、プロジェクト方針として優先度や実施してよいかをすり合わせます。 + +コミュニティには様々な技術領域・技量の方が混在しています。会話の途中で分からないことが出てくることも多いかとおもいます。不明点は質問し、理解を深めていきましょう。 + +#### 1.3 着手宣言 + +既にIssueとして登録されている課題に着手したい場合は、他の貢献者と作業が重複しないように、当該Issueのページで「私が着手する」旨の宣言を行ってください。 + +なお、着手宣言をした後は下記手順で作業をすることになります。定期的に相談や進捗をレポートしましょう。また、作業時間や技量などにより、コードを書き終えられないケースはあります。その場合は、抱え込まずにIssueページで相談をしてください。 + +### 2. ブランチを作る + +- 自分の作業フォルダ内に、今回加工するために作業エリアを作ります。 +- いくつかの案件を並走するなら、この手順でブランチをいくつか作ります。 +- ブランチ名は、自分のわかりやすいもので構いません。 + +### 3. プログラムを加工する + +実際にプログラムを書きます。プログラムを書くにあたっては、いくつかの流儀があります。 + +- 関数名や変数名は極力キャメルケースで命名する必要があります。何かしらの制約上キャメルケースで命名出来ない場合は、コメントを残します。 + +```ts +// FIXME: ●●のため、キャメルケースが採用できない +``` + +- 今回コーディングするが、構造制約などで「本来ありたい構造と異なる」場合にも、コメントを残します。 + +```ts +// TODO: ●●を使わずに、●●となる実装にしたい +``` + +- 変数名や型名の命名にあたっては、その仕組みで一般的に使われる命名則があれば、それらを優先して採用します。 + +- 関数名は、動詞+役割となるように設定をします。 + + | 命名例 | 役割 | + | ----------- | --------------------------- | + | setVolume() | 音量を設定 | + | getVolume() | 音量を取得 | + | isMuted | ミュート状態の取得(boolean) | + +- 変数や関数名につける英語は極力省略しないようにします。 +- コードは分かりやすさや単純さを保つようにしてください。 +- 不必要な定義や、作業中のコードは、コード提出までに除去しましょう。 + +### 4. 事前テスト + +- 提出前にコードをテストします。テストにはいくつかのツールを使います。このガイドラインの手順で進んでいれば既に必要なものはそろっている + +- 記述コードがコーディングルールに沿っていることを確認します。(特に今回の作業によって警告やエラーが増えていないかどうかに注目してください) + + ```bash + npm run lint + npm run fmt + ``` + +- TypeScriptの型チェックを行います。 + + ```bash + npm run typecheck + ``` + +- Markdownの記述が正しいことを確認します。 + + ```bash + npm run markdownlint ./*/*.md + ``` + +- 命名に使っている英語が誤っていないことを確認します。 + + ```bash + typos + ``` + +- 個人環境でVOICEVOXを実行し、提出前に、一通り動くことを確認します。 + + ```bash + npm run electron:serve + ``` + +- 使用するライブラリのライセンスに使用出来ないものが使われていないことを確認します。 + + ```bash + npm run license:generate -- -o voicevox_licenses.json + ``` + +- e2eテストの内容を確認します。 + + ```bash + npm run test:unit + npm run test:browser-e2e + npm run test:electron-e2e + ``` + + - e2eテストは実際には自分が提出する範囲外の指摘をしたり、完全に警告が消えないことがあります。 + - 確認の目安としては、加工前後で + e2eテスト結果による指摘が増えていないことを確認してください。(チェックアウト時点でe2eテストの指摘が残っていることがあるため、前後の差分で判断するのが良いでしょう) + - 提出する範囲で指摘されているようであれば提出前に訂正しましょう。 + - e2eテスト結果を修正出来ない事情がある場合や判断に迷う場合は、レビュー時に相談をしましょう。 + +### 5. コードの提出 + +- 先に個人のリポジトリにコミットします。この時、詳細欄に変更に関する具体内容を確実に記入しましょう。タイトルは簡素で分かりやすいものが好まれます。 + +- コミットが終わったら、コードをコミュニティに提案しましょう。作成したコードをコミュニティへ提案する作業の事を「Pull + Request(プルリクエスト)」といいます。 + +- Pull Requestには2つの種類があります。 + + - Draft Pull Request + - Pull Request + +- Draft Pull + Requestは、進捗状況を共有するために使います。検討の歩みがわかるため、議論が必要な項目では特に有効な手段です。 + +- Draft Pull Requestの場合は、タイトルの先頭に `WIP:` + をつけると見た目に分かりやすいです。 + +- 凡そ問題ないと判断できたら、Pull Requestを提出します。 + この先は共同作業になるので、今一度作業忘れや問題がないか確認をしてください。 + +- Pull Requestを出すときには、関連するIssueのナンバーを記載しておきます。 + + 記入例: + + ```text + タイトル:起動時の待ち時間を低減する + 内容:初期化を並列処理することで待ち時間を短縮させる + 関連Issue:ref #0000 + ``` + +- 議論する場所が分散する事を防ぐため、Pull + Requestするタイミングで問題提起した方の番号を閉じましょう。コメントに下記のような表記をすることでIssue側の議論を閉じることができます。 + + ```text + close #0000 + ``` + +### 6. コードレビュー + +- レビュー担当やコミュニティメンバーによってソースの査読が行われます。課題が発見された場合には、記述修正の提案がおこなわれます。 + +- 納得できれば「提案通りに直す」方法もありますが、課題があると感じていれば、議論を行い最良点を探してください。 + +- この段階では、既にPull + Requestが出されていますので、自分のリポジトリに修正分をプッシュするだけで自動的に追跡されます。 + +- 現在のVOICEVOXのマージルールでは、基本的に1名以上の査読が必要となっています。 + +### 7. コンフリクト対応 + +- レビューが終わると、マージ(取り込み)準備が始まります。 + +- 作業中に他の修正が取り込まれている場合には、修正箇所が重なる「コンフリクト」という現象が発生することがあります。 + +- コンフリクトが発生した場合には、Pull + Requestのページに「コンフリクトが発生している」と表示されるので、次の手順で修正を行います。 + + 1. 自分の作業リポジトリにプルします。プル元は、自分のGitHubリポジトリではなく、[VOICEVOXのリポジトリ](https://github.com/VOICEVOX/voicevox.git)を指定します。 + + 2. 変更差分をみながら、コンフリクトしている部分を正しい実装に修正します。 + + 3. 変更がすべて終わったら、手順4にあった「事前テスト」を改めて実施します。 + + 4. 問題なければ、コミットします。 + + 5. VOICEVOXのPull + Requestページで自動検査処理が走ります。この画面で「コンフリクトが発生した」という表示が消えたことを確認してください。 + +- お疲れさまでした。この工程までいけば、貢献者の仕事はおわりです。 +- マージ担当者が確認後、マージ処理をします。 +- マージが終われば、個人のブランチは削除できます。 + +### その他 + +- VOICEVOXプロジェクトメンバーは、貢献者が活動しやすいようサポートや相談に応じています。 +- こまったり、わからないことが発生したら、プロジェクトメンバーに相談しましょう。 +- [Discordコミュニティ](https://discord.gg/gJamMrqFHg)もあります。議論しながら考えをまとめたい場合など、コミュニティをうまく活用するとよいでしょう。 +- 諸事情で処理が継続できなくなった場合や、技術的ハードルが高かったりして心が折れたときは、ギブアップを宣言することも可能です。調整が可能な場合もあるので、宣言する前に相談をすることをお勧めします。 + +## 参考情報 + +- 実装時のデザインに関しては、[UX・UIデザインの方針](docs/UX・UIデザインの方針.md)を参考に実装してください。 + +- 設計詳細については、[細かい設計方針](docs/細かい設計方針.md)を参考にしてください。 + +- VOICEVOXは、様々な技術を使って実装しており、技術の理解が無いと読みづらい部分があります。全体の構造については、[コードの歩き方](docs/コードの歩き方.md)を参考にしてみてください。 + +- 色については、[色の実装](docs/色について.md)についてのドキュメントを参考にしてみてください。 + +- VOICEVOXで使用するフォントは、[フォントについて](docs/フォントについて.md)に生成方法などが書いてあります。 diff --git a/README.md b/README.md index 48a7fd5a47..c65861f4ac 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,13 @@ こちらは開発用のページになります。利用方法に関しては[VOICEVOX 公式サイト](https://voicevox.hiroshiba.jp/) をご覧ください。 -## 貢献者の方へ +## プロジェクトに貢献したいと考えている方へ + +VOICEVOXプロジェクトは興味ある方の参画を歓迎しています。 +[貢献手順について説明したガイド](./CONTRIBUTING.md)をご用意しております。 + +貢献というとプログラム作成と思われがちですが、ドキュメント執筆、テスト生成、改善提案への議論参加など様々な参加方法があります。 +初心者歓迎タスクもありますので、皆様のご参加をお待ちしております。 VOICEVOX のエディタは Electron・TypeScript・Vue・Vuex などが活用されており、全体構成がわかりにくくなっています。 [コードの歩き方](./docs/コードの歩き方.md)で構成を紹介しているので、開発の一助になれば幸いです。 diff --git a/package-lock.json b/package-lock.json index 5e9b60a014..f0b1d9500b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.0", "async-lock": "1.4.0", - "buffer": "6.0.3", "dayjs": "1.10.7", "electron-log": "5.1.2", "electron-window-state": "5.0.3", @@ -23,14 +23,12 @@ "hotkeys-js": "3.13.6", "immer": "9.0.21", "markdown-it": "13.0.2", - "midi-file": "1.2.4", "move-file": "3.0.0", "multistream": "4.1.0", "pixi.js": "7.4.0", "quasar": "2.11.6", "semver": "7.5.4", "shlex": "2.1.2", - "source-map-support": "0.5.19", "systeminformation": "5.21.15", "tree-kill": "1.2.2", "vue": "3.4.26", @@ -43,14 +41,11 @@ "@playwright/test": "1.43.1", "@quasar/vite-plugin": "1.6.0", "@types/async-lock": "1.4.0", - "@types/diff": "5.0.3", - "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", "@types/glob": "8.0.0", "@types/markdown-it": "12.2.0", "@types/multistream": "4.1.0", "@types/semver": "7.3.9", - "@types/unzipper": "0.10.5", "@types/wicg-file-system-access": "2020.9.6", "@types/yargs": "17.0.32", "@typescript-eslint/eslint-plugin": "7.11.0", @@ -74,15 +69,10 @@ "eslint-plugin-vue": "9.26.0", "happy-dom": "8.4.2", "license-checker-rseidelsohn": "4.3.0", - "markdownlint": "0.31.1", "markdownlint-cli": "0.37.0", - "optionator": "0.9.1", "prettier": "3.2.5", "sass": "1.32.13", - "sass-loader": "8.0.2", - "tmp": "0.2.1", "ts-node": "10.9.1", - "tsconfig-paths": "4.1.2", "typescript": "5.4.5", "vite": "5.2.9", "vite-plugin-checker": "0.6.4", @@ -1166,6 +1156,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1181,6 +1172,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1201,6 +1193,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "optional": true, "peer": true, "engines": { "node": ">=6.0.0" @@ -1211,6 +1204,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1222,6 +1216,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2204,6 +2199,16 @@ "win32" ] }, + "node_modules/@sevenc-nanashi/utaformatix-ts": { + "name": "@jsr/sevenc-nanashi__utaformatix-ts", + "version": "0.3.0", + "resolved": "https://npm.jsr.io/~/11/@jsr/sevenc-nanashi__utaformatix-ts/0.3.0.tgz", + "integrity": "sha512-D6Y6lkxOawpv3LKSgrTGKLquuzaFvkwNThSGDXT5QBnfk7VE4bFbqJr9JlgjvAdh5sNQVGPku6uMdIdvSDxzAg==", + "dependencies": { + "jszip": "^3.10.1", + "utaformatix-data": "^1.1.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2299,23 +2304,11 @@ "@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/earcut": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==" }, - "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", - "integrity": "sha512-8o2XkyAw2HZoVD5KpIoUJmEgZ7BPVv33p7rY1jmn/wJUbugtQUc44vNMDTguUNUGiLv+oqgtyYmiYctHDZEzdQ==", - "dev": true - }, "node_modules/@types/encoding-japanese": { "version": "1.0.18", "resolved": "https://registry.npmjs.org/@types/encoding-japanese/-/encoding-japanese-1.0.18.tgz", @@ -2327,23 +2320,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2474,15 +2457,6 @@ "integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==", "dev": true }, - "node_modules/@types/unzipper": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz", - "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/verror": { "version": "1.10.10", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", @@ -3061,167 +3035,6 @@ "@vue/language-core": "1.8.19" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", - "dev": true, - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3231,20 +3044,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "peer": true - }, "node_modules/7zip-bin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", @@ -3272,16 +3071,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "dev": true, - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3857,6 +3646,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "devOptional": true, "funding": [ { "type": "github", @@ -3872,15 +3662,6 @@ } ] }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4116,62 +3897,6 @@ "pako": "~1.0.5" } }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4196,7 +3921,8 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/buffer-xor": { "version": "1.0.3", @@ -4323,27 +4049,6 @@ "node": ">=6" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001615", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001615.tgz", - "integrity": "sha512-1IpazM5G3r38meiae0bHRnPhz+CBQ3ZLqbQMtrg+AsTPKAXgW38JNsXkyZ+v8waCsDmPq87lmfun5Q2AGysNEQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true - }, "node_modules/chai": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", @@ -4441,16 +4146,6 @@ "node": ">=10" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -4568,20 +4263,6 @@ "node": ">=0.8" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -4833,8 +4514,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "devOptional": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/crc": { "version": "3.8.0", @@ -5638,13 +5318,6 @@ "mime": "^2.5.2" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.756", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz", - "integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==", - "dev": true, - "peer": true - }, "node_modules/electron-window-state": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-5.0.3.tgz", @@ -5684,15 +5357,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "devOptional": true }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encoding-japanese": { "version": "1.0.30", "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-1.0.30.tgz", @@ -5707,20 +5371,6 @@ "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "dev": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", @@ -5826,13 +5476,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.2.tgz", - "integrity": "sha512-l60ETUTmLqbVbVHv1J4/qj+M8nq7AwMzEcg3kmJDt9dCNrTk+yHcYFf/Kw75pMDwd9mPcIGCG5LcS20SxYRzFA==", - "dev": true, - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -7041,13 +6684,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "peer": true - }, "node_modules/glob/node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -7450,6 +7086,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "devOptional": true, "funding": [ { "type": "github", @@ -7477,8 +7114,7 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/immer": { "version": "9.0.21", @@ -7827,18 +7463,6 @@ "node": ">=8" } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7980,15 +7604,6 @@ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isomorphic-timers-promises": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", @@ -8065,37 +7680,6 @@ "node": "*" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/js-beautify": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", @@ -8239,7 +7823,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -8250,14 +7833,12 @@ "node_modules/jszip/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8271,14 +7852,12 @@ "node_modules/jszip/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -8292,15 +7871,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -8415,7 +7985,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "dependencies": { "immediate": "~3.0.5" } @@ -8428,42 +7997,6 @@ "uc.micro": "^1.0.1" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/local-pkg": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", @@ -8789,11 +8322,6 @@ "node": ">=8.6" } }, - "node_modules/midi-file": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/midi-file/-/midi-file-1.2.4.tgz", - "integrity": "sha512-B5SnBC6i2bwJIXTY9MElIydJwAmnKx+r5eJ1jknTLetzLflEl0GWveuBB6ACrQpecSRkOB6fhTx1PwXk2BVxnA==" - }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -9042,12 +8570,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", @@ -9074,13 +8596,6 @@ } } }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true, - "peer": true - }, "node_modules/node-stdlib-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.2.0.tgz", @@ -9382,6 +8897,8 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -9474,8 +8991,7 @@ "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -9870,8 +9386,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -10563,76 +10078,12 @@ "node": ">=8.9.0" } }, - "node_modules/sass-loader": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz", - "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "loader-utils": "^1.2.3", - "neo-async": "^2.6.1", - "schema-utils": "^2.6.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0", - "sass": "^1.3.0", - "webpack": "^4.36.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/sass-loader/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/sax": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", "dev": true }, - "node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -10683,16 +10134,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10727,8 +10168,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, "node_modules/sha.js": { "version": "2.4.11", @@ -10743,18 +10183,6 @@ "sha.js": "bin.js" } }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10873,6 +10301,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10889,6 +10318,7 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -11249,16 +10679,6 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", @@ -11320,6 +10740,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11334,76 +10755,12 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, + "optional": true, "peer": true }, "node_modules/terser/node_modules/source-map-support": { @@ -11411,6 +10768,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "optional": true, "peer": true, "dependencies": { "buffer-from": "^1.0.0", @@ -11611,20 +10969,6 @@ } } }, - "node_modules/tsconfig-paths": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", - "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", - "dev": true, - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -11820,37 +11164,6 @@ "yaku": "^0.16.6" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz", - "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -11874,6 +11187,14 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" }, + "node_modules/utaformatix-data": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/utaformatix-data/-/utaformatix-data-1.1.0.tgz", + "integrity": "sha512-1AoxBvRMkXjifHqvIpTnvLLGo3Qyj1Q4PSQLgKd8e6RMq4HA5o6QNI4ila9BO1fkJAWA9Azj/hVANHWBkpbVvg==", + "engines": { + "node": ">=10" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -12427,20 +11748,6 @@ "vue": "^3.0.2" } }, - "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "dev": true, - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -12459,114 +11766,6 @@ "node": ">=12" } }, - "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", - "dev": true, - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "peer": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index de62702535..712f0241e7 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ "dependencies": { "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", + "@sevenc-nanashi/utaformatix-ts": "npm:@jsr/sevenc-nanashi__utaformatix-ts@0.3.0", "async-lock": "1.4.0", - "buffer": "6.0.3", "dayjs": "1.10.7", "electron-log": "5.1.2", "electron-window-state": "5.0.3", @@ -47,14 +47,12 @@ "hotkeys-js": "3.13.6", "immer": "9.0.21", "markdown-it": "13.0.2", - "midi-file": "1.2.4", "move-file": "3.0.0", "multistream": "4.1.0", "pixi.js": "7.4.0", "quasar": "2.11.6", "semver": "7.5.4", "shlex": "2.1.2", - "source-map-support": "0.5.19", "systeminformation": "5.21.15", "tree-kill": "1.2.2", "vue": "3.4.26", @@ -70,14 +68,11 @@ "@playwright/test": "1.43.1", "@quasar/vite-plugin": "1.6.0", "@types/async-lock": "1.4.0", - "@types/diff": "5.0.3", - "@types/electron-devtools-installer": "2.2.2", "@types/encoding-japanese": "1.0.18", "@types/glob": "8.0.0", "@types/markdown-it": "12.2.0", "@types/multistream": "4.1.0", "@types/semver": "7.3.9", - "@types/unzipper": "0.10.5", "@types/wicg-file-system-access": "2020.9.6", "@types/yargs": "17.0.32", "@typescript-eslint/eslint-plugin": "7.11.0", @@ -101,15 +96,10 @@ "eslint-plugin-vue": "9.26.0", "happy-dom": "8.4.2", "license-checker-rseidelsohn": "4.3.0", - "markdownlint": "0.31.1", "markdownlint-cli": "0.37.0", - "optionator": "0.9.1", "prettier": "3.2.5", "sass": "1.32.13", - "sass-loader": "8.0.2", - "tmp": "0.2.1", "ts-node": "10.9.1", - "tsconfig-paths": "4.1.2", "typescript": "5.4.5", "vite": "5.2.9", "vite-plugin-checker": "0.6.4", diff --git a/public/howtouse.md b/public/howtouse.md index e316c3a0ab..bb49033d75 100644 --- a/public/howtouse.md +++ b/public/howtouse.md @@ -299,6 +299,12 @@ VOICEVOX では、歌声合成機能がプロトタイプ版として提供さ ソング機能は鋭意制作中です。フィードバックをお待ちしています。 +### 歌詞の入力 + +ノートをダブルクリックすることで歌詞を入力できます。複数の文字を入力すれば一括入力できます。 + +ノートに複数の文字を入力することで、後ろのノートに歌詞が送られる様子が写っています + ### 音域調整 「音域調整」の値が大きいほど高い音域で、小さいほど低い音域でうまく歌えるようになります。 diff --git a/public/res/song2.png b/public/res/song2.png new file mode 100644 index 0000000000..be2014418c Binary files /dev/null and b/public/res/song2.png differ diff --git a/src/backend/browser/fileImpl.ts b/src/backend/browser/fileImpl.ts index 25c3f6684c..d60f85f30f 100644 --- a/src/backend/browser/fileImpl.ts +++ b/src/backend/browser/fileImpl.ts @@ -3,6 +3,9 @@ import { directoryHandleStoreKey } from "./contract"; import { openDB } from "./browserConfig"; import { SandboxKey } from "@/type/preload"; import { failure, success } from "@/type/result"; +import { createLogger } from "@/domain/frontend/log"; + +const log = createLogger("fileImpl"); const storeDirectoryHandle = async ( directoryHandle: FileSystemDirectoryHandle, @@ -176,3 +179,45 @@ export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileE return Promise.resolve(fileEntries.includes(fileName)); }; + +// FileSystemFileHandleを保持するMap。キーは生成した疑似パス。 +const fileHandleMap: Map = new Map(); + +// ファイル選択ダイアログを開く +// 返り値はファイルパスではなく、疑似パスを返す +export const showOpenFilePickerImpl = async (options: { + multiple: boolean; + fileTypes: { + description: string; + accept: Record; + }[]; +}) => { + try { + const handles = await showOpenFilePicker({ + excludeAcceptAllOption: true, + multiple: options.multiple, + types: options.fileTypes, + }); + const paths = []; + for (const handle of handles) { + const fakePath = `-${handle.name}`; + fileHandleMap.set(fakePath, handle); + paths.push(fakePath); + } + return handles.length > 0 ? paths : undefined; + } catch (e) { + log.warn(`showOpenFilePicker error: ${e}`); + return undefined; + } +}; + +// 指定した疑似パスのファイルを読み込む +export const readFileImpl = async (filePath: string) => { + const fileHandle = fileHandleMap.get(filePath); + if (fileHandle == undefined) { + return failure(new Error(`ファイルが見つかりません: ${filePath}`)); + } + const file = await fileHandle.getFile(); + const buffer = await file.arrayBuffer(); + return success(buffer); +}; diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index f3e298054d..f2f5451085 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -1,7 +1,9 @@ import { defaultEngine } from "./contract"; import { checkFileExistsImpl, + readFileImpl, showOpenDirectoryDialogImpl, + showOpenFilePickerImpl, writeFileImpl, } from "./fileImpl"; import { getConfigManager } from "./browserConfig"; @@ -127,10 +129,18 @@ export const api: Sandbox = { } }); }, - showProjectLoadDialog(/* obj: { title: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + async showProjectLoadDialog() { + return showOpenFilePickerImpl({ + multiple: false, + fileTypes: [ + { + description: "Voicevox Project File", + accept: { + "application/json": [".vvproj"], + }, + }, + ], + }); }, showMessageDialog(obj: { type: "none" | "info" | "error" | "question" | "warning"; @@ -156,18 +166,35 @@ export const api: Sandbox = { `Not implemented: showQuestionDialog, request: ${JSON.stringify(obj)}`, ); }, - showImportFileDialog(/* obj: { title: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + async showImportFileDialog(obj: { + name?: string; + extensions?: string[]; + title: string; + }) { + const fileHandle = await showOpenFilePickerImpl({ + multiple: false, + fileTypes: [ + { + description: obj.name ?? "Text", + accept: obj.extensions + ? { + "application/octet-stream": obj.extensions.map( + (ext) => `.${ext}`, + ), + } + : { + "plain/text": [".txt"], + }, + }, + ], + }); + return fileHandle?.[0]; }, writeFile(obj: { filePath: string; buffer: ArrayBuffer }) { return writeFileImpl(obj); }, - readFile(/* obj: { filePath: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + readFile(obj: { filePath: string }) { + return readFileImpl(obj.filePath); }, isAvailableGPUMode() { // TODO: WebAssembly版をサポートする時に実装する diff --git a/src/components/Dialog/AllDialog.vue b/src/components/Dialog/AllDialog.vue index 4711b8aea6..30d538c65b 100644 --- a/src/components/Dialog/AllDialog.vue +++ b/src/components/Dialog/AllDialog.vue @@ -22,7 +22,7 @@ - + diff --git a/src/components/Dialog/ImportMidiDialog.vue b/src/components/Dialog/ImportMidiDialog.vue deleted file mode 100644 index 6abdf11d33..0000000000 --- a/src/components/Dialog/ImportMidiDialog.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - diff --git a/src/components/Dialog/ImportSongProjectDialog.vue b/src/components/Dialog/ImportSongProjectDialog.vue new file mode 100644 index 0000000000..9b6153cba0 --- /dev/null +++ b/src/components/Dialog/ImportSongProjectDialog.vue @@ -0,0 +1,302 @@ + + + diff --git a/src/components/Dialog/SettingDialog/ButtonToggleCell.vue b/src/components/Dialog/SettingDialog/ButtonToggleCell.vue new file mode 100644 index 0000000000..fa4abf53fe --- /dev/null +++ b/src/components/Dialog/SettingDialog/ButtonToggleCell.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/components/Dialog/SettingDialog/SettingDialog.vue b/src/components/Dialog/SettingDialog/SettingDialog.vue index 7285c7dc93..75574e3a90 100644 --- a/src/components/Dialog/SettingDialog/SettingDialog.vue +++ b/src/components/Dialog/SettingDialog/SettingDialog.vue @@ -43,47 +43,22 @@ /> - -
エンジンモード
-
- - - GPU モードの利用には GPU が必要です。Linux は - NVIDIA™ 製 GPU のみ対応しています。 - - -
- - + - - {{ - engineInfos[selectedEngineId].name - }}はCPU版のためGPUモードを利用できません。 - - -
+ {{ + engineInfos[selectedEngineId].name + }}はCPU版のためGPUモードを利用できません。 + +
音声のサンプリングレート
@@ -134,7 +109,7 @@ :modelValue=" experimentalSetting.shouldApplyDefaultPresetOnVoiceChanged " - @updated=" + @update:modelValue=" changeExperimentalSetting( 'shouldApplyDefaultPresetOnVoiceChanged', $event, @@ -147,137 +122,69 @@ title="パラメータの引き継ぎ" description="ONの場合、テキスト欄追加の際に、現在の話速等のパラメータが引き継がれます。" :modelValue="inheritAudioInfoMode" - @updated="changeinheritAudioInfo" + @update:modelValue="changeinheritAudioInfo" + /> + + - -
再生位置を追従
-
- - - 音声再生中の、詳細調整欄の自動スクロールのモードを選べます。 - - -
- - - - - - -
- -
テキスト自動分割
-
- - - テキスト貼り付けの際のテキストの分割箇所を選べます。 - - -
- - - - - - - -
非表示にしたヒントを全て再表示
@@ -429,54 +336,37 @@ title="上書き防止" description="ONの場合、書き出す際に同名ファイルが既にあったとき、ファイル名に連番を付けて別名で保存されます。" :modelValue="savingSetting.avoidOverwrite" - @updated="handleSavingSettingChange('avoidOverwrite', $event)" + @update:modelValue=" + handleSavingSettingChange('avoidOverwrite', $event) + " + /> + - -
文字コード
-
- - - テキストファイルを書き出す際の文字コードを選べます。 - - -
- - -
@@ -484,76 +374,33 @@
外観
- -
テーマ
-
- - - エディタの色を選べます。 - - -
- - -
- - -
フォント
-
- - - エディタのフォントを選べます。 - - -
- - -
+ + @@ -566,13 +413,15 @@ title="マルチエンジン機能" description="ONの場合、複数のVOICEVOX準拠エンジンを利用可能にします。" :modelValue="enableMultiEngine" - @updated="setEnableMultiEngine" + @update:modelValue="setEnableMultiEngine" /> @@ -644,7 +495,7 @@ title="[開発時のみ機能] 調整結果の保持" description="ONの場合、テキスト変更時、同じ読みのアクセント区間内の調整結果を保持します。" :modelValue="experimentalSetting.shouldKeepTuningOnTextChange" - @updated=" + @update:modelValue=" changeExperimentalSetting( 'shouldKeepTuningOnTextChange', $event, @@ -655,7 +506,7 @@ title="ソング:ピッチ編集機能" description="ONの場合、ピッチ編集モードに切り替えて音の高さを変えられるようになります。" :modelValue="experimentalSetting.enablePitchEditInSongEditor" - @updated=" + @update:modelValue=" changeExperimentalSetting( 'enablePitchEditInSongEditor', $event, @@ -671,7 +522,7 @@ title="ソフトウェア利用状況のデータ収集を許可" description="ONの場合、各UIの利用率などのデータが送信され、VOICEVOXの改善に役立てられます。テキストデータや音声データは送信されません。" :modelValue="acceptRetrieveTelemetryComputed" - @updated="acceptRetrieveTelemetryComputed = $event" + @update:modelValue="acceptRetrieveTelemetryComputed = $event" />
@@ -685,6 +536,7 @@ import { computed, ref, watchEffect } from "vue"; import FileNamePatternDialog from "./FileNamePatternDialog.vue"; import ToggleCell from "./ToggleCell.vue"; +import ButtonToggleCell from "./ButtonToggleCell.vue"; import { useStore } from "@/store"; import { isProduction, @@ -694,6 +546,7 @@ import { ActivePointScrollMode, RootMiscSettingType, EngineId, + EditorFontType, } from "@/type/preload"; import { createLogger } from "@/domain/frontend/log"; diff --git a/src/components/Dialog/SettingDialog/ToggleCell.vue b/src/components/Dialog/SettingDialog/ToggleCell.vue index c96694f551..2b8ef85850 100644 --- a/src/components/Dialog/SettingDialog/ToggleCell.vue +++ b/src/components/Dialog/SettingDialog/ToggleCell.vue @@ -1,16 +1,21 @@ + + diff --git a/src/components/Sing/menuBarData.ts b/src/components/Sing/menuBarData.ts index 56af239d8a..b13733822e 100644 --- a/src/components/Sing/menuBarData.ts +++ b/src/components/Sing/menuBarData.ts @@ -7,23 +7,13 @@ export const useMenuBarData = () => { const uiLocked = computed(() => store.getters.UI_LOCKED); const isNotesSelected = computed(() => store.state.selectedNoteIds.size > 0); - const importMidiFile = async () => { + const importExternalSongProject = async () => { if (uiLocked.value) return; await store.dispatch("SET_DIALOG_OPEN", { - isImportMidiDialogOpen: true, + isImportSongProjectDialogOpen: true, }); }; - const importMusicXMLFile = async () => { - if (uiLocked.value) return; - await store.dispatch("IMPORT_MUSICXML_FILE", {}); - }; - - const importUstFile = async () => { - if (uiLocked.value) return; - await store.dispatch("IMPORT_UST_FILE", {}); - }; - const exportWaveFile = async () => { if (uiLocked.value) return; await store.dispatch("EXPORT_WAVE_FILE", {}); @@ -41,25 +31,9 @@ export const useMenuBarData = () => { { type: "separator" }, { type: "button", - label: "MIDI読み込み", - onClick: () => { - importMidiFile(); - }, - disableWhenUiLocked: true, - }, - { - type: "button", - label: "MusicXML読み込み", - onClick: () => { - importMusicXMLFile(); - }, - disableWhenUiLocked: true, - }, - { - type: "button", - label: "UST読み込み", + label: "インポート", onClick: () => { - importUstFile(); + importExternalSongProject(); }, disableWhenUiLocked: true, }, diff --git a/src/domain/frontend/log.ts b/src/domain/frontend/log.ts index ef4ab4554a..3b7a224d9f 100644 --- a/src/domain/frontend/log.ts +++ b/src/domain/frontend/log.ts @@ -3,8 +3,8 @@ export function createLogger(scope: string) { const createInner = (method: "logInfo" | "logError" | "logWarn") => - (message: string, ...args: unknown[]) => { - window.backend[method](`[${scope}] ${message}`, ...args); + (...args: unknown[]) => { + window.backend[method](`[${scope}] ${args[0]}`, ...args.slice(1)); }; return { info: createInner("logInfo"), diff --git a/src/sing/midi.ts b/src/sing/midi.ts deleted file mode 100644 index c493d9c18c..0000000000 --- a/src/sing/midi.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - MidiData, - parseMidi, - MidiTrackNameEvent, - MidiEvent, - MidiSetTempoEvent, - MidiTimeSignatureEvent, - MidiLyricsEvent, - MidiNoteOnEvent, - MidiNoteOffEvent, -} from "midi-file"; -type Tempo = { ticks: number; bpm: number }; -type TimeSignature = { - ticks: number; - numerator: number; - denominator: number; -}; -type Note = { - ticks: number; - noteNumber: number; - duration: number; - lyric?: string; -}; - -// BPMの精度。(小数点以下の桁数) -const bpmPrecision = 2; - -/** - * midi-fileの軽いラッパー。 - */ -export class Midi { - data: MidiData; - tracks: Track[]; - constructor(data: ArrayBuffer) { - this.data = parseMidi(new Uint8Array(data)); - this.tracks = this.data.tracks.map((track) => new Track(track)); - } - - get header() { - return this.data.header; - } - - get ticksPerBeat() { - const maybeTicksPerBeat = this.data.header.ticksPerBeat; - if (maybeTicksPerBeat == undefined) { - throw new Error("ticksPerBeat is undefined"); - } - return maybeTicksPerBeat; - } - - get tempos(): Tempo[] { - const tempos = this.tracks.flatMap((track) => track.tempos); - tempos.sort((a, b) => a.ticks - b.ticks); - return tempos; - } - - get timeSignatures(): TimeSignature[] { - const timeSignatures = this.tracks.flatMap((track) => track.timeSignatures); - timeSignatures.sort((a, b) => a.ticks - b.ticks); - return timeSignatures; - } -} - -type MidiEventWithTime = { - time: number; -} & T; -export class Track { - readonly data: MidiData["tracks"][0]; - readonly events: MidiEventWithTime[]; - readonly notes: Note[]; - constructor(data: MidiData["tracks"][0]) { - this.data = data; - let time = 0; - this.events = data.map((event) => { - time += event.deltaTime; - return { time, ...event }; - }); - const lyrics = this.events.filter( - (e) => e.type === "lyrics", - ) as MidiEventWithTime[]; - const lyricsMap = new Map( - lyrics.map((e) => { - // midi-fileはUTF-8としてデコードしてくれないので、ここでデコードする - const buffer = new Uint8Array( - e.text.split("").map((c) => c.charCodeAt(0)), - ); - const decoder = new TextDecoder("utf-8"); - return [e.time, decoder.decode(buffer)]; - }), - ); - - const noteOnOffs = this.events.filter( - (e) => e.type === "noteOn" || e.type === "noteOff", - ) as MidiEventWithTime[]; - noteOnOffs.sort((a, b) => a.time - b.time); - this.notes = []; - const temporaryNotes = new Map< - number, - { noteNumber: number; time: number } - >(); - for (const event of noteOnOffs) { - if (event.type === "noteOn") { - if (temporaryNotes.has(event.noteNumber)) { - throw new Error("noteOn without noteOff"); - } - temporaryNotes.set(event.noteNumber, { - noteNumber: event.noteNumber, - time: event.time, - }); - } else { - const note = temporaryNotes.get(event.noteNumber); - if (!note) { - throw new Error("noteOff without noteOn"); - } - temporaryNotes.delete(event.noteNumber); - this.notes.push({ - ticks: note.time, - noteNumber: note.noteNumber, - duration: event.time - note.time, - // 同じタイミングの歌詞をノートの歌詞として使う - lyric: lyricsMap.get(note.time), - }); - } - } - } - - get name() { - const nameEvent = this.data.find( - (e) => e.type === "trackName", - ) as MidiTrackNameEvent; - if (!nameEvent) { - return ""; - } - return nameEvent.text; - } - - get tempos(): Tempo[] { - const tempoEvents = this.events.filter( - (e) => e.type === "setTempo", - ) as MidiEventWithTime[]; - - const tempos = tempoEvents.map((e) => ({ - ticks: e.time, - bpm: - Math.round( - ((60 * 1000000) / e.microsecondsPerBeat) * 10 ** bpmPrecision, - ) / - 10 ** bpmPrecision, - })); - tempos.sort((a, b) => a.ticks - b.ticks); - return tempos; - } - - get timeSignatures(): TimeSignature[] { - const timeSignatureEvents = this.events.filter( - (e) => e.type === "timeSignature", - ) as MidiEventWithTime[]; - - const timeSignatures = timeSignatureEvents.map((e) => ({ - ticks: e.time, - numerator: e.numerator, - denominator: e.denominator, - })); - timeSignatures.sort((a, b) => a.ticks - b.ticks); - return timeSignatures; - } -} diff --git a/src/sing/utaformatixProject/common.ts b/src/sing/utaformatixProject/common.ts new file mode 100644 index 0000000000..dee6839344 --- /dev/null +++ b/src/sing/utaformatixProject/common.ts @@ -0,0 +1,8 @@ +import { Tempo, TimeSignature, Track } from "@/store/type"; + +export type VoicevoxScore = { + tracks: Track[]; + tpqn: number; + tempos: Tempo[]; + timeSignatures: TimeSignature[]; +}; diff --git a/src/sing/utaformatixProject/fromVoicevox.ts b/src/sing/utaformatixProject/fromVoicevox.ts new file mode 100644 index 0000000000..fba4a3ee9b --- /dev/null +++ b/src/sing/utaformatixProject/fromVoicevox.ts @@ -0,0 +1,38 @@ +// TODO: エクスポート機能を実装する + +import { Project as UfProject, UfData } from "@sevenc-nanashi/utaformatix-ts"; +import { VoicevoxScore } from "./common"; + +/** Voicevoxの楽譜データをUtaformatixのProjectに変換する */ +export const ufProjectFromVoicevox = ( + { tracks, tpqn, tempos, timeSignatures }: VoicevoxScore, + projectName: string, +): UfProject => { + const convertTicks = (ticks: number) => Math.round((ticks / tpqn) * 480); + const ufData: UfData = { + formatVersion: 1, + project: { + measurePrefix: 0, + name: projectName, + tempos: tempos.map((tempo) => ({ + tickPosition: convertTicks(tempo.position), + bpm: tempo.bpm, + })), + timeSignatures: timeSignatures.map((timeSignature) => ({ + measurePosition: timeSignature.measureNumber, + numerator: timeSignature.beats, + denominator: timeSignature.beatType, + })), + tracks: tracks.map((track) => ({ + name: `無名トラック`, + notes: track.notes.map((note) => ({ + key: note.noteNumber, + tickOn: convertTicks(note.position), + tickOff: convertTicks(note.position + note.duration), + lyric: note.lyric, + })), + })), + }, + }; + return new UfProject(ufData); +}; diff --git a/src/sing/utaformatixProject/toVoicevox.ts b/src/sing/utaformatixProject/toVoicevox.ts new file mode 100644 index 0000000000..f3e2448d67 --- /dev/null +++ b/src/sing/utaformatixProject/toVoicevox.ts @@ -0,0 +1,121 @@ +import { Project as UfProject } from "@sevenc-nanashi/utaformatix-ts"; +import { VoicevoxScore } from "./common"; +import { DEFAULT_TPQN, createDefaultTrack } from "@/sing/domain"; +import { getDoremiFromNoteNumber } from "@/sing/viewHelper"; +import { NoteId } from "@/type/preload"; +import { Note, Tempo, TimeSignature, Track } from "@/store/type"; + +/** UtaformatixのプロジェクトをVoicevoxの楽譜データに変換する */ +export const ufProjectToVoicevox = (project: UfProject): VoicevoxScore => { + const convertPosition = ( + position: number, + sourceTpqn: number, + targetTpqn: number, + ) => { + return Math.round(position * (targetTpqn / sourceTpqn)); + }; + + const convertDuration = ( + startPosition: number, + endPosition: number, + sourceTpqn: number, + targetTpqn: number, + ) => { + const convertedEndPosition = convertPosition( + endPosition, + sourceTpqn, + targetTpqn, + ); + const convertedStartPosition = convertPosition( + startPosition, + sourceTpqn, + targetTpqn, + ); + return Math.max(1, convertedEndPosition - convertedStartPosition); + }; + + const removeDuplicateTempos = (tempos: Tempo[]) => { + return tempos.filter((value, index, array) => { + return ( + index === array.length - 1 || + value.position !== array[index + 1].position + ); + }); + }; + + const removeDuplicateTimeSignatures = (timeSignatures: TimeSignature[]) => { + return timeSignatures.filter((value, index, array) => { + return ( + index === array.length - 1 || + value.measureNumber !== array[index + 1].measureNumber + ); + }); + }; + + // 歌詞をひらがなの単独音に変換する + const convertedProject = project.convertJapaneseLyrics("auto", "KanaCv", { + convertVowelConnections: true, + }); + + // 480は固定値。 + // https://github.com/sdercolin/utaformatix-data?tab=readme-ov-file#value-conventions + const projectTpqn = 480; + const projectTempos = convertedProject.tempos; + const projectTimeSignatures = convertedProject.timeSignatures; + + const tpqn = DEFAULT_TPQN; + + const tracks: Track[] = convertedProject.tracks.map((projectTrack) => { + const trackNotes = projectTrack.notes; + + trackNotes.sort((a, b) => a.tickOn - b.tickOn); + + const notes = trackNotes.map((value): Note => { + return { + id: NoteId(crypto.randomUUID()), + position: convertPosition(value.tickOn, projectTpqn, tpqn), + duration: convertDuration( + value.tickOn, + value.tickOff, + projectTpqn, + tpqn, + ), + noteNumber: value.key, + lyric: value.lyric || getDoremiFromNoteNumber(value.key), + }; + }); + + return { + ...createDefaultTrack(), + notes, + }; + }); + + let tempos = projectTempos.map((value): Tempo => { + return { + position: convertPosition(value.tickPosition, projectTpqn, tpqn), + bpm: value.bpm, + }; + }); + tempos = removeDuplicateTempos(tempos); + + let timeSignatures: TimeSignature[] = []; + for (const ts of projectTimeSignatures) { + const beats = ts.numerator; + const beatType = ts.denominator; + timeSignatures.push({ + // UtaFormatixは0から始まるので+1する + measureNumber: ts.measurePosition + 1, + beats, + beatType, + }); + } + timeSignatures = removeDuplicateTimeSignatures(timeSignatures); + + return { + tracks, + tpqn, + tempos, + timeSignatures, + }; +}; diff --git a/src/store/project.ts b/src/store/project.ts index 8b6feae799..64f14a2910 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -130,6 +130,29 @@ export const projectStore = createPartialStore({ ), }, + PARSE_PROJECT_FILE: { + async action({ dispatch, getters }, { projectJson }) { + const projectData = JSON.parse(projectJson); + + const characterInfos = getters.USER_ORDERED_CHARACTER_INFOS("talk"); + if (characterInfos == undefined) + throw new Error("characterInfos == undefined"); + + const parsedProjectData = await migrateProjectFileObject(projectData, { + fetchMoraData: (payload) => dispatch("FETCH_MORA_DATA", payload), + voices: characterInfos.flatMap((characterInfo) => + characterInfo.metas.styles.map((style) => ({ + engineId: style.engineId, + speakerId: characterInfo.metas.speakerUuid, + styleId: style.styleId, + })), + ), + }); + + return parsedProjectData; + }, + }, + LOAD_PROJECT_FILE: { /** * プロジェクトファイルを読み込む。読み込めたかの成否が返る。 @@ -137,7 +160,7 @@ export const projectStore = createPartialStore({ */ action: createUILockAction( async ( - context, + { dispatch, commit, getters }, { filePath, confirm }: { filePath?: string; confirm?: boolean }, ) => { if (!filePath) { @@ -157,58 +180,31 @@ export const projectStore = createPartialStore({ .readFile({ filePath }) .then(getValueOrThrow); - await context.dispatch("APPEND_RECENTLY_USED_PROJECT", { + await dispatch("APPEND_RECENTLY_USED_PROJECT", { filePath, }); const text = new TextDecoder("utf-8").decode(buf).trim(); - const projectData = JSON.parse(text); - - const characterInfos = - context.getters.USER_ORDERED_CHARACTER_INFOS("talk"); - if (characterInfos == undefined) - throw new Error("characterInfos == undefined"); - - const parsedProjectData = await migrateProjectFileObject( - projectData, - { - fetchMoraData: (payload) => - context.dispatch("FETCH_MORA_DATA", payload), - voices: characterInfos.flatMap((characterInfo) => - characterInfo.metas.styles.map((style) => ({ - engineId: style.engineId, - speakerId: characterInfo.metas.speakerUuid, - styleId: style.styleId, - })), - ), - }, - ); + const parsedProjectData = await dispatch("PARSE_PROJECT_FILE", { + projectJson: text, + }); - if (confirm !== false && context.getters.IS_EDITED) { - const result = await context.dispatch( - "SAVE_OR_DISCARD_PROJECT_FILE", - { - additionalMessage: - "プロジェクトをロードすると現在のプロジェクトは破棄されます。", - }, - ); + if (confirm !== false && getters.IS_EDITED) { + const result = await dispatch("SAVE_OR_DISCARD_PROJECT_FILE", { + additionalMessage: + "プロジェクトをロードすると現在のプロジェクトは破棄されます。", + }); if (result == "canceled") { return false; } } - await applyTalkProjectToStore( - context.dispatch, - parsedProjectData.talk, - ); - await applySongProjectToStore( - context.dispatch, - parsedProjectData.song, - ); + await applyTalkProjectToStore(dispatch, parsedProjectData.talk); + await applySongProjectToStore(dispatch, parsedProjectData.song); - context.commit("SET_PROJECT_FILEPATH", { filePath }); - context.commit("SET_SAVED_LAST_COMMAND_UNIX_MILLISEC", null); - context.commit("CLEAR_COMMANDS"); + commit("SET_PROJECT_FILEPATH", { filePath }); + commit("SET_SAVED_LAST_COMMAND_UNIX_MILLISEC", null); + commit("CLEAR_COMMANDS"); return true; } catch (err) { window.backend.logError(err); diff --git a/src/store/singing.ts b/src/store/singing.ts index 446e43692b..af159545d9 100644 --- a/src/store/singing.ts +++ b/src/store/singing.ts @@ -24,7 +24,6 @@ import { } from "./type"; import { sanitizeFileName } from "./utility"; import { EngineId, NoteId, StyleId } from "@/type/preload"; -import { Midi } from "@/sing/midi"; import { FrameAudioQuery, Note as NoteForRequestToEngine } from "@/openapi"; import { ResultError, getValueOrThrow } from "@/type/result"; import { @@ -43,7 +42,6 @@ import { } from "@/sing/audioRendering"; import { selectPriorPhrase, - getMeasureDuration, getNoteDuration, isValidNote, isValidSnapType, @@ -79,7 +77,6 @@ import { removeNotesFromOverlappingNoteInfos, updateNotesOfOverlappingNoteInfos, } from "@/sing/storeHelper"; -import { getDoremiFromNoteNumber } from "@/sing/viewHelper"; import { AnimationTimer, createPromiseThatResolvesWhen, @@ -90,6 +87,7 @@ import { getWorkaroundKeyRangeAdjustment } from "@/sing/workaroundKeyRangeAdjust import { createLogger } from "@/domain/frontend/log"; import { noteSchema } from "@/domain/project/schema"; import { getOrThrow } from "@/helpers/mapHelper"; +import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; const logger = createLogger("store/singing"); @@ -1692,140 +1690,21 @@ export const singingStore = createPartialStore({ }), }, - IMPORT_MIDI_FILE: { + // TODO: Undoできるようにする + IMPORT_UTAFORMATIX_PROJECT: { action: createUILockAction( - async ( - { state, dispatch }, - { filePath, trackIndex = 0 }: { filePath: string; trackIndex: number }, - ) => { - const convertPosition = ( - position: number, - sourceTpqn: number, - targetTpqn: number, - ) => { - return Math.round(position * (targetTpqn / sourceTpqn)); - }; - - const convertDuration = ( - startPosition: number, - endPosition: number, - sourceTpqn: number, - targetTpqn: number, - ) => { - const convertedEndPosition = convertPosition( - endPosition, - sourceTpqn, - targetTpqn, - ); - const convertedStartPosition = convertPosition( - startPosition, - sourceTpqn, - targetTpqn, - ); - return Math.max(1, convertedEndPosition - convertedStartPosition); - }; + async ({ state, commit, dispatch }, { project, trackIndex = 0 }) => { + const { tempos, timeSignatures, tracks, tpqn } = + ufProjectToVoicevox(project); - const getTopNotes = (notes: Note[]) => { - const topNotes: Note[] = []; - for (const note of notes) { - if (topNotes.length === 0) { - topNotes.push(note); - continue; - } - const topNote = topNotes[topNotes.length - 1]; - const topNoteEndPos = topNote.position + topNote.duration; - if (topNoteEndPos <= note.position) { - topNotes.push(note); - continue; - } - if (topNote.noteNumber < note.noteNumber) { - topNotes.pop(); - topNotes.push(note); - } - } - return topNotes; - }; - - const removeDuplicateTempos = (tempos: Tempo[]) => { - return tempos.filter((value, index, array) => { - return ( - index === array.length - 1 || - value.position !== array[index + 1].position - ); - }); - }; - - const removeDuplicateTimeSignatures = ( - timeSignatures: TimeSignature[], - ) => { - return timeSignatures.filter((value, index, array) => { - return ( - index === array.length - 1 || - value.measureNumber !== array[index + 1].measureNumber - ); - }); - }; - - // NOTE: トラック選択のために一度ファイルを読み込んでいるので、Midiを渡すなどでもよさそう - const midiData = getValueOrThrow( - await window.backend.readFile({ filePath }), - ); - const midi = new Midi(midiData); - const midiTpqn = midi.ticksPerBeat; - const midiTempos = midi.tempos; - const midiTimeSignatures = midi.timeSignatures; + const notes = tracks[trackIndex].notes; - const midiNotes = midi.tracks[trackIndex].notes; - - midiNotes.sort((a, b) => a.ticks - b.ticks); - - const tpqn = DEFAULT_TPQN; - - let notes = midiNotes.map((value): Note => { - return { - id: NoteId(crypto.randomUUID()), - position: convertPosition(value.ticks, midiTpqn, tpqn), - duration: convertDuration( - value.ticks, - value.ticks + value.duration, - midiTpqn, - tpqn, - ), - noteNumber: value.noteNumber, - lyric: value.lyric || getDoremiFromNoteNumber(value.noteNumber), - }; - }); - // ノートの重なりを考慮して、一番音が高いノート(トップノート)のみインポートする - notes = getTopNotes(notes); - - let tempos = midiTempos.map((value): Tempo => { - return { - position: convertPosition(value.ticks, midiTpqn, tpqn), - bpm: round(value.bpm, 2), - }; - }); - tempos.unshift(createDefaultTempo(0)); - tempos = removeDuplicateTempos(tempos); - - let timeSignatures: TimeSignature[] = []; - let tsPosition = 0; - let measureNumber = 1; - for (let i = 0; i < midiTimeSignatures.length; i++) { - const midiTs = midiTimeSignatures[i]; - const beats = midiTs.numerator; - const beatType = midiTs.denominator; - timeSignatures.push({ measureNumber, beats, beatType }); - if (i < midiTimeSignatures.length - 1) { - const nextTsTicks = midiTimeSignatures[i + 1].ticks; - const nextTsPos = convertPosition(nextTsTicks, midiTpqn, tpqn); - const tsDuration = nextTsPos - tsPosition; - const measureDuration = getMeasureDuration(beats, beatType, tpqn); - tsPosition = nextTsPos; - measureNumber += tsDuration / measureDuration; - } + if (tempos.length > 1) { + logger.warn("Multiple tempos are not supported."); + } + if (timeSignatures.length > 1) { + logger.warn("Multiple time signatures are not supported."); } - timeSignatures.unshift(createDefaultTimeSignature(1)); - timeSignatures = removeDuplicateTimeSignatures(timeSignatures); tempos.splice(1, tempos.length - 1); // TODO: 複数テンポに対応したら削除 timeSignatures.splice(1, timeSignatures.length - 1); // TODO: 複数拍子に対応したら削除 @@ -1833,432 +1712,59 @@ export const singingStore = createPartialStore({ if (tpqn !== state.tpqn) { throw new Error("TPQN does not match. Must be converted."); } + + // TODO: ここら辺のSET系の処理をまとめる + await dispatch("SET_TPQN", { tpqn }); await dispatch("SET_TEMPOS", { tempos }); await dispatch("SET_TIME_SIGNATURES", { timeSignatures }); await dispatch("SET_NOTES", { notes }); + + commit("SET_SAVED_LAST_COMMAND_UNIX_MILLISEC", null); + commit("CLEAR_COMMANDS"); + dispatch("RENDER"); }, ), }, - IMPORT_MUSICXML_FILE: { + // TODO: Undoできるようにする + IMPORT_VOICEVOX_PROJECT: { action: createUILockAction( - async ({ state, dispatch }, { filePath }: { filePath?: string }) => { - if (!filePath) { - filePath = await window.backend.showImportFileDialog({ - title: "MusicXML読み込み", - name: "MusicXML", - extensions: ["musicxml", "xml"], - }); - if (!filePath) return; - } + async ({ state, commit, dispatch }, { project, trackIndex = 0 }) => { + const { tempos, timeSignatures, tracks, tpqn } = project.song; - let xmlStr = new TextDecoder("utf-8").decode( - getValueOrThrow(await window.backend.readFile({ filePath })), - ); - if (xmlStr.indexOf("\ufffd") > -1) { - xmlStr = new TextDecoder("shift-jis").decode( - getValueOrThrow(await window.backend.readFile({ filePath })), - ); - } - - const tpqn = DEFAULT_TPQN; - const tempos = [createDefaultTempo(0)]; - const timeSignatures = [createDefaultTimeSignature(1)]; - const notes: Note[] = []; - - let divisions = 1; - let position = 0; - let measureNumber = 1; - let measurePosition = 0; - let measureDuration = getMeasureDuration( - timeSignatures[0].beats, - timeSignatures[0].beatType, - tpqn, - ); - let tieStartNote: Note | undefined; - - const getChild = (element: Element | undefined, tagName: string) => { - if (element) { - for (const childElement of element.children) { - if (childElement.tagName === tagName) { - return childElement; - } - } - } - return undefined; - }; - - const getValueAsNumber = (element: Element) => { - const value = Number(element.textContent); - if (Number.isNaN(value)) { - throw new Error("The value is invalid."); - } - return value; - }; - - const getAttributeAsNumber = ( - element: Element, - qualifiedName: string, - ) => { - const value = Number(element.getAttribute(qualifiedName)); - if (Number.isNaN(value)) { - throw new Error("The value is invalid."); - } - return value; - }; - - const getStepNumber = (stepElement: Element) => { - const stepNumberDict: { [key: string]: number } = { - C: 0, - D: 2, - E: 4, - F: 5, - G: 7, - A: 9, - B: 11, - }; - const stepChar = stepElement.textContent; - if (stepChar == null) { - throw new Error("The value is invalid."); - } - return stepNumberDict[stepChar]; - }; - - const getDuration = (durationElement: Element) => { - const duration = getValueAsNumber(durationElement); - return Math.round((tpqn * duration) / divisions); - }; - - const getTie = (elementThatMayBeTied: Element) => { - let tie: boolean | undefined; - for (const childElement of elementThatMayBeTied.children) { - if ( - childElement.tagName === "tie" || - childElement.tagName === "tied" - ) { - const tieType = childElement.getAttribute("type"); - if (tieType === "start") { - tie = true; - } else if (tieType === "stop") { - tie = false; - } else { - throw new Error("The value is invalid."); - } - } - } - return tie; - }; - - const parseSound = (soundElement: Element) => { - if (!soundElement.hasAttribute("tempo")) { - return; - } - if (tempos.length !== 0) { - const lastTempo = tempos[tempos.length - 1]; - if (lastTempo.position === position) { - tempos.pop(); - } - } - const tempo = getAttributeAsNumber(soundElement, "tempo"); - tempos.push({ - position: position, - bpm: round(tempo, 2), - }); - }; - - const parseDirection = (directionElement: Element) => { - for (const childElement of directionElement.children) { - if (childElement.tagName === "sound") { - parseSound(childElement); - } - } - }; - - const parseDivisions = (divisionsElement: Element) => { - divisions = getValueAsNumber(divisionsElement); - }; - - const parseTime = (timeElement: Element) => { - const beatsElement = getChild(timeElement, "beats"); - if (!beatsElement) { - throw new Error("beats element does not exist."); - } - const beatTypeElement = getChild(timeElement, "beat-type"); - if (!beatTypeElement) { - throw new Error("beat-type element does not exist."); - } - const beats = getValueAsNumber(beatsElement); - const beatType = getValueAsNumber(beatTypeElement); - measureDuration = getMeasureDuration(beats, beatType, tpqn); - if (timeSignatures.length !== 0) { - const lastTimeSignature = timeSignatures[timeSignatures.length - 1]; - if (lastTimeSignature.measureNumber === measureNumber) { - timeSignatures.pop(); - } - } - timeSignatures.push({ - measureNumber, - beats, - beatType, - }); - }; - - const parseAttributes = (attributesElement: Element) => { - for (const childElement of attributesElement.children) { - if (childElement.tagName === "divisions") { - parseDivisions(childElement); - } else if (childElement.tagName === "time") { - parseTime(childElement); - } else if (childElement.tagName === "sound") { - parseSound(childElement); - } - } - }; - - const parseNote = (noteElement: Element) => { - // TODO: ノートの重なり・和音を考慮していないので、 - // それらが存在する場合でも読み込めるようにする - - const durationElement = getChild(noteElement, "duration"); - if (!durationElement) { - throw new Error("duration element does not exist."); - } - let duration = getDuration(durationElement); - let noteEnd = position + duration; - const measureEnd = measurePosition + measureDuration; - if (noteEnd > measureEnd) { - // 小節に収まらない場合、ノートの長さを変えて小節に収まるようにする - duration = measureEnd - position; - noteEnd = position + duration; - } - - if (getChild(noteElement, "rest")) { - position += duration; - return; - } - - const pitchElement = getChild(noteElement, "pitch"); - if (!pitchElement) { - throw new Error("pitch element does not exist."); - } - const octaveElement = getChild(pitchElement, "octave"); - if (!octaveElement) { - throw new Error("octave element does not exist."); - } - const stepElement = getChild(pitchElement, "step"); - if (!stepElement) { - throw new Error("step element does not exist."); - } - const alterElement = getChild(pitchElement, "alter"); - - const octave = getValueAsNumber(octaveElement); - const stepNumber = getStepNumber(stepElement); - let noteNumber = 12 * (octave + 1) + stepNumber; - if (alterElement) { - noteNumber += getValueAsNumber(alterElement); - } - - const lyricElement = getChild(noteElement, "lyric"); - let lyric = getChild(lyricElement, "text")?.textContent ?? ""; - lyric = lyric.trim(); - - let tie = getTie(noteElement); - for (const childElement of noteElement.children) { - if (childElement.tagName === "notations") { - tie = getTie(childElement); - } - } - - const note: Note = { - id: NoteId(crypto.randomUUID()), - position, - duration, - noteNumber, - lyric, - }; - - if (tieStartNote) { - if (tie === false) { - tieStartNote.duration = noteEnd - tieStartNote.position; - notes.push(tieStartNote); - tieStartNote = undefined; - } - } else { - if (tie === true) { - tieStartNote = note; - } else { - notes.push(note); - } - } - position += duration; - }; - - const parseMeasure = (measureElement: Element) => { - measurePosition = position; - measureNumber = getAttributeAsNumber(measureElement, "number"); - for (const childElement of measureElement.children) { - if (childElement.tagName === "direction") { - parseDirection(childElement); - } else if (childElement.tagName === "sound") { - parseSound(childElement); - } else if (childElement.tagName === "attributes") { - parseAttributes(childElement); - } else if (childElement.tagName === "note") { - if (position < measurePosition + measureDuration) { - parseNote(childElement); - } - } - } - const measureEnd = measurePosition + measureDuration; - if (position !== measureEnd) { - tieStartNote = undefined; - position = measureEnd; - } - }; - - const parsePart = (partElement: Element) => { - for (const childElement of partElement.children) { - if (childElement.tagName === "measure") { - parseMeasure(childElement); - } - } - }; - - const parseMusicXml = (xmlStr: string) => { - const parser = new DOMParser(); - const dom = parser.parseFromString(xmlStr, "application/xml"); - const partElements = dom.getElementsByTagName("part"); - if (partElements.length === 0) { - throw new Error("part element does not exist."); - } - // TODO: UIで読み込むパートを選択できるようにする - parsePart(partElements[0]); - }; - - parseMusicXml(xmlStr); - - tempos.splice(1, tempos.length - 1); // TODO: 複数テンポに対応したら削除 - timeSignatures.splice(1, timeSignatures.length - 1); // TODO: 複数拍子に対応したら削除 + const track = tracks[trackIndex]; + const notes = track.notes.map((note) => ({ + ...note, + id: NoteId(crypto.randomUUID()), + })); if (tpqn !== state.tpqn) { throw new Error("TPQN does not match. Must be converted."); } - await dispatch("SET_TEMPOS", { tempos }); - await dispatch("SET_TIME_SIGNATURES", { timeSignatures }); - await dispatch("SET_NOTES", { notes }); - }, - ), - }, - - IMPORT_UST_FILE: { - action: createUILockAction( - async ({ state, dispatch }, { filePath }: { filePath?: string }) => { - // USTファイルの読み込み - if (!filePath) { - filePath = await window.backend.showImportFileDialog({ - title: "UST読み込み", - name: "UST", - extensions: ["ust"], - }); - if (!filePath) return; - } - // ファイルの読み込み - const fileData = getValueOrThrow( - await window.backend.readFile({ filePath }), - ); - - // ファイルフォーマットに応じてエンコーディングを変える - // UTF-8とShiftJISの2種類に対応 - let ustData; - try { - ustData = new TextDecoder("utf-8").decode(fileData); - // ShiftJISの場合はShiftJISでデコードし直す - if (ustData.includes("\ufffd")) { - ustData = new TextDecoder("shift-jis").decode(fileData); - } - } catch (error) { - throw new Error("Failed to decode UST file.", { cause: error }); - } - if (!ustData || typeof ustData !== "string") { - throw new Error("Failed to decode UST file."); - } - // 初期化 - const tpqn = DEFAULT_TPQN; - const tempos = [createDefaultTempo(0)]; - const timeSignatures = [createDefaultTimeSignature(1)]; - const notes: Note[] = []; - - // USTファイルのセクションをパース - const parseSection = (section: string): { [key: string]: string } => { - const sectionNameMatch = section.match(/\[(.+)\]/); - if (!sectionNameMatch) { - throw new Error("UST section name not found"); - } - const params = section.split(/[\r\n]+/).reduce( - (acc, line) => { - const [key, value] = line.split("="); - if (key && value) { - acc[key] = value; - } - return acc; - }, - {} as { [key: string]: string }, - ); - return { - ...params, - sectionName: sectionNameMatch[1], - }; - }; - - // セクションを分割 - const sections = ustData.split(/^(?=\[)/m); - // ポジション - let position = 0; - // セクションごとに処理 - sections.forEach((section) => { - const params = parseSection(section); - // SETTINGセクション - if (params.sectionName === "#SETTING") { - const tempo = Number(params["Tempo"]); - if (tempo) tempos[0].bpm = tempo; - } - // ノートセクション - // #以降に数字の場合はノートセクション ex: #0, #0000 - if (params.sectionName.match(/^#\d+$/)) { - // テンポ変更があれば追加 - const tempo = Number(params["Tempo"]); - if (tempo) tempos.push({ position, bpm: tempo }); - const noteNumber = Number(params["NoteNum"]); - const duration = Number(params["Length"]); - let lyric = params["Lyric"].trim(); - // 歌詞の前に連続音が含まれている場合は除去 - if (lyric.includes(" ")) { - lyric = lyric.split(" ")[1]; - } - // 休符であればポジションを進めるのみ - if (lyric === "R") { - position += duration; - } else { - // それ以外の場合はノートを追加 - notes.push({ - id: NoteId(crypto.randomUUID()), - position, - duration, - noteNumber, - lyric, - }); - position += duration; - } - } + // TODO: ここら辺のSET系の処理をまとめる + await dispatch("SET_SINGER", { + singer: track.singer, }); - - if (tpqn !== state.tpqn) { - throw new Error("TPQN does not match. Must be converted."); - } + await dispatch("SET_KEY_RANGE_ADJUSTMENT", { + keyRangeAdjustment: track.keyRangeAdjustment, + }); + await dispatch("SET_VOLUME_RANGE_ADJUSTMENT", { + volumeRangeAdjustment: track.volumeRangeAdjustment, + }); + await dispatch("SET_TPQN", { tpqn }); await dispatch("SET_TEMPOS", { tempos }); await dispatch("SET_TIME_SIGNATURES", { timeSignatures }); await dispatch("SET_NOTES", { notes }); + await dispatch("CLEAR_PITCH_EDIT_DATA"); // FIXME: SET_PITCH_EDIT_DATAがセッターになれば不要 + await dispatch("SET_PITCH_EDIT_DATA", { + data: track.pitchEditData, + startFrame: 0, + }); + + commit("SET_SAVED_LAST_COMMAND_UNIX_MILLISEC", null); + commit("CLEAR_COMMANDS"); + dispatch("RENDER"); }, ), }, diff --git a/src/store/type.ts b/src/store/type.ts index a5873a13b5..f926511550 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1,5 +1,6 @@ import { Patch } from "immer"; import { z } from "zod"; +import { Project as UfProject } from "@sevenc-nanashi/utaformatix-ts"; import { MutationTree, MutationsBase, @@ -61,6 +62,7 @@ import { } from "@/components/Dialog/Dialog"; import { OverlappingNoteInfos } from "@/sing/storeHelper"; import { + LatestProjectType, noteSchema, singerSchema, tempoSchema, @@ -1016,16 +1018,12 @@ export type SingingStoreTypes = { action(payload: { isDrag: boolean }): void; }; - IMPORT_MIDI_FILE: { - action(payload: { filePath: string; trackIndex: number }): void; + IMPORT_UTAFORMATIX_PROJECT: { + action(payload: { project: UfProject; trackIndex: number }): void; }; - IMPORT_MUSICXML_FILE: { - action(payload: { filePath?: string }): void; - }; - - IMPORT_UST_FILE: { - action(payload: { filePath?: string }): void; + IMPORT_VOICEVOX_PROJECT: { + action(payload: { project: LatestProjectType; trackIndex: number }): void; }; EXPORT_WAVE_FILE: { @@ -1489,6 +1487,10 @@ export type ProjectStoreTypes = { action(payload: { confirm?: boolean }): void; }; + PARSE_PROJECT_FILE: { + action(payload: { projectJson: string }): Promise; + }; + LOAD_PROJECT_FILE: { action(payload: { filePath?: string; confirm?: boolean }): boolean; }; @@ -1642,7 +1644,7 @@ export type UiStoreState = { isDictionaryManageDialogOpen: boolean; isEngineManageDialogOpen: boolean; isUpdateNotificationDialogOpen: boolean; - isImportMidiDialogOpen: boolean; + isImportSongProjectDialogOpen: boolean; isMaximized: boolean; isPinned: boolean; isFullscreen: boolean; @@ -1714,7 +1716,7 @@ export type UiStoreTypes = { isCharacterOrderDialogOpen?: boolean; isEngineManageDialogOpen?: boolean; isUpdateNotificationDialogOpen?: boolean; - isImportMidiDialogOpen?: boolean; + isImportExternalProjectDialogOpen?: boolean; }; action(payload: { isDefaultStyleSelectDialogOpen?: boolean; @@ -1728,7 +1730,7 @@ export type UiStoreTypes = { isCharacterOrderDialogOpen?: boolean; isEngineManageDialogOpen?: boolean; isUpdateNotificationDialogOpen?: boolean; - isImportMidiDialogOpen?: boolean; + isImportSongProjectDialogOpen?: boolean; }): void; }; diff --git a/src/store/ui.ts b/src/store/ui.ts index d83d9974db..59ac754d6d 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -66,7 +66,7 @@ export const uiStoreState: UiStoreState = { isDictionaryManageDialogOpen: false, isEngineManageDialogOpen: false, isUpdateNotificationDialogOpen: false, - isImportMidiDialogOpen: false, + isImportSongProjectDialogOpen: false, isMaximized: false, isPinned: false, isFullscreen: false, @@ -186,7 +186,7 @@ export const uiStore = createPartialStore({ isCharacterOrderDialogOpen?: boolean; isEngineManageDialogOpen?: boolean; isUpdateNotificationDialogOpen?: boolean; - isImportMidiDialogOpen?: boolean; + isImportExternalProjectDialogOpen?: boolean; }, ) { for (const [key, value] of Object.entries(dialogState)) { diff --git a/src/styles/_index.scss b/src/styles/_index.scss index 02912d5603..f598a5995a 100644 --- a/src/styles/_index.scss +++ b/src/styles/_index.scss @@ -1,5 +1,5 @@ -@use './variables' as vars; -@use './colors' as colors; +@use "./variables" as vars; +@use "./colors" as colors; @import "./fonts"; // 優先度を強引に上げる @@ -20,6 +20,14 @@ img { pointer-events: none; } +// detailsタグのスタイル +details { + summary { + display: list-item; + cursor: pointer; + } +} + // スクロールバーのデザイン ::-webkit-scrollbar { width: 12px; diff --git a/src/type/preload.ts b/src/type/preload.ts index 67b32c34cb..01a3bdecb7 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -7,25 +7,28 @@ export const isProduction = import.meta.env.MODE === "production"; export const isElectron = import.meta.env.VITE_TARGET === "electron"; export const isBrowser = import.meta.env.VITE_TARGET === "browser"; -// electronのメイン・レンダラープロセス内、ブラウザ内どこでも使用可能なmacOS判定 -function checkIsMac(): boolean { - let isMac: boolean | undefined = undefined; +// electronのメイン・レンダラープロセス内、ブラウザ内どこでも使用可能なOS判定 +function checkOs(os: "windows" | "mac"): boolean { + let isSpecifiedOs: boolean | undefined = undefined; if (process?.platform) { // electronのメインプロセス用 - isMac = process.platform === "darwin"; + isSpecifiedOs = + process.platform === (os === "windows" ? "win32" : "darwin"); } else if (navigator?.userAgentData) { // electronのレンダラープロセス用、Chrome系統が実装する実験的機能 - isMac = navigator.userAgentData.platform.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.userAgentData.platform.toLowerCase().includes(os); } else if (navigator?.platform) { // ブラウザ用、非推奨機能 - isMac = navigator.platform.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.platform.toLowerCase().includes(os); } else { // ブラウザ用、不正確 - isMac = navigator.userAgent.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.userAgent.toLowerCase().includes(os); } - return isMac; + return isSpecifiedOs; } -export const isMac = checkIsMac(); + +export const isMac = checkOs("mac"); +export const isWindows = checkOs("windows"); const urlStringSchema = z.string().url().brand("URL"); export type UrlString = z.infer; diff --git a/tests/unit/lib/midi/midi.spec.ts b/tests/unit/lib/midi/midi.spec.ts deleted file mode 100644 index 10df1ee4b3..0000000000 --- a/tests/unit/lib/midi/midi.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -// @vitest-environment node - -import { promises as fs } from "fs"; -import { it, expect } from "vitest"; -import { Midi } from "@/sing/midi"; - -// MIDIファイルの作成情報: -// - synthv.mid:SynthVで作成(Synthesizer V Studio Pro 1.11.0、プロジェクトファイルは https://github.com/VOICEVOX/voicevox/pull/1982 を参照) -// - timeSig.mid、bpm.mid:signalで作成(https://signal.vercel.app/edit) - -const midiRoot = "tests/unit/lib/midi/"; - -it("BPMをパースできる", async () => { - const bpmMid = await fs.readFile(midiRoot + "bpm.mid"); - const midi = new Midi(bpmMid); - const ticksPerBeat = midi.ticksPerBeat; - expect(midi.tempos).toEqual([ - { ticks: 0, bpm: 120 }, - { ticks: ticksPerBeat * 4, bpm: 180 }, - { ticks: ticksPerBeat * 8, bpm: 240 }, - ]); -}); - -const lyricExpectation: [noteNumber: number, lyric: string][] = [ - [60, "ど"], - [62, "れ"], - [64, "み"], - [65, "ふぁ"], - [67, "そ"], - [69, "ら"], - [71, "し"], - [72, "ど"], -]; - -it("SynthVのノートと歌詞をパースできる", async () => { - const synthvMid = await fs.readFile(midiRoot + "synthv.mid"); - const midi = new Midi(synthvMid); - const ticksPerBeat = midi.ticksPerBeat; - // SynthVのMIDIファイルの1トラック目はBPM情報のみなので、2トラック目を取得 - expect(midi.tracks[1].notes).toEqual( - lyricExpectation.map(([noteNumber, lyric], index) => ({ - ticks: index * ticksPerBeat, - noteNumber, - duration: ticksPerBeat, - lyric, - })), - ); -}); - -it("拍子をパースできる", async () => { - const timeSigMid = await fs.readFile(midiRoot + "timeSig.mid"); - const midi = new Midi(timeSigMid); - const ticksPerBeat = midi.ticksPerBeat; - expect(midi.timeSignatures).toEqual([ - { ticks: 0, numerator: 4, denominator: 4 }, - { ticks: ticksPerBeat * 4, numerator: 3, denominator: 4 }, - { ticks: ticksPerBeat * 7, numerator: 4, denominator: 8 }, - ]); -}); diff --git a/tests/unit/lib/utaformatixProject/__snapshots__/export.spec.ts.snap b/tests/unit/lib/utaformatixProject/__snapshots__/export.spec.ts.snap new file mode 100644 index 0000000000..f15a3be488 --- /dev/null +++ b/tests/unit/lib/utaformatixProject/__snapshots__/export.spec.ts.snap @@ -0,0 +1,37 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`トラックを変換できる 1`] = ` +{ + "formatVersion": 1, + "project": { + "measurePrefix": 0, + "name": "test", + "tempos": [ + { + "bpm": 120, + "tickPosition": 0, + }, + ], + "timeSignatures": [ + { + "denominator": 4, + "measurePosition": 1, + "numerator": 4, + }, + ], + "tracks": [ + { + "name": "無名トラック", + "notes": [ + { + "key": 60, + "lyric": "ど", + "tickOff": 480, + "tickOn": 0, + }, + ], + }, + ], + }, +} +`; diff --git a/tests/unit/lib/midi/bpm.mid b/tests/unit/lib/utaformatixProject/bpm.mid similarity index 100% rename from tests/unit/lib/midi/bpm.mid rename to tests/unit/lib/utaformatixProject/bpm.mid diff --git a/tests/unit/lib/utaformatixProject/export.spec.ts b/tests/unit/lib/utaformatixProject/export.spec.ts new file mode 100644 index 0000000000..1fad3469b5 --- /dev/null +++ b/tests/unit/lib/utaformatixProject/export.spec.ts @@ -0,0 +1,35 @@ +import { it, expect } from "vitest"; +import { ufProjectFromVoicevox } from "@/sing/utaformatixProject/fromVoicevox"; +import { + createDefaultTempo, + createDefaultTimeSignature, + createDefaultTrack, +} from "@/sing/domain"; +import { NoteId } from "@/type/preload"; + +const createNoteId = () => NoteId(crypto.randomUUID()); + +it("トラックを変換できる", async () => { + const track = createDefaultTrack(); + track.notes.push({ + id: createNoteId(), + noteNumber: 60, + position: 0, + duration: 480, + lyric: "ど", + }); + + const project = ufProjectFromVoicevox( + { + tracks: [track], + tpqn: 480, + tempos: [createDefaultTempo(0)], + timeSignatures: [createDefaultTimeSignature(1)], + }, + "test", + ); + + const ufData = project.toUfDataObject(); + + expect(ufData).toMatchSnapshot(); +}); diff --git a/tests/unit/lib/utaformatixProject/import.spec.ts b/tests/unit/lib/utaformatixProject/import.spec.ts new file mode 100644 index 0000000000..ea9632464e --- /dev/null +++ b/tests/unit/lib/utaformatixProject/import.spec.ts @@ -0,0 +1,61 @@ +// @vitest-environment node + +import { promises as fs } from "fs"; +import { it, expect } from "vitest"; +import { Project as UfProject } from "@sevenc-nanashi/utaformatix-ts"; +import { ufProjectToVoicevox } from "@/sing/utaformatixProject/toVoicevox"; + +// MIDIファイルの作成情報: +// - synthv.mid:SynthVで作成(Synthesizer V Studio Pro 1.11.0、プロジェクトファイルは https://github.com/VOICEVOX/voicevox/pull/1982 を参照) +// - timeSig.mid、bpm.mid:signalで作成(https://signal.vercel.app/edit) + +const midiRoot = "tests/unit/lib/utaformatixProject/"; + +const convertMidi = async (filename: string) => { + const midi = await fs.readFile(midiRoot + filename); + const project = await UfProject.fromStandardMid(midi); + return ufProjectToVoicevox(project); +}; + +it("BPMを変換できる", async () => { + const state = await convertMidi("bpm.mid"); + const tpqn = state.tpqn; + expect(state.tempos).toEqual([ + { position: 0, bpm: 120 }, + { position: tpqn * 4, bpm: 180 }, + { position: tpqn * 8, bpm: 240 }, + ]); +}); + +const lyricExpectation: [noteNumber: number, lyric: string][] = [ + [60, "ど"], + [62, "れ"], + [64, "み"], + [65, "ふぁ"], + [67, "そ"], + [69, "ら"], + [71, "し"], + [72, "ど"], +]; + +it("SynthVのノートと歌詞を変換できる", async () => { + const state = await convertMidi("synthv.mid"); + expect(state.tracks[0].notes).toMatchObject( + lyricExpectation.map(([noteNumber, lyric], index) => ({ + // id: string, + noteNumber, + position: index * state.tpqn, + duration: state.tpqn, + lyric, + })), + ); +}); + +it("拍子を変換できる", async () => { + const state = await convertMidi("timeSig.mid"); + expect(state.timeSignatures).toEqual([ + { measureNumber: 1, beats: 4, beatType: 4 }, + { measureNumber: 2, beats: 3, beatType: 4 }, + { measureNumber: 3, beats: 4, beatType: 8 }, + ]); +}); diff --git a/tests/unit/lib/midi/synthv.mid b/tests/unit/lib/utaformatixProject/synthv.mid similarity index 100% rename from tests/unit/lib/midi/synthv.mid rename to tests/unit/lib/utaformatixProject/synthv.mid diff --git a/tests/unit/lib/midi/timeSig.mid b/tests/unit/lib/utaformatixProject/timeSig.mid similarity index 100% rename from tests/unit/lib/midi/timeSig.mid rename to tests/unit/lib/utaformatixProject/timeSig.mid