mirror of
https://github.com/readest/readest.git
synced 2026-04-30 04:20:53 +00:00
tts: add native tts plugin for Android (#1376)
This commit is contained in:
parent
69d418aa61
commit
f8ac30adf1
45 changed files with 2256 additions and 3 deletions
17
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/.gitignore
vendored
Normal file
17
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/.vs
|
||||
.DS_Store
|
||||
.Thumbs.db
|
||||
*.sublime*
|
||||
.idea/
|
||||
debug.log
|
||||
package-lock.json
|
||||
.vscode/settings.json
|
||||
yarn.lock
|
||||
|
||||
/.tauri
|
||||
/target
|
||||
Cargo.lock
|
||||
node_modules/
|
||||
|
||||
dist-js
|
||||
dist
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "tauri-plugin-native-tts"
|
||||
version = "0.1.0"
|
||||
authors = [ "You" ]
|
||||
description = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"]
|
||||
links = "tauri-plugin-native-tts"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.5.0" }
|
||||
serde = "1.0"
|
||||
thiserror = "2"
|
||||
schemars = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.2.0", features = ["build"] }
|
||||
schemars = "0.8"
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Tauri Plugin native-tts
|
||||
2
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/.gitignore
vendored
Normal file
2
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/build
|
||||
/.tauri
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.readest.native_tts"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
21
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/proguard-rules.pro
vendored
Normal file
21
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
resolutionStrategy {
|
||||
eachPlugin {
|
||||
switch (requested.id.id) {
|
||||
case "com.android.library":
|
||||
useVersion("8.0.2")
|
||||
break
|
||||
case "org.jetbrains.kotlin.android":
|
||||
useVersion("1.8.20")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.readest.native-tts
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.readest.native-tts", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,485 @@
|
|||
package com.readest.native_tts
|
||||
|
||||
import android.os.Bundle
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import android.speech.tts.Voice
|
||||
import android.util.Log
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.PluginResult
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
data class TTSVoiceData(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val disabled: Boolean = false
|
||||
)
|
||||
|
||||
data class TTSMessageEvent(
|
||||
val code: String, // 'boundary' | 'error' | 'end'
|
||||
val message: String? = null,
|
||||
val mark: String? = null
|
||||
)
|
||||
|
||||
enum class TTSGranularity(val value: String) {
|
||||
WORD("word"),
|
||||
SENTENCE("sentence"),
|
||||
PARAGRAPH("paragraph")
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NativeTTSPlugin"
|
||||
private const val CHANNEL_NAME = "tts_events"
|
||||
}
|
||||
|
||||
private var textToSpeech: TextToSpeech? = null
|
||||
private var isInitialized = AtomicBoolean(false)
|
||||
private var isPaused = AtomicBoolean(false)
|
||||
private var isSpeaking = AtomicBoolean(false)
|
||||
private var currentVoiceId = AtomicReference<String>("")
|
||||
private var currentLang = AtomicReference<String>("en-US")
|
||||
private var currentRate = AtomicReference<Float>(1.0f)
|
||||
private var currentPitch = AtomicReference<Float>(1.0f)
|
||||
|
||||
// Event channels for each speaking session
|
||||
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
|
||||
private val speakingJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
@Command
|
||||
fun init(invoke: Invoke) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val success = initializeTTS()
|
||||
val result = JSObject().apply {
|
||||
put("success", success)
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize TTS", e)
|
||||
invoke.reject("Failed to initialize TTS: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initializeTTS(): Boolean = suspendCancellableCoroutine { continuation ->
|
||||
try {
|
||||
textToSpeech = TextToSpeech(activity) { status ->
|
||||
when (status) {
|
||||
TextToSpeech.SUCCESS -> {
|
||||
setupTTSListener()
|
||||
isInitialized.set(true)
|
||||
continuation.resume(true) {}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "TTS initialization failed with status: $status")
|
||||
continuation.resume(false) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception during TTS initialization", e)
|
||||
continuation.resume(false) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTTSListener() {
|
||||
textToSpeech?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(true)
|
||||
sendEvent(id, TTSMessageEvent("boundary", "start"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDone(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(false)
|
||||
sendEvent(id, TTSMessageEvent("end"))
|
||||
closeEventChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(utteranceId: String?) {
|
||||
utteranceId?.let { id ->
|
||||
isSpeaking.set(false)
|
||||
sendEvent(id, TTSMessageEvent("error", "TTS playback error"))
|
||||
closeEventChannel(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRangeStart(utteranceId: String?, start: Int, end: Int, frame: Int) {
|
||||
utteranceId?.let { id ->
|
||||
sendEvent(id, TTSMessageEvent("boundary", "range", "pos:$start-$end"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Command
|
||||
fun speak(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SpeakArgs::class.java)
|
||||
|
||||
if (!isInitialized.get()) {
|
||||
invoke.reject("TTS not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
val utteranceId = UUID.randomUUID().toString()
|
||||
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val eventChannel = Channel<TTSMessageEvent>(Channel.UNLIMITED)
|
||||
eventChannels[utteranceId] = eventChannel
|
||||
|
||||
val speakJob = launch {
|
||||
speakText(args.ssml, utteranceId, args.preload ?: false)
|
||||
}
|
||||
speakingJobs[utteranceId] = speakJob
|
||||
|
||||
// Return the utterance ID so frontend can listen to events
|
||||
val result = JSObject().apply {
|
||||
put("utteranceId", utteranceId)
|
||||
}
|
||||
invoke.resolve(result)
|
||||
|
||||
// Start sending events to the frontend
|
||||
startEventStream(utteranceId)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start speaking", e)
|
||||
invoke.reject("Failed to start speaking: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun speakText(ssml: String, utteranceId: String, preload: Boolean) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
// Parse SSML and extract text
|
||||
val text = parseSSML(ssml)
|
||||
|
||||
textToSpeech?.apply {
|
||||
setSpeechRate(currentRate.get())
|
||||
setPitch(currentPitch.get())
|
||||
}
|
||||
|
||||
val params = Bundle().apply {
|
||||
putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId)
|
||||
}
|
||||
|
||||
val result = textToSpeech?.speak(
|
||||
text,
|
||||
if (preload) TextToSpeech.QUEUE_ADD else TextToSpeech.QUEUE_FLUSH,
|
||||
params,
|
||||
utteranceId
|
||||
)
|
||||
|
||||
if (result != TextToSpeech.SUCCESS) {
|
||||
sendEvent(utteranceId, TTSMessageEvent("error", "Failed to start speech"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
sendEvent(utteranceId, TTSMessageEvent("error", "Exception during speech: ${e.message}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSSML(ssml: String): String {
|
||||
// Simple SSML parsing - extract text content
|
||||
return ssml
|
||||
.replace(Regex("<[^>]*>"), " ")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.trim()
|
||||
}
|
||||
|
||||
private fun startEventStream(utteranceId: String) {
|
||||
coroutineScope.launch {
|
||||
val channel = eventChannels[utteranceId] ?: return@launch
|
||||
|
||||
try {
|
||||
for (event in channel) {
|
||||
val eventData = JSObject().apply {
|
||||
put("utteranceId", utteranceId)
|
||||
put("code", event.code)
|
||||
event.message?.let { put("message", it) }
|
||||
event.mark?.let { put("mark", it) }
|
||||
}
|
||||
|
||||
// Send event to frontend via Tauri event system
|
||||
trigger(CHANNEL_NAME, eventData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in event stream for $utteranceId", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendEvent(utteranceId: String, event: TTSMessageEvent) {
|
||||
coroutineScope.launch {
|
||||
eventChannels[utteranceId]?.trySend(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeEventChannel(utteranceId: String) {
|
||||
coroutineScope.launch {
|
||||
eventChannels[utteranceId]?.close()
|
||||
eventChannels.remove(utteranceId)
|
||||
speakingJobs[utteranceId]?.cancel()
|
||||
speakingJobs.remove(utteranceId)
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun pause(invoke: Invoke) {
|
||||
try {
|
||||
if (textToSpeech?.stop() == TextToSpeech.SUCCESS) {
|
||||
isPaused.set(true)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to pause TTS")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while pausing: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun resume(invoke: Invoke) {
|
||||
// Android TTS doesn't have native resume, so we'll need to track state
|
||||
try {
|
||||
isPaused.set(false)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while resuming: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun stop(invoke: Invoke) {
|
||||
try {
|
||||
if (textToSpeech?.stop() == TextToSpeech.SUCCESS) {
|
||||
isSpeaking.set(false)
|
||||
isPaused.set(false)
|
||||
|
||||
// Cancel all active speaking jobs and close channels
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
eventChannels.values.forEach { it.close() }
|
||||
speakingJobs.clear()
|
||||
eventChannels.clear()
|
||||
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to stop TTS")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception while stopping: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_primary_lang(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetLangArgs::class.java)
|
||||
try {
|
||||
val locale = Locale.forLanguageTag(args.lang)
|
||||
val result = textToSpeech?.setLanguage(locale)
|
||||
|
||||
when (result) {
|
||||
TextToSpeech.LANG_AVAILABLE,
|
||||
TextToSpeech.LANG_COUNTRY_AVAILABLE,
|
||||
TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE -> {
|
||||
currentLang.set(args.lang)
|
||||
invoke.resolve()
|
||||
}
|
||||
else -> {
|
||||
invoke.reject("Language not supported: ${args.lang}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting language: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_rate(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetRateArgs::class.java)
|
||||
try {
|
||||
currentRate.set(args.rate)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting rate: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_pitch(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetPitchArgs::class.java)
|
||||
try {
|
||||
currentPitch.set(args.pitch)
|
||||
invoke.resolve()
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting pitch: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun set_voice(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(SetVoiceArgs::class.java)
|
||||
try {
|
||||
val voices = textToSpeech?.voices
|
||||
val targetVoice = voices?.find { it.name == args.voice }
|
||||
|
||||
if (targetVoice != null) {
|
||||
val result = textToSpeech?.setVoice(targetVoice)
|
||||
if (result == TextToSpeech.SUCCESS) {
|
||||
currentVoiceId.set(args.voice)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("Failed to set voice: ${args.voice}")
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Voice not found: ${args.voice}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception setting voice: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_all_voices(invoke: Invoke) {
|
||||
try {
|
||||
val voices = textToSpeech?.voices?.map { voice ->
|
||||
JSObject().apply {
|
||||
put("id", voice.name)
|
||||
put("name", voice.name)
|
||||
put("lang", voice.locale.toLanguageTag())
|
||||
put("disabled", false)
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("voices", JSONArray(voices))
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voices: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_voices(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(GetVoicesArgs::class.java)
|
||||
try {
|
||||
val locale = Locale.forLanguageTag(args.lang)
|
||||
val voices = textToSpeech?.voices?.filter { voice ->
|
||||
voice.locale.language == locale.language
|
||||
}?.map { voice ->
|
||||
JSObject().apply {
|
||||
put("id", voice.name)
|
||||
put("name", voice.name)
|
||||
put("lang", voice.locale.toLanguageTag())
|
||||
put("disabled", false)
|
||||
}
|
||||
} ?: emptyList()
|
||||
|
||||
val result = JSObject().apply {
|
||||
put("voices", JSONArray(voices))
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voices for language: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_granularities(invoke: Invoke) {
|
||||
try {
|
||||
val granularities = TTSGranularity.values().map { it.value }
|
||||
val result = JSObject().apply {
|
||||
put("granularities", JSONArray(granularities))
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting granularities: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_voice_id(invoke: Invoke) {
|
||||
try {
|
||||
val result = JSObject().apply {
|
||||
put("voiceId", currentVoiceId.get())
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting voice ID: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_speaking_lang(invoke: Invoke) {
|
||||
try {
|
||||
val result = JSObject().apply {
|
||||
put("lang", currentLang.get())
|
||||
}
|
||||
invoke.resolve(result)
|
||||
} catch (e: Exception) {
|
||||
invoke.reject("Exception getting speaking language: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
coroutineScope.cancel()
|
||||
textToSpeech?.shutdown()
|
||||
eventChannels.values.forEach { it.close() }
|
||||
eventChannels.clear()
|
||||
speakingJobs.values.forEach { it.cancel() }
|
||||
speakingJobs.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes for command arguments
|
||||
data class SpeakArgs(
|
||||
val ssml: String,
|
||||
val preload: Boolean? = false
|
||||
)
|
||||
|
||||
data class SetLangArgs(
|
||||
val lang: String
|
||||
)
|
||||
|
||||
data class SetRateArgs(
|
||||
val rate: Float
|
||||
)
|
||||
|
||||
data class SetPitchArgs(
|
||||
val pitch: Float
|
||||
)
|
||||
|
||||
data class SetVoiceArgs(
|
||||
val voice: String
|
||||
)
|
||||
|
||||
data class GetVoicesArgs(
|
||||
val lang: String
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.readest.native-tts
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
const COMMANDS: &[&str] = &[
|
||||
"init",
|
||||
"speak",
|
||||
"stop",
|
||||
"pause",
|
||||
"resume",
|
||||
"set_rate",
|
||||
"set_pitch",
|
||||
"set_voice",
|
||||
"set_primary_lang",
|
||||
"get_voices",
|
||||
"get_voice_id",
|
||||
"get_all_voices",
|
||||
"get_granularities",
|
||||
"get_speaking_lang",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
.android_path("android")
|
||||
.ios_path("ios")
|
||||
.build();
|
||||
}
|
||||
10
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/ios/.gitignore
vendored
Normal file
10
apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/ios/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
Package.resolved
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// swift-tools-version:5.3
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "tauri-plugin-native-tts",
|
||||
platforms: [
|
||||
.macOS(.v10_13),
|
||||
.iOS(.v13),
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "tauri-plugin-native-tts",
|
||||
type: .static,
|
||||
targets: ["tauri-plugin-native-tts"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "Tauri", path: "../.tauri/tauri-api")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "tauri-plugin-native-tts",
|
||||
dependencies: [
|
||||
.byName(name: "Tauri")
|
||||
],
|
||||
path: "Sources")
|
||||
]
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Tauri Plugin native-tts
|
||||
|
||||
A description of this package.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
class PingArgs: Decodable {
|
||||
let value: String?
|
||||
}
|
||||
|
||||
class NativeTTSPlugin: Plugin {
|
||||
@objc public func ping(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(PingArgs.self)
|
||||
invoke.resolve(["value": args.value ?? ""])
|
||||
}
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_native_tts")
|
||||
func initPlugin() -> Plugin {
|
||||
return NativeTTSPlugin()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import XCTest
|
||||
@testable import ExamplePlugin
|
||||
|
||||
final class ExamplePluginTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
let plugin = ExamplePlugin()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-all-voices"
|
||||
description = "Enables the get_all_voices command without any pre-configured scope."
|
||||
commands.allow = ["get_all_voices"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-all-voices"
|
||||
description = "Denies the get_all_voices command without any pre-configured scope."
|
||||
commands.deny = ["get_all_voices"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-granularities"
|
||||
description = "Enables the get_granularities command without any pre-configured scope."
|
||||
commands.allow = ["get_granularities"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-granularities"
|
||||
description = "Denies the get_granularities command without any pre-configured scope."
|
||||
commands.deny = ["get_granularities"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-speaking-lang"
|
||||
description = "Enables the get_speaking_lang command without any pre-configured scope."
|
||||
commands.allow = ["get_speaking_lang"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-speaking-lang"
|
||||
description = "Denies the get_speaking_lang command without any pre-configured scope."
|
||||
commands.deny = ["get_speaking_lang"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-voice-id"
|
||||
description = "Enables the get_voice_id command without any pre-configured scope."
|
||||
commands.allow = ["get_voice_id"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-voice-id"
|
||||
description = "Denies the get_voice_id command without any pre-configured scope."
|
||||
commands.deny = ["get_voice_id"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-voices"
|
||||
description = "Enables the get_voices command without any pre-configured scope."
|
||||
commands.allow = ["get_voices"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-voices"
|
||||
description = "Denies the get_voices command without any pre-configured scope."
|
||||
commands.deny = ["get_voices"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-init"
|
||||
description = "Enables the init command without any pre-configured scope."
|
||||
commands.allow = ["init"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-init"
|
||||
description = "Denies the init command without any pre-configured scope."
|
||||
commands.deny = ["init"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-pause"
|
||||
description = "Enables the pause command without any pre-configured scope."
|
||||
commands.allow = ["pause"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-pause"
|
||||
description = "Denies the pause command without any pre-configured scope."
|
||||
commands.deny = ["pause"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-resume"
|
||||
description = "Enables the resume command without any pre-configured scope."
|
||||
commands.allow = ["resume"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-resume"
|
||||
description = "Denies the resume command without any pre-configured scope."
|
||||
commands.deny = ["resume"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-pitch"
|
||||
description = "Enables the set_pitch command without any pre-configured scope."
|
||||
commands.allow = ["set_pitch"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-pitch"
|
||||
description = "Denies the set_pitch command without any pre-configured scope."
|
||||
commands.deny = ["set_pitch"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-primary-lang"
|
||||
description = "Enables the set_primary_lang command without any pre-configured scope."
|
||||
commands.allow = ["set_primary_lang"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-primary-lang"
|
||||
description = "Denies the set_primary_lang command without any pre-configured scope."
|
||||
commands.deny = ["set_primary_lang"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-rate"
|
||||
description = "Enables the set_rate command without any pre-configured scope."
|
||||
commands.allow = ["set_rate"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-rate"
|
||||
description = "Denies the set_rate command without any pre-configured scope."
|
||||
commands.deny = ["set_rate"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-set-voice"
|
||||
description = "Enables the set_voice command without any pre-configured scope."
|
||||
commands.allow = ["set_voice"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-set-voice"
|
||||
description = "Denies the set_voice command without any pre-configured scope."
|
||||
commands.deny = ["set_voice"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-speak"
|
||||
description = "Enables the speak command without any pre-configured scope."
|
||||
commands.allow = ["speak"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-speak"
|
||||
description = "Denies the speak command without any pre-configured scope."
|
||||
commands.deny = ["speak"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-stop"
|
||||
description = "Enables the stop command without any pre-configured scope."
|
||||
commands.allow = ["stop"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-stop"
|
||||
description = "Denies the stop command without any pre-configured scope."
|
||||
commands.deny = ["stop"]
|
||||
|
|
@ -0,0 +1,394 @@
|
|||
## Default Permission
|
||||
|
||||
Default permissions for the plugin
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-init`
|
||||
- `allow-speak`
|
||||
- `allow-stop`
|
||||
- `allow-pause`
|
||||
- `allow-resume`
|
||||
- `allow-set-rate`
|
||||
- `allow-set-pitch`
|
||||
- `allow-set-voice`
|
||||
- `allow-set-primary-lang`
|
||||
- `allow-get-voices`
|
||||
- `allow-get-voice-id`
|
||||
- `allow-get-all-voices`
|
||||
- `allow-get-granularities`
|
||||
- `allow-get-speaking-lang`
|
||||
|
||||
## Permission Table
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Identifier</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-all-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_all_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-all-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_all_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-granularities`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_granularities command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-granularities`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_granularities command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-speaking-lang`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_speaking_lang command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-speaking-lang`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_speaking_lang command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-voice-id`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_voice_id command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-voice-id`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_voice_id command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-get-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-get-voices`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_voices command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-init`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the init command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-pause`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the pause command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-pause`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the pause command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-resume`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the resume command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-resume`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the resume command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-pitch`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_pitch command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-pitch`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_pitch command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-primary-lang`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_primary_lang command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-primary-lang`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_primary_lang command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-rate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_rate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-rate`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_rate command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-set-voice`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the set_voice command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-set-voice`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the set_voice command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-speak`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the speak command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-speak`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the speak command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:allow-stop`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the stop command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-tts:deny-stop`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the stop command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
[default]
|
||||
description = "Default permissions for the plugin"
|
||||
permissions = [
|
||||
"allow-init",
|
||||
"allow-speak",
|
||||
"allow-stop",
|
||||
"allow-pause",
|
||||
"allow-resume",
|
||||
"allow-set-rate",
|
||||
"allow-set-pitch",
|
||||
"allow-set-voice",
|
||||
"allow-set-primary-lang",
|
||||
"allow-get-voices",
|
||||
"allow-get-voice-id",
|
||||
"allow-get-all-voices",
|
||||
"allow-get-granularities",
|
||||
"allow-get-speaking-lang",
|
||||
]
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PermissionFile",
|
||||
"description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "The default permission set for the plugin",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/DefaultPermission"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"set": {
|
||||
"description": "A list of permissions sets defined",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionSet"
|
||||
}
|
||||
},
|
||||
"permission": {
|
||||
"description": "A list of inlined permissions",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Permission"
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"DefaultPermission": {
|
||||
"description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"PermissionSet": {
|
||||
"description": "A set of direct permissions grouped together under a new name.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"identifier",
|
||||
"permissions"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does.",
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"description": "All permissions this set contains.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PermissionKind"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Permission": {
|
||||
"description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"version": {
|
||||
"description": "The version of the permission.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint64",
|
||||
"minimum": 1.0
|
||||
},
|
||||
"identifier": {
|
||||
"description": "A unique identifier for the permission.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
"description": "Allowed or denied commands when using this permission.",
|
||||
"default": {
|
||||
"allow": [],
|
||||
"deny": []
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Commands"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Allowed or denied scoped when using this permission.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Scopes"
|
||||
}
|
||||
]
|
||||
},
|
||||
"platforms": {
|
||||
"description": "Target platforms this permission applies. By default all platforms are affected by this permission.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Commands": {
|
||||
"description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Allowed command.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Denied command, which takes priority.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes": {
|
||||
"description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {
|
||||
"description": "Data that defines what is allowed by the scope.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
"deny": {
|
||||
"description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Value": {
|
||||
"description": "All supported ACL values.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents a null JSON value.",
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`bool`].",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"description": "Represents a valid ACL [`Number`].",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`String`].",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Represents a list of other [`Value`]s.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Represents a map of [`String`] keys to [`Value`]s.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/Value"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Number": {
|
||||
"description": "A valid ACL number.",
|
||||
"anyOf": [
|
||||
{
|
||||
"description": "Represents an [`i64`].",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
{
|
||||
"description": "Represents a [`f64`].",
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Target": {
|
||||
"description": "Platform target.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "MacOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"macOS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Windows.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"windows"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Linux.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Android.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "iOS.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"iOS"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"PermissionKind": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Enables the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-all-voices",
|
||||
"markdownDescription": "Enables the get_all_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_all_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-all-voices",
|
||||
"markdownDescription": "Denies the get_all_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_granularities command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-granularities",
|
||||
"markdownDescription": "Enables the get_granularities command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_granularities command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-granularities",
|
||||
"markdownDescription": "Denies the get_granularities command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_speaking_lang command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-speaking-lang",
|
||||
"markdownDescription": "Enables the get_speaking_lang command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_speaking_lang command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-speaking-lang",
|
||||
"markdownDescription": "Denies the get_speaking_lang command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_voice_id command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-voice-id",
|
||||
"markdownDescription": "Enables the get_voice_id command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_voice_id command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-voice-id",
|
||||
"markdownDescription": "Denies the get_voice_id command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-voices",
|
||||
"markdownDescription": "Enables the get_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_voices command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-voices",
|
||||
"markdownDescription": "Denies the get_voices command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-init",
|
||||
"markdownDescription": "Enables the init command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the init command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-init",
|
||||
"markdownDescription": "Denies the init command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the pause command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-pause",
|
||||
"markdownDescription": "Enables the pause command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the pause command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-pause",
|
||||
"markdownDescription": "Denies the pause command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-resume",
|
||||
"markdownDescription": "Enables the resume command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the resume command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-resume",
|
||||
"markdownDescription": "Denies the resume command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_pitch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-pitch",
|
||||
"markdownDescription": "Enables the set_pitch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_pitch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-pitch",
|
||||
"markdownDescription": "Denies the set_pitch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_primary_lang command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-primary-lang",
|
||||
"markdownDescription": "Enables the set_primary_lang command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_primary_lang command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-primary-lang",
|
||||
"markdownDescription": "Denies the set_primary_lang command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_rate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-rate",
|
||||
"markdownDescription": "Enables the set_rate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_rate command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-rate",
|
||||
"markdownDescription": "Denies the set_rate command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the set_voice command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-set-voice",
|
||||
"markdownDescription": "Enables the set_voice command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the set_voice command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-set-voice",
|
||||
"markdownDescription": "Denies the set_voice command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the speak command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-speak",
|
||||
"markdownDescription": "Enables the speak command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the speak command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-speak",
|
||||
"markdownDescription": "Denies the speak command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the stop command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-stop",
|
||||
"markdownDescription": "Enables the stop command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the stop command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-stop",
|
||||
"markdownDescription": "Denies the stop command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-set-primary-lang`\n- `allow-get-voices`\n- `allow-get-voice-id`\n- `allow-get-all-voices`\n- `allow-get-granularities`\n- `allow-get-speaking-lang`",
|
||||
"type": "string",
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-set-primary-lang`\n- `allow-get-voices`\n- `allow-get-voice-id`\n- `allow-get-all-voices`\n- `allow-get-granularities`\n- `allow-get-speaking-lang`"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
use tauri::{command, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
use crate::NativeTtsExt;
|
||||
use crate::Result;
|
||||
|
||||
#[command]
|
||||
pub async fn init<R: Runtime>(app: AppHandle<R>) -> Result<bool> {
|
||||
app.native_tts().init()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn speak<R: Runtime>(app: AppHandle<R>, args: SpeakArgs) -> Result<String> {
|
||||
app.native_tts().speak(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn pause<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().pause()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn resume<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().resume()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn stop<R: Runtime>(app: AppHandle<R>) -> Result<()> {
|
||||
app.native_tts().stop()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn set_primary_lang<R: Runtime>(app: AppHandle<R>, args: SetLangArgs) -> Result<()> {
|
||||
app.native_tts().set_primary_lang(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn set_rate<R: Runtime>(app: AppHandle<R>, args: SetRateArgs) -> Result<()> {
|
||||
app.native_tts().set_rate(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn set_pitch<R: Runtime>(app: AppHandle<R>, args: SetPitchArgs) -> Result<()> {
|
||||
app.native_tts().set_pitch(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn set_voice<R: Runtime>(app: AppHandle<R>, args: SetVoiceArgs) -> Result<()> {
|
||||
app.native_tts().set_voice(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_all_voices<R: Runtime>(app: AppHandle<R>) -> Result<Vec<TTSVoice>> {
|
||||
app.native_tts().get_all_voices()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_voices<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
args: GetVoicesArgs,
|
||||
) -> Result<Vec<TTSVoice>> {
|
||||
app.native_tts().get_voices(args)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_granularities<R: Runtime>(app: AppHandle<R>) -> Result<Vec<TTSGranularity>> {
|
||||
app.native_tts().get_granularities()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_voice_id<R: Runtime>(app: AppHandle<R>) -> Result<String> {
|
||||
app.native_tts().get_voice_id()
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn get_speaking_lang<R: Runtime>(app: AppHandle<R>) -> Result<String> {
|
||||
app.native_tts().get_speaking_lang()
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
app: &AppHandle<R>,
|
||||
_api: PluginApi<R, C>,
|
||||
) -> crate::Result<NativeTts<R>> {
|
||||
Ok(NativeTts(app.clone()))
|
||||
}
|
||||
|
||||
/// Access to the native-tts APIs.
|
||||
pub struct NativeTts<R: Runtime>(AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn init(&self) -> crate::Result<bool> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn speak(&self, _args: SpeakArgs) -> crate::Result<String> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn pause(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn resume(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn stop(&self) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_primary_lang(&self, _args: SetLangArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_rate(&self, _args: SetRateArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_pitch(&self, _args: SetPitchArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn set_voice(&self, _args: SetVoiceArgs) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_all_voices(&self) -> crate::Result<Vec<TTSVoice>> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_voices(&self, _args: GetVoicesArgs) -> crate::Result<Vec<TTSVoice>> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_granularities(&self) -> crate::Result<Vec<TTSGranularity>> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_voice_id(&self) -> crate::Result<String> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
pub fn get_speaking_lang(&self) -> crate::Result<String> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unsupported platform for this plugin")]
|
||||
UnsupportedPlatformError,
|
||||
#[error("Native tts error: {0}")]
|
||||
NativeTTSError(String),
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[cfg(mobile)]
|
||||
#[error(transparent)]
|
||||
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
mod desktop;
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
mod commands;
|
||||
mod error;
|
||||
mod models;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
#[cfg(desktop)]
|
||||
use desktop::NativeTts;
|
||||
#[cfg(mobile)]
|
||||
use mobile::NativeTts;
|
||||
|
||||
/// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the native-tts APIs.
|
||||
pub trait NativeTtsExt<R: Runtime> {
|
||||
fn native_tts(&self) -> &NativeTts<R>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> crate::NativeTtsExt<R> for T {
|
||||
fn native_tts(&self) -> &NativeTts<R> {
|
||||
self.state::<NativeTts<R>>().inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("native-tts")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::init,
|
||||
commands::speak,
|
||||
commands::stop,
|
||||
commands::pause,
|
||||
commands::resume,
|
||||
commands::set_rate,
|
||||
commands::set_pitch,
|
||||
commands::set_voice,
|
||||
commands::set_primary_lang,
|
||||
commands::get_voices,
|
||||
commands::get_voice_id,
|
||||
commands::get_all_voices,
|
||||
commands::get_granularities,
|
||||
commands::get_speaking_lang
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
let native_tts = mobile::init(app, api)?;
|
||||
#[cfg(desktop)]
|
||||
let native_tts = desktop::init(app, api)?;
|
||||
app.manage(native_tts);
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
use serde::de::DeserializeOwned;
|
||||
use tauri::{
|
||||
plugin::{PluginApi, PluginHandle},
|
||||
AppHandle, Runtime,
|
||||
};
|
||||
|
||||
use crate::models::*;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
tauri::ios_plugin_binding!(init_plugin_native_tts);
|
||||
|
||||
// initializes the Kotlin or Swift plugin classes
|
||||
pub fn init<R: Runtime, C: DeserializeOwned>(
|
||||
_app: &AppHandle<R>,
|
||||
api: PluginApi<R, C>,
|
||||
) -> crate::Result<NativeTts<R>> {
|
||||
#[cfg(target_os = "android")]
|
||||
let handle = api.register_android_plugin("com.readest.native_tts", "NativeTTSPlugin")?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let handle = api.register_ios_plugin(init_plugin_native_tts)?;
|
||||
Ok(NativeTts(handle))
|
||||
}
|
||||
|
||||
/// Access to the native-tts APIs.
|
||||
pub struct NativeTts<R: Runtime>(PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn init(&self) -> crate::Result<bool> {
|
||||
self.0.run_mobile_plugin("init", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn speak(&self, args: SpeakArgs) -> crate::Result<String> {
|
||||
self.0.run_mobile_plugin("speak", args).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn pause(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("pause", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn resume(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("resume", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn stop(&self) -> crate::Result<()> {
|
||||
self.0.run_mobile_plugin("stop", ()).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_primary_lang(&self, args: SetLangArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_primary_lang", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_rate(&self, args: SetRateArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_rate", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_pitch(&self, args: SetPitchArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_pitch", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn set_voice(&self, args: SetVoiceArgs) -> crate::Result<()> {
|
||||
self.0
|
||||
.run_mobile_plugin("set_voice", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_voices(&self, args: GetVoicesArgs) -> crate::Result<Vec<TTSVoice>> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_voices", args)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_all_voices(&self) -> crate::Result<Vec<TTSVoice>> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_all_voices", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_granularities(&self) -> crate::Result<Vec<TTSGranularity>> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_granularities", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_voice_id(&self) -> crate::Result<String> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_voice_id", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeTts<R> {
|
||||
pub fn get_speaking_lang(&self) -> crate::Result<String> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_speaking_lang", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TTSVoice {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub lang: String,
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TTSMessageEvent {
|
||||
pub code: String, // 'boundary' | 'error' | 'end'
|
||||
pub message: Option<String>,
|
||||
pub mark: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum TTSGranularity {
|
||||
#[serde(rename = "word")]
|
||||
Word,
|
||||
#[serde(rename = "sentence")]
|
||||
Sentence,
|
||||
#[serde(rename = "paragraph")]
|
||||
Paragraph,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SpeakArgs {
|
||||
pub ssml: String,
|
||||
#[serde(default)]
|
||||
pub preload: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SetLangArgs {
|
||||
pub lang: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SetRateArgs {
|
||||
pub rate: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SetPitchArgs {
|
||||
pub pitch: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SetVoiceArgs {
|
||||
pub voice: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetVoicesArgs {
|
||||
pub lang: String,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue