From be8b5254af44343e054b280a18509782b7688394 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Wed, 15 Nov 2023 18:57:35 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Add:=20JS=E5=81=B4=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/capacitor.build.gradle | 1 + android/capacitor.settings.gradle | 3 + package-lock.json | 15 +++ package.json | 1 + src/mobile/engine/dict.ts | 167 ++++++++++++++++++++++++++--- src/mobile/plugin.ts | 2 + 6 files changed, 175 insertions(+), 14 deletions(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 259821da27..fcd7443159 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,6 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-preferences') implementation project(':capacitor-splash-screen') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 13b19d64a8..deca3e9867 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,5 +2,8 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') + include ':capacitor-splash-screen' project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android') diff --git a/package-lock.json b/package-lock.json index 4e2fb3f3ba..98497f6fb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@capacitor/android": "5.5.1", "@capacitor/core": "5.5.1", + "@capacitor/preferences": "5.0.6", "@capacitor/splash-screen": "5.0.2", "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", @@ -334,6 +335,14 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, + "node_modules/@capacitor/preferences": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-5.0.6.tgz", + "integrity": "sha512-aDe4wGTVSAIue6XXdUFgyz7SGszxK/Ptt/iWTydMpzc1PlZXw1XTTnciM+S+SLLNZFzXlkpXT3wMnh9t0DojUA==", + "peerDependencies": { + "@capacitor/core": "^5.0.0" + } + }, "node_modules/@capacitor/splash-screen": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-5.0.2.tgz", @@ -17086,6 +17095,12 @@ } } }, + "@capacitor/preferences": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-5.0.6.tgz", + "integrity": "sha512-aDe4wGTVSAIue6XXdUFgyz7SGszxK/Ptt/iWTydMpzc1PlZXw1XTTnciM+S+SLLNZFzXlkpXT3wMnh9t0DojUA==", + "requires": {} + }, "@capacitor/splash-screen": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-5.0.2.tgz", diff --git a/package.json b/package.json index 7e13b301bb..6c60189d69 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "@capacitor/android": "5.5.1", "@capacitor/core": "5.5.1", + "@capacitor/preferences": "5.0.6", "@capacitor/splash-screen": "5.0.2", "@gtm-support/vue-gtm": "1.2.3", "@quasar/extras": "1.10.10", diff --git a/src/mobile/engine/dict.ts b/src/mobile/engine/dict.ts index 9b2ff70eee..2bcc06ecb6 100644 --- a/src/mobile/engine/dict.ts +++ b/src/mobile/engine/dict.ts @@ -1,23 +1,162 @@ +import { Preferences } from "@capacitor/preferences"; +import { v4 as uuidv4 } from "uuid"; import { ApiProvider } from "."; +import { UserDictWord } from "@/openapi"; + +type InternalUserDict = Record; + +const preferenceKey = "userDict"; +const getUserDictWords = async () => { + const userDictJson = await Preferences.get({ key: preferenceKey }); + const dict: InternalUserDict = userDictJson.value + ? JSON.parse(userDictJson.value) + : {}; + return dict; +}; +const setUserDictWords = async (dict: InternalUserDict) => { + await Preferences.set({ key: preferenceKey, value: JSON.stringify(dict) }); +}; + +type InternalWordType = + | "PROPER_NOUN" + | "COMMON_NOUN" + | "VERB" + | "ADJECTIVE" + | "SUFFIX"; +type InternalDictWord = { + priority: number; + accentType: number; + surface: string; + pronunciation: string; + wordType: InternalWordType; +}; +const internalWordTypeMap: Record< + InternalWordType, + { + partOfSpeech: string; + partOfSpeechDetail1: string; + partOfSpeechDetail2: string; + partOfSpeechDetail3: string; + } +> = { + PROPER_NOUN: { + partOfSpeech: "名詞", + partOfSpeechDetail1: "固有名詞", + partOfSpeechDetail2: "一般", + partOfSpeechDetail3: "*", + }, + COMMON_NOUN: { + partOfSpeech: "名詞", + partOfSpeechDetail1: "一般", + partOfSpeechDetail2: "*", + partOfSpeechDetail3: "*", + }, + VERB: { + partOfSpeech: "動詞", + partOfSpeechDetail1: "自立", + partOfSpeechDetail2: "*", + partOfSpeechDetail3: "*", + }, + ADJECTIVE: { + partOfSpeech: "形容詞", + partOfSpeechDetail1: "自立", + partOfSpeechDetail2: "*", + partOfSpeechDetail3: "*", + }, + SUFFIX: { + partOfSpeech: "名詞", + partOfSpeechDetail1: "接尾", + partOfSpeechDetail2: "一般", + partOfSpeechDetail3: "*", + }, +}; +const apiWordToInternalWord = (word: UserDictWord): InternalDictWord => { + const wordType = Object.entries(internalWordTypeMap).find( + ([, value]) => + value.partOfSpeech === word.partOfSpeech && + value.partOfSpeechDetail1 === word.partOfSpeechDetail1 && + value.partOfSpeechDetail2 === word.partOfSpeechDetail2 && + value.partOfSpeechDetail3 === word.partOfSpeechDetail3 + )?.[0] as InternalWordType | undefined; + if (!wordType) { + throw new Error("wordType not found"); + } + return { + surface: word.surface, + pronunciation: word.pronunciation, + accentType: word.accentType, + priority: word.priority, + wordType, + }; +}; const dictProvider: ApiProvider = () => { - // TODO: - // ユーザー辞書機能がCoreに実装されるまで、必要最低限のモックにしておく。 - // cf: https://github.com/VOICEVOX/voicevox_core/issues/265 return { async getUserDictWordsUserDictGet() { - return {}; + const dict = await getUserDictWords(); + + return Object.fromEntries( + Object.entries(dict).map<[string, UserDictWord]>(([uuid, word]) => [ + uuid, + { + surface: word.surface, + pronunciation: word.pronunciation, + accentType: word.accentType, + priority: word.priority, + ...internalWordTypeMap[word.wordType], + // 本来はもっとプロパティがあるが、 + // https://github.com/VOICEVOX/voicevox_core/blob/main/crates/voicevox_core/src/user_dict/part_of_speech_data.rs のような + // 対応表をもってくるのは面倒なので、とりあえずこれだけ用意しておく。 + } as UserDictWord, + ]) + ); + }, + async addUserDictWordUserDictWordPost(word) { + if (!word.wordType) { + throw new Error("wordType is required"); + } + const uuid = uuidv4(); + const dict = await getUserDictWords(); + dict[uuid] = { + surface: word.surface, + pronunciation: word.pronunciation, + accentType: word.accentType, + priority: word.priority ?? 5, + wordType: word.wordType, + }; + await setUserDictWords(dict); + return uuid; + }, + async deleteUserDictWordUserDictWordWordUuidDelete(req) { + const dict = await getUserDictWords(); + delete dict[req.wordUuid]; + await setUserDictWords(dict); + return; + }, + async rewriteUserDictWordUserDictWordWordUuidPut(req) { + const dict = await getUserDictWords(); + const word = dict[req.wordUuid]; + if (!word) { + throw new Error("wordUuid not found"); + } + dict[req.wordUuid] = { + surface: req.surface, + pronunciation: req.pronunciation, + accentType: req.accentType, + priority: req.priority || word.priority, + wordType: req.wordType || word.wordType, + }; + await setUserDictWords(dict); + return; }, - // async addUserDictWordUserDictWordPost() { - // return; - // }, - // async deleteUserDictWordUserDictWordWordUuidDelete() { - // return; - // }, - // async rewriteUserDictWordUserDictWordWordUuidPut() { - // return; - // }, - async importUserDictWordsImportUserDictPost() { + async importUserDictWordsImportUserDictPost(req) { + const dict = await getUserDictWords(); + for (const [uuid, word] of Object.entries(req.requestBody)) { + if (dict[uuid] && !req.override) { + continue; + } + dict[uuid] = apiWordToInternalWord(word); + } return; }, }; diff --git a/src/mobile/plugin.ts b/src/mobile/plugin.ts index 103bf0fcd6..a3b01e6bd6 100644 --- a/src/mobile/plugin.ts +++ b/src/mobile/plugin.ts @@ -37,6 +37,8 @@ export type VoicevoxCorePlugin = { speakerId: number; enableInterrogativeUpspeak: boolean; }) => Promise<{ value: string }>; + + userDictLoad: (obj: { dictJson: string }) => Promise; }; const loadPlugin = () => { From 0cb9694a20b8c95e040650f78dfd60399f07673b Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Wed, 15 Nov 2023 20:41:52 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Add:=20Android=E5=81=B4=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle | 6 +- .../java/jp/hiroshiba/voicevox/CorePlugin.kt | 55 +++++++++++++++---- src/mobile/engine/dict.ts | 31 ++++++++--- src/mobile/engine/index.ts | 8 ++- src/mobile/engine/manifestAssets/base.json | 2 +- src/mobile/plugin.ts | 2 +- 6 files changed, 79 insertions(+), 25 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index cc6d810b5e..7088395020 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -74,7 +74,11 @@ dependencies { implementation project(':capacitor-cordova-android-plugins') // TODO: ちゃんと公開されたらそれに置き換える - implementation urlZipFile("voicevoxcore-android", "jp/hiroshiba/voicevoxcore/voicevoxcore-android/0.15.0-preview.13/voicevoxcore-android-0.15.0-preview.13.aar", "https://github.com/VOICEVOX/voicevox_core/releases/download/0.15.0-preview.13/java_packages.zip") + implementation urlZipFile( + "voicevoxcore-android_0.15.0-preview.15", + "jp/hiroshiba/voicevoxcore/voicevoxcore-android/0.15.0-preview.15/voicevoxcore-android-0.15.0-preview.15.aar", + "https://github.com/VOICEVOX/voicevox_core/releases/download/0.15.0-preview.15/java_packages.zip" + ) // https://mvnrepository.com/artifact/com.google.code.gson/gson implementation group: 'com.google.code.gson', name: 'gson', version: "2.10.1" diff --git a/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt b/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt index 54184b1fad..4a3d111044 100644 --- a/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt +++ b/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt @@ -1,6 +1,7 @@ package jp.hiroshiba.voicevox import android.app.Activity +import android.system.Os import android.util.Log import com.getcapacitor.JSObject import com.getcapacitor.Plugin @@ -34,8 +35,9 @@ class CorePlugin : Plugin() { @PluginMethod fun getSupportedDevicesJson(call: PluginCall) { val ret = JSObject() - // TODO: ハードコードをやめてちゃんと取得する - ret.put("value", "{\"cpu\": true, \"cuda\": false, \"dml\": false}") + val supportedDevices = GlobalInfo.getSupportedDevices() + val supportedDevicesJson = gson.toJson(supportedDevices) + ret.put("value", supportedDevicesJson) call.resolve(ret) } @@ -86,12 +88,22 @@ class CorePlugin : Plugin() { Log.e("CorePlugin", "Couldn't get vvms") return } + vvms.sortWith(compareBy { + it.name.split(".")[0].length + }) voiceModels = vvms.map { VoiceModel(it.absolutePath) } + // Rustのtempfileクレートのための設定。 + // /data/local/tmp はAndroid 10から書き込めなくなった。そのため、 + // filesのディレクトリ内に一時フォルダを用意してそこから書き込むように設定する。 + val tempDir = File(activity.filesDir.absolutePath + "/.tmp") + tempDir.mkdirs() + Os.setenv("TMPDIR", tempDir.absolutePath, true) + call.resolve() - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -112,7 +124,7 @@ class CorePlugin : Plugin() { } synthesizer.loadVoiceModel(model) call.resolve() - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -135,7 +147,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", result) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -154,7 +166,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", gson.toJson(audioQuery)) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -173,7 +185,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", gson.toJson(accentPhrases)) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -194,7 +206,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", gson.toJson(newAccentPhrases)) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -215,7 +227,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", gson.toJson(newAccentPhrases)) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -236,7 +248,7 @@ class CorePlugin : Plugin() { val ret = JSObject() ret.put("value", gson.toJson(newAccentPhrases)) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { call.reject(e.message) } } @@ -260,7 +272,28 @@ class CorePlugin : Plugin() { val encodedResult = Base64.getEncoder().encodeToString(result) ret.put("value", encodedResult) call.resolve(ret) - } catch (e: VoicevoxException) { + } catch (e: Exception) { + call.reject(e.message) + } + } + + @PluginMethod + fun useUserDict(call: PluginCall) { + val wordsJson = call.getString("wordsJson") + if (wordsJson == null) { + call.reject("Type mismatch") + return + } + + try { + val words = gson.fromJson(wordsJson, Array::class.java).asList() + val userDict = UserDict() + words.forEach { word -> + userDict.addWord(word) + } + openJtalk.useUserDict(userDict) + call.resolve() + } catch (e: Exception) { call.reject(e.message) } } diff --git a/src/mobile/engine/dict.ts b/src/mobile/engine/dict.ts index 2bcc06ecb6..56f2b208bf 100644 --- a/src/mobile/engine/dict.ts +++ b/src/mobile/engine/dict.ts @@ -1,20 +1,34 @@ import { Preferences } from "@capacitor/preferences"; import { v4 as uuidv4 } from "uuid"; +import { VoicevoxCorePlugin } from "../plugin"; import { ApiProvider } from "."; import { UserDictWord } from "@/openapi"; type InternalUserDict = Record; const preferenceKey = "userDict"; -const getUserDictWords = async () => { +export const getUserDictWords = async () => { const userDictJson = await Preferences.get({ key: preferenceKey }); const dict: InternalUserDict = userDictJson.value ? JSON.parse(userDictJson.value) : {}; return dict; }; -const setUserDictWords = async (dict: InternalUserDict) => { - await Preferences.set({ key: preferenceKey, value: JSON.stringify(dict) }); +export const useUserDictWords = async ( + corePlugin: VoicevoxCorePlugin, + dict: InternalUserDict +) => { + await corePlugin.useUserDict({ + wordsJson: JSON.stringify( + Object.values(dict).map((word) => ({ + surface: word.surface, + pronunciation: word.pronunciation, + accent_type: word.accentType, + priority: word.priority, + word_type: word.wordType, + })) + ), + }); }; type InternalWordType = @@ -90,7 +104,11 @@ const apiWordToInternalWord = (word: UserDictWord): InternalDictWord => { }; }; -const dictProvider: ApiProvider = () => { +const dictProvider: ApiProvider = ({ corePlugin }) => { + const setUserDictWords = async (dict: InternalUserDict) => { + await Preferences.set({ key: preferenceKey, value: JSON.stringify(dict) }); + await useUserDictWords(corePlugin, dict); + }; return { async getUserDictWordsUserDictGet() { const dict = await getUserDictWords(); @@ -112,9 +130,6 @@ const dictProvider: ApiProvider = () => { ); }, async addUserDictWordUserDictWordPost(word) { - if (!word.wordType) { - throw new Error("wordType is required"); - } const uuid = uuidv4(); const dict = await getUserDictWords(); dict[uuid] = { @@ -122,7 +137,7 @@ const dictProvider: ApiProvider = () => { pronunciation: word.pronunciation, accentType: word.accentType, priority: word.priority ?? 5, - wordType: word.wordType, + wordType: word.wordType ?? "COMMON_NOUN", }; await setUserDictWords(dict); return uuid; diff --git a/src/mobile/engine/index.ts b/src/mobile/engine/index.ts index d6af74fef8..965bc5abb6 100644 --- a/src/mobile/engine/index.ts +++ b/src/mobile/engine/index.ts @@ -2,7 +2,7 @@ import { VoicevoxCorePlugin } from "../plugin"; import queryProvider from "./query"; import infoProvider from "./info"; import speakerProvider from "./speaker"; -import dictProvider from "./dict"; +import dictProvider, { getUserDictWords, useUserDictWords } from "./dict"; import { DefaultApi, DefaultApiInterface } from "@/openapi"; let api: DefaultApi | undefined; @@ -16,9 +16,11 @@ const loadApi = () => { const corePlugin = window.plugins?.voicevoxCore; if (!corePlugin) throw new Error("assert: corePlugin != null"); let isCoreInitialized = false; - corePlugin.initialize().then(() => { + (async () => { + await corePlugin.initialize(); + await useUserDictWords(corePlugin, await getUserDictWords()); isCoreInitialized = true; - }); + })(); // コアベースのOpenAPI Connectorライクなオブジェクト。 // - コアベースの実装がある場合は呼び出し、 diff --git a/src/mobile/engine/manifestAssets/base.json b/src/mobile/engine/manifestAssets/base.json index 98ec9b9194..cf3df651bc 100644 --- a/src/mobile/engine/manifestAssets/base.json +++ b/src/mobile/engine/manifestAssets/base.json @@ -17,7 +17,7 @@ "adjust_intonation_scale": true, "adjust_volume_scale": true, "interrogative_upspeak": true, - "synthesis_morphing": true, + "synthesis_morphing": false, "manage_library": false } } diff --git a/src/mobile/plugin.ts b/src/mobile/plugin.ts index a3b01e6bd6..826f8ce14e 100644 --- a/src/mobile/plugin.ts +++ b/src/mobile/plugin.ts @@ -38,7 +38,7 @@ export type VoicevoxCorePlugin = { enableInterrogativeUpspeak: boolean; }) => Promise<{ value: string }>; - userDictLoad: (obj: { dictJson: string }) => Promise; + useUserDict: (obj: { wordsJson: string }) => Promise; }; const loadPlugin = () => { From 5dbfead7cb80bcfd97f3cb220951e2f253889b32 Mon Sep 17 00:00:00 2001 From: sevenc-nanashi Date: Wed, 15 Nov 2023 20:50:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Change:=20=E3=82=AD=E3=83=A3=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A5=E3=83=87=E3=82=A3=E3=83=AC=E3=82=AF=E3=83=88?= =?UTF-8?q?=E3=83=AA=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt b/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt index 4a3d111044..cfe4dde3ea 100644 --- a/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt +++ b/android/app/src/main/java/jp/hiroshiba/voicevox/CorePlugin.kt @@ -97,8 +97,8 @@ class CorePlugin : Plugin() { // Rustのtempfileクレートのための設定。 // /data/local/tmp はAndroid 10から書き込めなくなった。そのため、 - // filesのディレクトリ内に一時フォルダを用意してそこから書き込むように設定する。 - val tempDir = File(activity.filesDir.absolutePath + "/.tmp") + // キャッシュディレクトリ内に一時フォルダを用意してそこから書き込むように設定する。 + val tempDir = File(activity.cacheDir.absolutePath + "/.tmp") tempDir.mkdirs() Os.setenv("TMPDIR", tempDir.absolutePath, true)