├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build_resources └── engine_manifest.json ├── deno.json ├── deno.lock ├── deps.ts ├── main.ts ├── providers ├── dict.ts ├── icon.png ├── index.ts ├── info.ts ├── noop.ts └── synthesis.ts └── store.ts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [macos-latest, windows-latest] 17 | include: 18 | - os: macos-latest 19 | name: mac 20 | - os: windows-latest 21 | name: win 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v3 26 | - name: Setup Deno 27 | uses: denoland/setup-deno@v1 28 | 29 | - name: Compile 30 | run: deno task compile 31 | - name: Pack 32 | shell: bash 33 | run: | 34 | cp coeiroink-v2-bridge.exe ./build_resources/ || true 35 | cp coeiroink-v2-bridge ./build_resources/ || true 36 | cp providers/icon.png ./build_resources/ 37 | cd build_resources 38 | 7z a -tzip coeiroink-bridge-${{ github.ref_name }}-${{ matrix.name }}.zip * 39 | mv coeiroink-bridge-${{ github.ref_name }}-${{ matrix.name }}.zip ../coeiroink-bridge-${{ github.ref_name }}-${{ matrix.name }}.vvpp 40 | 41 | - name: Create Release 42 | uses: softprops/action-gh-release@v1 43 | with: 44 | files: | 45 | coeiroink-bridge-${{ github.ref_name }}-${{ matrix.name }}.vvpp 46 | tag_name: ${{ github.ref_name }} 47 | draft: false 48 | prerelease: false 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/deno 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=deno 3 | 4 | ### Deno ### 5 | /.idea/ 6 | /.vscode/ 7 | 8 | /node_modules 9 | 10 | .env 11 | *.orig 12 | *.pyc 13 | *.swp 14 | 15 | # End of https://www.toptal.com/developers/gitignore/api/deno 16 | 17 | *.exe 18 | speakerMap.json 19 | *.vvpp 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sevenc-nanashi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COEIROINK v2 bridge 2 | 3 | COEIROINK v2 を VOICEVOX のマルチエンジンで読み込めるようにするためのブリッジ。 4 | 5 | > [!WARNING] 6 | > このブリッジの初回起動時は、COEIROINK v2 が起動している必要があります。 7 | > 2回目以降は、COEIROINK v2 が起動していなくても自動で起動します。 8 | 9 | ## 使い方 10 | 11 | 1. [Releases](https://github.com/sevenc-nanashi/coeiroink-v2-bridge/releases) から最新の `coeiroink-v2-v0.0.0.vvpp` をダウンロードする 12 | 2. vvpp を VOICEVOX に読み込ませる 13 | 3. COEIROINK v2 を起動する 14 | 15 | ## TODO 16 | 17 | - [ ] v2のピッチ調整機能(中心を0として上下で制御する?) 18 | 19 | ## 開発 20 | 21 | ### 実行 22 | 23 | ``` 24 | deno task start 25 | 26 | # 監視モード 27 | deno task watch 28 | ``` 29 | 30 | ### ビルド 31 | 32 | ``` 33 | deno task compile 34 | ``` 35 | 36 | ## ライセンス 37 | 38 | MIT License で公開しています。 39 | -------------------------------------------------------------------------------- /build_resources/engine_manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "COEIROINK v2 bridge", 3 | "uuid": "96755ba9-6c9d-4166-aaf3-86633dfa0ca5", 4 | "command": "coeiroink-v2-bridge", 5 | "port": 50132, 6 | "supported_features": {} 7 | } 8 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "watch": "deno run --watch --allow-net --allow-run --allow-env --allow-read --allow-write main.ts", 4 | "start": "deno run --allow-net --allow-run --allow-env --allow-read --allow-write main.ts", 5 | "compile": "deno compile --allow-net --allow-run --allow-env --allow-read --allow-write -o coeiroink-v2-bridge main.ts" 6 | }, 7 | "lock": false 8 | } 9 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.190.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", 5 | "https://deno.land/std@0.190.0/async/abortable.ts": "fd682fa46f3b7b16b4606a5ab52a7ce309434b76f820d3221bdfb862719a15d7", 6 | "https://deno.land/std@0.190.0/async/deadline.ts": "58f72a3cc0fcb731b2cc055ba046f4b5be3349ff6bf98f2e793c3b969354aab2", 7 | "https://deno.land/std@0.190.0/async/debounce.ts": "adab11d04ca38d699444ac8a9d9856b4155e8dda2afd07ce78276c01ea5a4332", 8 | "https://deno.land/std@0.190.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8", 9 | "https://deno.land/std@0.190.0/async/delay.ts": "73aa04cec034c84fc748c7be49bb15cac3dd43a57174bfdb7a4aec22c248f0dd", 10 | "https://deno.land/std@0.190.0/async/mod.ts": "f04344fa21738e5ad6bea37a6bfffd57c617c2d372bb9f9dcfd118a1b622e576", 11 | "https://deno.land/std@0.190.0/async/mux_async_iterator.ts": "70c7f2ee4e9466161350473ad61cac0b9f115cff4c552eaa7ef9d50c4cbb4cc9", 12 | "https://deno.land/std@0.190.0/async/pool.ts": "f1b8d3df4d7fd3c73f8cbc91cc2e8b8e950910f1eab94230b443944d7584c657", 13 | "https://deno.land/std@0.190.0/async/retry.ts": "c9248325ec08cc2cceb7618472e77589a51a25cee460e07b6096be34966c2ead", 14 | "https://deno.land/std@0.190.0/async/tee.ts": "47e42d35f622650b02234d43803d0383a89eb4387e1b83b5a40106d18ae36757", 15 | "https://deno.land/std@0.190.0/flags/mod.ts": "17f444ddbee43c5487568de0c6a076c7729cfe90d96d2ffcd2b8f8adadafb6e8", 16 | "https://deno.land/std@0.190.0/http/server.ts": "1b23463b5b36e4eebc495417f6af47a6f7d52e3294827a1226d2a1aab23d9d20", 17 | "https://deno.land/x/base64@v0.2.1/base.ts": "47dc8d68f07dc91524bdd6db36eccbe59cf4d935b5fc09f27357a3944bb3ff7b", 18 | "https://deno.land/x/base64@v0.2.1/mod.ts": "1cbdc4ba7229d3c6d1763fecdb9d46844777c7e153abb6dabea8b0dd01448db4", 19 | "https://deno.land/x/hono@v3.2.3/client/client.ts": "7a819af2aa9aabc746add4d01715fa37f83676fdf1b63fba1bbcd655b2d066a5", 20 | "https://deno.land/x/hono@v3.2.3/client/index.ts": "3ff4cf246f3543f827a85a2c84d66a025ac350ee927613629bda47e854bfb7ba", 21 | "https://deno.land/x/hono@v3.2.3/client/types.ts": "3b7b789e55ad01d67db0c2638054f2aa72fcc39fc2ef9fceaf902f854ce19381", 22 | "https://deno.land/x/hono@v3.2.3/client/utils.ts": "9f9825a3f96e0e332fa91f4cd1cfffe2ef9da796c58904941c8405e3f18bda07", 23 | "https://deno.land/x/hono@v3.2.3/compose.ts": "e55ed7be2134f363ff3c8e8e2f520ff682c6a11a47d7189100ed69704ce10b9e", 24 | "https://deno.land/x/hono@v3.2.3/context.ts": "04b3ec24fddce2438fa173d48c329c1ac30ab0d50d65f64da3019219abc0165f", 25 | "https://deno.land/x/hono@v3.2.3/hono-base.ts": "edbbd8453b6e39b9aafc0014cbc76f07014b053770c8a9022f38e9a29b55fcae", 26 | "https://deno.land/x/hono@v3.2.3/hono.ts": "c185fae30dc3aa9966b0f3e8599f81314ba5e8621b907d27cffb77414a1c0908", 27 | "https://deno.land/x/hono@v3.2.3/http-exception.ts": "be4736556518a6c923ed99573eea823d7b14e7f37dcca7d38015896426c1928a", 28 | "https://deno.land/x/hono@v3.2.3/mod.ts": "ec8ad69c08e40bee63cac3d9e3df5fd062e9f4692a81cecf43a51feb3421625e", 29 | "https://deno.land/x/hono@v3.2.3/request.ts": "c7b156bb5c16a9eaf59314836734e3b4f879dc5a53e0b0085660a711898ce319", 30 | "https://deno.land/x/hono@v3.2.3/router.ts": "b847c114347c19c222cd27cea25106b3bbea978370a026231563986987b7fa19", 31 | "https://deno.land/x/hono@v3.2.3/router/linear-router/index.ts": "8a2a7144c50b1f4a92d9ee99c2c396716af144c868e10608255f969695efccd0", 32 | "https://deno.land/x/hono@v3.2.3/router/linear-router/router.ts": "90d4afc052b72f53dafbcf97fd32f24299b985f8a35dbdc70b28048201b3dcbc", 33 | "https://deno.land/x/hono@v3.2.3/router/pattern-router/index.ts": "304a66c50e243872037ed41c7dd79ed0c89d815e17e172e7ad7cdc4bc07d3383", 34 | "https://deno.land/x/hono@v3.2.3/router/pattern-router/router.ts": "f3cb60f151b86aa339549cc6e84cfed19638fc995afd606fe6029e159dcec48f", 35 | "https://deno.land/x/hono@v3.2.3/router/reg-exp-router/index.ts": "52755829213941756159b7a963097bafde5cc4fc22b13b1c7c9184dc0512d1db", 36 | "https://deno.land/x/hono@v3.2.3/router/reg-exp-router/node.ts": "8006b5bccb83d9fc98e0562a5545f6dd0be639ce445b089a6171c9c617aa8693", 37 | "https://deno.land/x/hono@v3.2.3/router/reg-exp-router/router.ts": "ba1dc5cd84596ee811666ca5c72556f0624d16a94128752b090a302016720a40", 38 | "https://deno.land/x/hono@v3.2.3/router/reg-exp-router/trie.ts": "567493b301c44174f0895aedb8d055bbecf88f8a25626fa8ca61333bbd0c882c", 39 | "https://deno.land/x/hono@v3.2.3/router/smart-router/index.ts": "74f9b4fe15ea535900f2b9b048581915f12fe94e531dd2b0032f5610e61c3bef", 40 | "https://deno.land/x/hono@v3.2.3/router/smart-router/router.ts": "38209165dadea4182b42807a0a6c84a9258532a07ebf87262dc2b5b09d25a2a2", 41 | "https://deno.land/x/hono@v3.2.3/router/trie-router/index.ts": "3eb75e7f71ba81801631b30de6b1f5cefb2c7239c03797e2b2cbab5085911b41", 42 | "https://deno.land/x/hono@v3.2.3/router/trie-router/node.ts": "f01d0dfa2de4797e9984d28934c37dfe8b4801d72076ed70422abf46e92fd384", 43 | "https://deno.land/x/hono@v3.2.3/router/trie-router/router.ts": "ad0b3fdabc33abd11a9f5819734aec743602a743cfc9f90ddad73787cd5e7727", 44 | "https://deno.land/x/hono@v3.2.3/types.ts": "511c0c903786dd529b5979caf3bf2f2a48c18925203d819f343a6c49116aae51", 45 | "https://deno.land/x/hono@v3.2.3/utils/body.ts": "b6b5ed679122968a74845df4c5454c677f09adc4f3466d822f3b1397884e540e", 46 | "https://deno.land/x/hono@v3.2.3/utils/cookie.ts": "4d57e055922f7e47fb1d45927aadbef04ae6aa433ccdd1a81450a54c3a4179d4", 47 | "https://deno.land/x/hono@v3.2.3/utils/http-status.ts": "e0c4343ea7717c314dc600131e16b636c29d61cfdaf9df93b267258d1729d1a0", 48 | "https://deno.land/x/hono@v3.2.3/utils/types.ts": "64fe2bf6526c682024bc5d0cd59d01a32f12bb9b80dca58d1cfc4e70231ef696", 49 | "https://deno.land/x/hono@v3.2.3/utils/url.ts": "014ac4958553650c23b094b44029f70b491f45a2808ee476e3248475910153e9", 50 | "https://deno.land/x/hono@v3.2.3/validator/index.ts": "6c986e8b91dcf857ecc8164a506ae8eea8665792a4ff7215471df669c632ae7c", 51 | "https://deno.land/x/hono@v3.2.3/validator/validator.ts": "e52183ddf8cefc3cfe2788802fe6ce766df8ac904df485107b01cbcca167dbad", 52 | "https://esm.sh/ky@0.33.2": "0f1af108555ec94b4334fc24d54158e0fb640da16497a2abff0115dd633f8abc", 53 | "https://esm.sh/v106/ky@0.33.2/deno/ky.js": "c24d4e80cc124c5cd9873e6ea806295f829ce3909b67784240c05e20d592e1db", 54 | "https://esm.sh/v106/ky@0.33.2/distribution/core/constants.d.ts": "9c23fb2acd5ed818c950c6802b22cafde46f1cd6217898ca4f0f726558576f48", 55 | "https://esm.sh/v106/ky@0.33.2/distribution/errors/HTTPError.d.ts": "bec5524c0d8addd2e4d678b14434976fad37b83bacffa3ae65d14afe71a76e83", 56 | "https://esm.sh/v106/ky@0.33.2/distribution/errors/TimeoutError.d.ts": "162f748e98203b829d8505f499c99ee0fc2200c11f3743d0e7124e7bf8ca253c", 57 | "https://esm.sh/v106/ky@0.33.2/distribution/index.d.ts": "33e8773cbec165d42ba5b39476f2293cd0506a345cd7c2f706ab4b9c452479b5", 58 | "https://esm.sh/v106/ky@0.33.2/distribution/types/ResponsePromise.d.ts": "b038f6f19c25ffb29ef3d8a89dc78fb69a03fdf7438e4b14e45ec5390b2039bc", 59 | "https://esm.sh/v106/ky@0.33.2/distribution/types/common.d.ts": "20fcb118bebfb69ca52595ac4f0dd500fe457a5cedb0a4186a3b09cb20aad7c7", 60 | "https://esm.sh/v106/ky@0.33.2/distribution/types/hooks.d.ts": "5654d6c3b3540f3768c8c8dd125c3bd6c44b03f70f6ddc1212a2cd3edb1f8039", 61 | "https://esm.sh/v106/ky@0.33.2/distribution/types/ky.d.ts": "e2d296326628fde655ead9f8dd32c87ecc32b84f11511e7b4b7c3a778ba63d5a", 62 | "https://esm.sh/v106/ky@0.33.2/distribution/types/options.d.ts": "35bb34e6a2af7d02645bb66aef564e41457fc33f293d1aba2da397b6da8866aa", 63 | "https://esm.sh/v106/ky@0.33.2/distribution/types/response.d.ts": "fd7cdcf4a60ac72826774dddc27878b62ab87f74e574dfaa6a8818458f0f505d", 64 | "https://esm.sh/v106/ky@0.33.2/distribution/types/retry.d.ts": "96a0e14854cadd119faac1df7e4fa92ddb0a6119fa7535b5a9b08903ecc4cf52" 65 | }, 66 | "npm": { 67 | "specifiers": { 68 | "@types/wav": "@types/wav@1.0.1", 69 | "wav": "wav@1.0.2" 70 | }, 71 | "packages": { 72 | "@types/node@18.11.18": { 73 | "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", 74 | "dependencies": {} 75 | }, 76 | "@types/wav@1.0.1": { 77 | "integrity": "sha512-AKJeM5mqO1pdR2/HaTUQzSCm12No36KUM1larivXUmsLx+4JmMuC2Tv0kCdZzTx66h7IH2Xr92DGc9NQsXxa9Q==", 78 | "dependencies": { 79 | "@types/node": "@types/node@18.11.18" 80 | } 81 | }, 82 | "buffer-alloc-unsafe@1.1.0": { 83 | "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", 84 | "dependencies": {} 85 | }, 86 | "buffer-alloc@1.2.0": { 87 | "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", 88 | "dependencies": { 89 | "buffer-alloc-unsafe": "buffer-alloc-unsafe@1.1.0", 90 | "buffer-fill": "buffer-fill@1.0.0" 91 | } 92 | }, 93 | "buffer-fill@1.0.0": { 94 | "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", 95 | "dependencies": {} 96 | }, 97 | "buffer-from@1.1.2": { 98 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 99 | "dependencies": {} 100 | }, 101 | "core-util-is@1.0.3": { 102 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 103 | "dependencies": {} 104 | }, 105 | "debug@2.6.9": { 106 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 107 | "dependencies": { 108 | "ms": "ms@2.0.0" 109 | } 110 | }, 111 | "inherits@2.0.4": { 112 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 113 | "dependencies": {} 114 | }, 115 | "isarray@0.0.1": { 116 | "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", 117 | "dependencies": {} 118 | }, 119 | "ms@2.0.0": { 120 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 121 | "dependencies": {} 122 | }, 123 | "readable-stream@1.1.14": { 124 | "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", 125 | "dependencies": { 126 | "core-util-is": "core-util-is@1.0.3", 127 | "inherits": "inherits@2.0.4", 128 | "isarray": "isarray@0.0.1", 129 | "string_decoder": "string_decoder@0.10.31" 130 | } 131 | }, 132 | "stream-parser@0.3.1": { 133 | "integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==", 134 | "dependencies": { 135 | "debug": "debug@2.6.9" 136 | } 137 | }, 138 | "string_decoder@0.10.31": { 139 | "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", 140 | "dependencies": {} 141 | }, 142 | "wav@1.0.2": { 143 | "integrity": "sha512-viHtz3cDd/Tcr/HbNqzQCofKdF6kWUymH9LGDdskfWFoIy/HJ+RTihgjEcHfnsy1PO4e9B+y4HwgTwMrByquhg==", 144 | "dependencies": { 145 | "buffer-alloc": "buffer-alloc@1.2.0", 146 | "buffer-from": "buffer-from@1.1.2", 147 | "debug": "debug@2.6.9", 148 | "readable-stream": "readable-stream@1.1.14", 149 | "stream-parser": "stream-parser@0.3.1" 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { Hono } from "https://deno.land/x/hono@v3.2.3/mod.ts"; 2 | export { logger as honoLogger } from "https://deno.land/x/hono@v3.2.3/middleware.ts"; 3 | export { serve } from "https://deno.land/std@0.190.0/http/server.ts"; 4 | export { parse } from "https://deno.land/std@0.190.0/flags/mod.ts"; 5 | export { dirname } from "https://deno.land/std@0.190.0/path/mod.ts"; 6 | export { default as ky } from "https://esm.sh/ky@0.33.2"; 7 | export { fromUint8Array as toBase64 } from "https://deno.land/x/base64@v0.2.1/mod.ts"; 8 | // @deno-types="npm:@types/async-lock@1.4.2" 9 | export { default as AsyncLock } from "npm:async-lock@1.4.0"; 10 | // @deno-types="npm:@types/wanakana@4.0.6" 11 | export { default as wanakana } from "npm:wanakana@5.2.0"; 12 | import osPaths from "https://deno.land/x/os_paths@v7.4.0/src/mod.deno.ts"; 13 | 14 | export const homeDir = osPaths.home(); 15 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { dirname, Hono, honoLogger, ky, parse, serve } from "./deps.ts"; 2 | import dictProvider from "./providers/dict.ts"; 3 | import infoProvider from "./providers/info.ts"; 4 | import noopProvider from "./providers/noop.ts"; 5 | import synthesisProvider from "./providers/synthesis.ts"; 6 | import { saveStore, store } from "./store.ts"; 7 | 8 | const args = parse(Deno.args, { 9 | string: ["host", "port", "originalUrl"], 10 | default: { 11 | host: "127.0.0.1", 12 | port: "50132", 13 | originalUrl: "http://127.0.0.1:50032", 14 | }, 15 | }); 16 | const baseClient = ky.create({ 17 | prefixUrl: args.originalUrl, 18 | }); 19 | 20 | if (store.enginePath != undefined) { 21 | if (await baseClient.get("").catch(() => null)) { 22 | console.log("The server is already running, not starting the engine."); 23 | } else if (Deno.stat(store.enginePath).catch(() => null) == undefined) { 24 | console.log( 25 | `The engine path ${store.enginePath} does not exist, not starting the engine.`, 26 | ); 27 | store.enginePath = undefined; 28 | } else { 29 | console.log(`Starting the engine at ${store.enginePath}...`); 30 | const process = new Deno.Command( 31 | store.enginePath, 32 | { 33 | stdout: "inherit", 34 | stderr: "inherit", 35 | }, 36 | ).spawn(); 37 | self.addEventListener("unload", () => { 38 | process.kill(); 39 | }); 40 | } 41 | } else { 42 | console.log("No engine path, not starting the engine."); 43 | } 44 | 45 | while (true) { 46 | try { 47 | await baseClient.get(""); 48 | break; 49 | } catch { 50 | console.log("Waiting for the server to be ready..."); 51 | await new Promise((resolve) => setTimeout(resolve, 1000)); 52 | } 53 | } 54 | 55 | const speakers: { 56 | speakerUuid: string; 57 | }[] = await baseClient.get("v1/speakers").json(); 58 | const path: { 59 | speakerFolderPath: string; 60 | } = await baseClient.get("v1/speaker_folder_path", { 61 | searchParams: { 62 | speakerUuid: speakers[0].speakerUuid, 63 | }, 64 | }).json(); 65 | const speakerFolderPath = path.speakerFolderPath; 66 | const enginePath = dirname(dirname(speakerFolderPath)) + "/engine/engine.exe"; 67 | 68 | console.log(`Engine path: ${enginePath}`); 69 | 70 | store.enginePath = enginePath; 71 | await saveStore(); 72 | 73 | const app = new Hono(); 74 | 75 | app.use("*", honoLogger()); 76 | 77 | app.use("*", async (c, next) => { 78 | await next(); 79 | c.res.headers.set("Access-Control-Allow-Origin", "*"); 80 | c.res.headers.set("Access-Control-Allow-Headers", "*"); 81 | c.res.headers.set("Access-Control-Allow-Methods", "*"); 82 | }); 83 | app.options("*", (c) => { 84 | return c.text("", 200); 85 | }); 86 | 87 | [infoProvider, noopProvider, synthesisProvider, dictProvider].forEach(( 88 | provider, 89 | ) => provider({ baseClient, app })); 90 | 91 | serve(app.fetch, { hostname: args.host, port: parseInt(args.port) }); 92 | -------------------------------------------------------------------------------- /providers/dict.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "./index.ts"; 2 | 3 | type VoicevoxWord = { 4 | surface: string; 5 | priority: number; 6 | context_id: number; 7 | part_of_speech: string; 8 | part_of_speech_detail_1: string; 9 | part_of_speech_detail_2: string; 10 | part_of_speech_detail_3: string; 11 | inflectional_type: string; 12 | inflectional_form: string; 13 | stem: string; 14 | yomi: string; 15 | pronunciation: string; 16 | accent_type: number; 17 | mora_count: number; 18 | accent_associative_rule: string; 19 | }; 20 | type VoicevoxMinimumWord = { 21 | surface: string; 22 | pronunciation: string; 23 | accent_type: number; 24 | }; 25 | type CoeiroinkWord = { 26 | word: string; 27 | yomi: string; 28 | accent: number; 29 | numMoras: number; 30 | }; 31 | 32 | let words: Record = {}; 33 | const moraRegex = new RegExp( 34 | [ 35 | "[イ][ェ]|[ヴ][ャュョ]|[トド][ゥ]|[テデ][ィャュョ]|[デ][ェ]|[クグ][ヮ]|", // rule_others 36 | "[キシチニヒミリギジビピ][ェャュョ]|", // rule_line_i 37 | "[ツフヴ][ァ]|[ウスツフヴズ][ィ]|[ウツフヴ][ェォ]|", // rule_line_u 38 | "[ァ-ヴー]", // rule_one_mora 39 | ].join(""), 40 | "g", 41 | ); 42 | const minWordToCoeiroinkWord = (word: VoicevoxMinimumWord) => { 43 | const moraCount = word.pronunciation.match(moraRegex)?.length ?? 0; 44 | return { 45 | word: word.surface, 46 | 47 | yomi: word.pronunciation, 48 | accent: word.accent_type, 49 | numMoras: moraCount, 50 | } satisfies CoeiroinkWord; 51 | }; 52 | 53 | const dictProvider: Provider = ( 54 | { app, baseClient }, 55 | ) => { 56 | const updateDict = async () => { 57 | await baseClient.post("v1/set_dictionary", { 58 | json: { 59 | dictionaryWords: Object.values(words), 60 | }, 61 | }); 62 | }; 63 | app.get("/user_dict", (c) => 64 | c.json( 65 | words, 66 | )); 67 | app.post("/user_dict_word", async (c) => { 68 | const body = await c.req.json() as VoicevoxMinimumWord; 69 | const uuid = crypto.randomUUID(); 70 | words[uuid] = minWordToCoeiroinkWord(body); 71 | await updateDict(); 72 | return c.json(uuid); 73 | }); 74 | app.put("/user_dict_word/:uuid", async (c) => { 75 | const uuid = c.req.param("uuid"); 76 | const body = { 77 | surface: c.req.query("surface"), 78 | pronunciation: c.req.query("pronunciation"), 79 | accent_type: parseInt(c.req.query("accent_type") || "0"), 80 | } as VoicevoxMinimumWord; 81 | words[uuid] = minWordToCoeiroinkWord(body); 82 | await updateDict(); 83 | c.status(204); 84 | return c.text(""); 85 | }); 86 | app.delete("/user_dict_word/:uuid", async (c) => { 87 | const uuid = c.req.param("uuid"); 88 | delete words[uuid]; 89 | await updateDict(); 90 | 91 | c.status(204); 92 | return c.text(""); 93 | }); 94 | 95 | app.post("/import_user_dict", async (c) => { 96 | const body = await c.req.json() as Record; 97 | words = Object.fromEntries( 98 | Object.entries(body).map(([uuid, value]) => [ 99 | uuid, 100 | { 101 | word: value.surface, 102 | yomi: value.yomi, 103 | accent: value.accent_type, 104 | numMoras: value.mora_count, 105 | } satisfies CoeiroinkWord, 106 | ]), 107 | ); 108 | await updateDict(); 109 | return c.text("OK", 200); 110 | }); 111 | }; 112 | 113 | export default dictProvider; 114 | -------------------------------------------------------------------------------- /providers/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sevenc-nanashi/coeiroink-v2-bridge/89eee92d4e6880c2df86a3cbe2060e703860e10a/providers/icon.png -------------------------------------------------------------------------------- /providers/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono, ky } from "../deps.ts"; 2 | 3 | export type Provider = ({ 4 | baseClient, 5 | app, 6 | }: { 7 | baseClient: typeof ky; 8 | app: Hono; 9 | }) => void; 10 | -------------------------------------------------------------------------------- /providers/info.ts: -------------------------------------------------------------------------------- 1 | import { dirname, toBase64 } from "../deps.ts"; 2 | import { Provider } from "./index.ts"; 3 | 4 | let speakers: { 5 | speakerName: string; 6 | speakerUuid: string; 7 | base64Portrait: string; 8 | version: string; 9 | styles: { 10 | styleName: string; 11 | styleId: number; 12 | base64Icon: string; 13 | base64Portrait: string | null; 14 | }[]; 15 | }[]; 16 | 17 | const infoProvider: Provider = ({ baseClient, app }) => { 18 | app.get("/version", async (c) => { 19 | await baseClient.get(""); 20 | 21 | return c.json("0.2.1"); 22 | }); 23 | 24 | app.get( 25 | "/supported_devices", 26 | (c) => c.json({ cpu: true, cuda: false, dml: false }), 27 | ); 28 | 29 | app.get("/engine_manifest", async (c) => { 30 | return c.json({ 31 | manifest_version: "0.13.1", 32 | name: "COEIROINK v2 @ bridge", 33 | brand_name: "COEIROINK v2", 34 | uuid: "96755ba9-6c9d-4166-aaf3-86633dfa0ca5", 35 | url: "https://github.com/sevenc-nanashi/coeiroink-v2-bridge", 36 | icon: await Deno.readFile( 37 | Deno.execPath().endsWith("deno.exe") 38 | ? new URL("./icon.png", import.meta.url) 39 | : dirname(Deno.execPath()) + "/icon.png", 40 | ).then( 41 | (buf) => toBase64(buf), 42 | ), 43 | default_sampling_rate: 24000, 44 | terms_of_service: "https://coeiroink.com/terms を参照して下さい。", 45 | update_infos: [ 46 | { 47 | version: "情報について", 48 | descriptions: [ 49 | "VOICEVOXアプリ内ではCOEIORINKの変更履歴を表示することができません。COEIROINK側のサイトを参照して下さい。", 50 | "この下のアップデート内容はCOEIROINK v2 bridgeのものです。", 51 | ], 52 | contributors: [], 53 | }, 54 | { 55 | version: "0.2.1", 56 | descriptions: [ 57 | "Fix: 空のAccentPhraseで無音を返すように", 58 | ], 59 | contributors: ["sevenc-nanashi"], 60 | }, 61 | { 62 | version: "0.2.0", 63 | descriptions: [ 64 | "Change: Coeiroink側のstyleIdを使うように変更", 65 | ], 66 | contributors: ["sevenc-nanashi"], 67 | }, 68 | { 69 | version: "0.1.3", 70 | descriptions: [ 71 | "Fix: 読点周りの挙動を修正", 72 | ], 73 | contributors: ["sevenc-nanashi"], 74 | }, 75 | { 76 | version: "0.1.2", 77 | descriptions: [ 78 | "Add: mutexを追加", 79 | "Add: 自動起動を追加", 80 | ], 81 | contributors: ["sevenc-nanashi"], 82 | }, 83 | { 84 | version: "0.1.1", 85 | descriptions: [ 86 | "Add: Mac版ビルドを追加", 87 | "Fix: ユーザー辞書周りを修正", 88 | ], 89 | contributors: ["sevenc-nanashi"], 90 | }, 91 | { 92 | version: "0.1.0", 93 | descriptions: [ 94 | "Update: COEIROINK v2正式版に追従", 95 | "Add: 辞書読み込みを追加", 96 | "Delete: outputSamplingRate周りのワークアラウンドを削除", 97 | "Fix: pitchScaleをデフォルトで0に(by @itsuka-dev)", 98 | ], 99 | contributors: ["sevenc-nanashi", "itsuka-dev"], 100 | }, 101 | { 102 | version: "0.0.1", 103 | descriptions: ["初期リリース。"], 104 | contributors: ["sevenc-nanashi"], 105 | }, 106 | ], 107 | dependency_licenses: [ 108 | { 109 | name: "Info", 110 | version: "-", 111 | license: "Dummy", 112 | text: "COEIROINK v2のライセンスを確認して下さい。", 113 | }, 114 | ], 115 | supported_features: { 116 | adjust_mora_pitch: false, 117 | adjust_phoneme_length: false, 118 | adjust_speed_scale: true, 119 | adjust_pitch_scale: true, 120 | adjust_intonation_scale: true, 121 | adjust_volume_scale: true, 122 | interrogative_upspeak: false, 123 | synthesis_morphing: false, 124 | manage_library: false, 125 | }, 126 | }); 127 | }); 128 | 129 | app.get("/speakers", async (c) => { 130 | speakers = await baseClient.get("v1/speakers").json(); 131 | 132 | return c.json( 133 | speakers.map((speaker) => ({ 134 | name: speaker.speakerName, 135 | speaker_uuid: speaker.speakerUuid, 136 | styles: speaker.styles.map((style) => ({ 137 | name: style.styleName, 138 | id: style.styleId, 139 | })), 140 | version: speaker.version, 141 | })), 142 | ); 143 | }); 144 | 145 | app.get("/speaker_info", (c) => { 146 | const speakerUuid = c.req.query("speaker_uuid"); 147 | const speaker = speakers.find( 148 | (speaker) => speaker.speakerUuid === speakerUuid, 149 | ); 150 | if (!speaker || !speakerUuid) { 151 | c.status(404); 152 | return c.json({ error: "speaker not found" }); 153 | } 154 | return c.json({ 155 | policy: "https://coeiroink.com/terms を参照して下さい。", 156 | portrait: speaker.base64Portrait, 157 | style_infos: speaker.styles.map((style) => ({ 158 | id: style.styleId, 159 | icon: style.base64Icon, 160 | portrait: style.base64Portrait, 161 | voice_samples: [], 162 | })), 163 | }); 164 | }); 165 | }; 166 | 167 | export default infoProvider; 168 | -------------------------------------------------------------------------------- /providers/noop.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "./index.ts"; 2 | 3 | const noopProvider: Provider = ( 4 | { app }, 5 | ) => { 6 | app.post("/initialize_speaker", (c) => c.json(true)); 7 | app.get("/is_initialized_speaker", (c) => c.json(true)); 8 | 9 | app.post("/mora_data", async (c) => c.json(await c.req.json())); 10 | }; 11 | 12 | export default noopProvider; 13 | -------------------------------------------------------------------------------- /providers/synthesis.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from "./index.ts"; 2 | import { AsyncLock, wanakana } from "../deps.ts"; 3 | 4 | type Prosody = { 5 | plain: string[]; 6 | detail: { 7 | phoneme: string; 8 | hira: string; 9 | accent: number; 10 | }[][]; 11 | }; 12 | type Mora = { 13 | text: string; 14 | consonant: string | null; 15 | consonant_length: number | null; 16 | vowel: string; 17 | vowel_length: number; 18 | pitch: number; 19 | }; 20 | type AccentPhrase = { 21 | moras: Mora[]; 22 | accent: number; 23 | is_interrogative: boolean; 24 | pause_mora: Mora | null; 25 | }; 26 | 27 | const synthesisLock = new AsyncLock(); 28 | 29 | const prosodyToAccentPhrases = (prosody: Prosody) => { 30 | const result: AccentPhrase[] = []; 31 | for (const d of prosody.detail) { 32 | let accentPosition = -1; 33 | const moras: Mora[] = []; 34 | let moraIndex = -1; 35 | for (const m of d) { 36 | moraIndex++; 37 | if (m.hira === "、") { 38 | result[result.length - 1].pause_mora = { 39 | text: "、", 40 | consonant: null, 41 | consonant_length: null, 42 | vowel: "pau", 43 | vowel_length: 0, 44 | pitch: 0, 45 | }; 46 | } else { 47 | let vowel: string, consonant: string | null; 48 | if (m.phoneme.includes("-")) { 49 | [consonant, vowel] = m.phoneme.split("-"); 50 | } else { 51 | consonant = null; 52 | vowel = m.phoneme; 53 | } 54 | moras.push({ 55 | text: wanakana.toKatakana( 56 | m.hira, 57 | ), 58 | consonant, 59 | consonant_length: consonant ? 0 : null, 60 | vowel, 61 | vowel_length: 0, 62 | pitch: 0, 63 | }); 64 | if (m.accent === 1) { 65 | accentPosition = moraIndex; 66 | } 67 | } 68 | } 69 | if (moras.length === 0) { 70 | continue; 71 | } 72 | result.push({ 73 | moras, 74 | accent: accentPosition + 1, 75 | is_interrogative: false, 76 | pause_mora: null, 77 | }); 78 | } 79 | return result; 80 | }; 81 | 82 | const accentPhrasesToProsody = (accentPhrases: AccentPhrase[]) => { 83 | return accentPhrases.map((accentPhrase) => { 84 | const detail = []; 85 | 86 | accentPhrase.moras.forEach((mora, i) => { 87 | let phoneme; 88 | if (mora.consonant && mora.consonant.length > 0) { 89 | phoneme = `${mora.consonant}-${mora.vowel}`; 90 | } else { 91 | phoneme = mora.vowel; 92 | } 93 | 94 | let accent = 0; 95 | if ( 96 | i === accentPhrase.accent - 1 || 97 | (i !== 0 && i <= accentPhrase.accent - 1) 98 | ) { 99 | accent = 1; 100 | } 101 | 102 | detail.push({ 103 | hira: wanakana.toHiragana( 104 | mora.text, 105 | ), 106 | phoneme, 107 | accent, 108 | }); 109 | }); 110 | 111 | if (accentPhrase.pause_mora) { 112 | detail.push({ 113 | hira: "、", 114 | phoneme: "_", 115 | accent: 0, 116 | }); 117 | } 118 | 119 | return detail; 120 | }); 121 | }; 122 | 123 | const synthesisProvider: Provider = ({ baseClient, app }) => { 124 | app.post("/accent_phrases", async (c) => { 125 | const text = c.req.query("text"); 126 | 127 | const prosody = await baseClient 128 | .post("v1/estimate_prosody", { 129 | json: { 130 | text, 131 | }, 132 | }) 133 | .json(); 134 | return c.json(prosodyToAccentPhrases(prosody)); 135 | }); 136 | app.post("/audio_query", async (c) => { 137 | const text = c.req.query("text"); 138 | 139 | const prosody = await baseClient 140 | .post("v1/estimate_prosody", { 141 | json: { 142 | text, 143 | }, 144 | }) 145 | .json(); 146 | return c.json({ 147 | accent_phrases: prosodyToAccentPhrases(prosody), 148 | speedScale: 1, 149 | pitchScale: 0, 150 | intonationScale: 1, 151 | volumeScale: 1, 152 | prePhonemeLength: 0.1, 153 | postPhonemeLength: 0.1, 154 | outputSamplingRate: 24000, 155 | outputStereo: true, 156 | kana: "", 157 | }); 158 | }); 159 | app.post("/synthesis", async (c) => { 160 | return await synthesisLock.acquire("lock", async () => { 161 | const audioQuery = await c.req.json(); 162 | const speakerId = parseInt(c.req.query("speaker") ?? ""); 163 | if (isNaN(speakerId)) { 164 | c.status(400); 165 | return c.json({ 166 | error: "speaker is not a number", 167 | }); 168 | } 169 | const accentPhrases = audioQuery.accent_phrases; 170 | const prosody = accentPhrasesToProsody(accentPhrases); 171 | const speakers = await baseClient.get("v1/speakers").json<{ 172 | speakerUuid: string; 173 | styles: { 174 | styleId: number; 175 | }[]; 176 | }[]>(); 177 | let speakerUuid: string | undefined; 178 | let styleId: number | undefined; 179 | for (const speaker of speakers) { 180 | if (speaker.styles.find((style) => style.styleId === speakerId)) { 181 | speakerUuid = speaker.speakerUuid; 182 | styleId = speakerId; 183 | break; 184 | } 185 | } 186 | if (!speakerUuid) { 187 | c.status(400); 188 | return c.json({ 189 | error: "speaker not found", 190 | }); 191 | } 192 | const body = { 193 | speakerUuid: speakerUuid, 194 | styleId: styleId, 195 | // TODO: 無音はここより前で返すようにしたい 196 | text: accentPhrases.length > 0 197 | ? "この文章が読み上げられているのはバグです。" 198 | : "", 199 | prosodyDetail: prosody, 200 | speedScale: audioQuery.speedScale, 201 | volumeScale: audioQuery.volumeScale, 202 | pitchScale: audioQuery.pitchScale, 203 | intonationScale: audioQuery.intonationScale, 204 | prePhonemeLength: audioQuery.prePhonemeLength, 205 | postPhonemeLength: audioQuery.postPhonemeLength, 206 | outputSamplingRate: audioQuery.outputSamplingRate, 207 | }; 208 | const result = await baseClient.post("v1/synthesis", { 209 | json: body, 210 | timeout: false, 211 | }); 212 | if (!result.ok) { 213 | c.status(500); 214 | return c.json({ 215 | error: "synthesis failed", 216 | }); 217 | } 218 | return c.body(result.body); 219 | }); 220 | }); 221 | }; 222 | 223 | export default synthesisProvider; 224 | -------------------------------------------------------------------------------- /store.ts: -------------------------------------------------------------------------------- 1 | import { homeDir } from "./deps.ts"; 2 | 3 | type Store = { 4 | enginePath: string | undefined; 5 | }; 6 | 7 | const filePath = homeDir + "/.coeiroink-v2.json"; 8 | export let store: Store = { enginePath: undefined }; 9 | 10 | if (await Deno.stat(filePath).catch(() => null)) { 11 | console.log("Loading store..."); 12 | store = JSON.parse(await Deno.readTextFile(filePath)); 13 | } 14 | export const saveStore = async () => { 15 | await Deno.writeTextFile(filePath + ".tmp", JSON.stringify(store)); 16 | await Deno.rename(filePath + ".tmp", filePath); 17 | }; 18 | --------------------------------------------------------------------------------