expo-speech 9.1.0 → 10.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/android/build.gradle +27 -14
- package/android/src/main/java/expo/modules/speech/LanguageUtils.kt +34 -0
- package/android/src/main/java/expo/modules/speech/SpeechModule.kt +191 -0
- package/android/src/main/java/expo/modules/speech/SpeechOptions.kt +58 -0
- package/android/src/main/java/expo/modules/speech/SpeechPackage.kt +8 -0
- package/build/ExponentSpeech.d.ts +1 -1
- package/build/ExponentSpeech.js +1 -1
- package/build/ExponentSpeech.js.map +1 -1
- package/build/ExponentSpeech.web.d.ts +1 -1
- package/build/ExponentSpeech.web.js +25 -5
- package/build/ExponentSpeech.web.js.map +1 -1
- package/build/Speech.d.ts +31 -2
- package/build/Speech.js +44 -2
- package/build/Speech.js.map +1 -1
- package/build/Speech.types.d.ts +44 -0
- package/build/Speech.types.js +4 -0
- package/build/Speech.types.js.map +1 -1
- package/ios/EXSpeech/EXSpeech.h +4 -4
- package/ios/EXSpeech/EXSpeech.m +25 -25
- package/ios/EXSpeech.podspec +3 -2
- package/package.json +6 -3
- package/src/Speech/ExponentSpeech.ts +1 -1
- package/src/Speech/ExponentSpeech.web.ts +33 -5
- package/src/Speech/Speech.ts +46 -4
- package/src/Speech/Speech.types.ts +48 -1
- package/android/src/main/java/expo/modules/speech/LanguageUtils.java +0 -48
- package/android/src/main/java/expo/modules/speech/SpeechModule.java +0 -238
- package/android/src/main/java/expo/modules/speech/SpeechPackage.java +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,40 @@
|
|
|
8
8
|
|
|
9
9
|
### 🐛 Bug fixes
|
|
10
10
|
|
|
11
|
+
### 💡 Others
|
|
12
|
+
|
|
13
|
+
## 10.0.1 — 2021-10-01
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 10.0.0 — 2021-09-28
|
|
18
|
+
|
|
19
|
+
### 🛠 Breaking changes
|
|
20
|
+
|
|
21
|
+
- Dropped support for iOS 11.0 ([#14383](https://github.com/expo/expo/pull/14383) by [@cruzach](https://github.com/cruzach))
|
|
22
|
+
|
|
23
|
+
### 🐛 Bug fixes
|
|
24
|
+
|
|
25
|
+
- Fix setting speaking listener for projects with `react-native@>0.64.0`. ([#13654](https://github.com/expo/expo/pull/13654) by [@dsokal](https://github.com/dsokal))
|
|
26
|
+
- Fix empty voices list on web and allow to change voice when using `speak`. ([#4516](https://github.com/expo/expo/pull/14516) by [@Federkun](https://github.com/Federkun))
|
|
27
|
+
- Fix building errors from use_frameworks! in Podfile. ([#14523](https://github.com/expo/expo/pull/14523) by [@kudo](https://github.com/kudo))
|
|
28
|
+
|
|
29
|
+
### 💡 Others
|
|
30
|
+
|
|
31
|
+
- Migrated from `@unimodules/core` to `expo-modules-core`. ([#13757](https://github.com/expo/expo/pull/13757) by [@tsapeta](https://github.com/tsapeta))
|
|
32
|
+
- Rewritten Android code to Kotlin. ([#14008](https://github.com/expo/expo/pull/14008) by [@barthap](https://github.com/barthap))
|
|
33
|
+
|
|
34
|
+
## 9.2.0 — 2021-06-16
|
|
35
|
+
|
|
36
|
+
### 🐛 Bug fixes
|
|
37
|
+
|
|
38
|
+
- Enable kotlin in all modules. ([#12716](https://github.com/expo/expo/pull/12716) by [@wschurman](https://github.com/wschurman))
|
|
39
|
+
|
|
40
|
+
### 💡 Others
|
|
41
|
+
|
|
42
|
+
- Build Android code using Java 8 to fix Android instrumented test build error. ([#12939](https://github.com/expo/expo/pull/12939) by [@kudo](https://github.com/kudo))
|
|
43
|
+
- Export missing `WebVoice` type. ([#13257](https://github.com/expo/expo/pull/13257) by [@Simek](https://github.com/Simek))
|
|
44
|
+
|
|
11
45
|
## 9.1.0 — 2021-03-10
|
|
12
46
|
|
|
13
47
|
### 🎉 New features
|
package/android/build.gradle
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
2
3
|
apply plugin: 'maven'
|
|
3
4
|
|
|
4
5
|
group = 'host.exp.exponent'
|
|
5
|
-
version = '
|
|
6
|
+
version = '10.0.1'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
buildscript {
|
|
9
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
10
|
+
ext.safeExtGet = { prop, fallback ->
|
|
11
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
repositories {
|
|
15
|
+
mavenCentral()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
dependencies {
|
|
19
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet('kotlinVersion', '1.4.21')}")
|
|
20
|
+
}
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
// Upload android library to maven with javadoc and android sources
|
|
@@ -37,25 +48,27 @@ uploadArchives {
|
|
|
37
48
|
android {
|
|
38
49
|
compileSdkVersion safeExtGet("compileSdkVersion", 30)
|
|
39
50
|
|
|
51
|
+
compileOptions {
|
|
52
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
53
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
defaultConfig {
|
|
41
57
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
42
58
|
targetSdkVersion safeExtGet("targetSdkVersion", 30)
|
|
43
59
|
versionCode 18
|
|
44
|
-
versionName "
|
|
60
|
+
versionName "10.0.1"
|
|
45
61
|
}
|
|
46
62
|
lintOptions {
|
|
47
63
|
abortOnError false
|
|
48
64
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
apply from: project(":unimodules-core").file("../unimodules-core.gradle")
|
|
53
|
-
} else {
|
|
54
|
-
throw new GradleException(
|
|
55
|
-
"'unimodules-core.gradle' was not found in the usual React Native dependency location. " +
|
|
56
|
-
"This package can only be used in such projects. Are you sure you've installed the dependencies properly?")
|
|
65
|
+
kotlinOptions {
|
|
66
|
+
jvmTarget = JavaVersion.VERSION_1_8
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
dependencies {
|
|
60
|
-
|
|
71
|
+
implementation project(':expo-modules-core')
|
|
72
|
+
|
|
73
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${safeExtGet('kotlinVersion', '1.4.21')}"
|
|
61
74
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package expo.modules.speech
|
|
2
|
+
|
|
3
|
+
import java.util.*
|
|
4
|
+
|
|
5
|
+
// Lazy load the ISO codes into a Map then transform the codes to match other localization patterns in Expo
|
|
6
|
+
object LanguageUtils {
|
|
7
|
+
private val countryISOCodes: Map<String, Locale> by lazy {
|
|
8
|
+
Locale.getISOCountries().map { country ->
|
|
9
|
+
val locale = Locale("", country)
|
|
10
|
+
locale.getISO3Country().toUpperCase(locale) to locale
|
|
11
|
+
}.toMap()
|
|
12
|
+
}
|
|
13
|
+
private val languageISOCodes: Map<String, Locale> by lazy {
|
|
14
|
+
Locale.getISOLanguages().map { language ->
|
|
15
|
+
val locale = Locale(language)
|
|
16
|
+
locale.getISO3Language() to locale
|
|
17
|
+
}.toMap()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// NOTE: These helpers are null-unsafe and should be called ONLY with codes
|
|
21
|
+
// returned by Locale.getISO3Country() and Locale.getISO3Language() respectively
|
|
22
|
+
private fun transformCountryISO(code: String) = countryISOCodes[code]!!.country
|
|
23
|
+
private fun transformLanguageISO(code: String) = languageISOCodes[code]!!.language
|
|
24
|
+
|
|
25
|
+
fun getISOCode(locale: Locale): String {
|
|
26
|
+
var language = transformLanguageISO(locale.getISO3Language())
|
|
27
|
+
val country = locale.getISO3Country()
|
|
28
|
+
if (country != "") {
|
|
29
|
+
val countryCode = transformCountryISO(country)
|
|
30
|
+
language += "-$countryCode"
|
|
31
|
+
}
|
|
32
|
+
return language
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
package expo.modules.speech
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.speech.tts.TextToSpeech
|
|
6
|
+
import android.speech.tts.UtteranceProgressListener
|
|
7
|
+
import android.speech.tts.Voice
|
|
8
|
+
import expo.modules.core.ExportedModule
|
|
9
|
+
import expo.modules.core.ModuleRegistry
|
|
10
|
+
import expo.modules.core.ModuleRegistryDelegate
|
|
11
|
+
import expo.modules.core.Promise
|
|
12
|
+
import expo.modules.core.interfaces.ExpoMethod
|
|
13
|
+
import expo.modules.core.interfaces.LifecycleEventListener
|
|
14
|
+
import expo.modules.core.interfaces.services.EventEmitter
|
|
15
|
+
import expo.modules.core.interfaces.services.UIManager
|
|
16
|
+
import java.util.*
|
|
17
|
+
|
|
18
|
+
class SpeechModule(
|
|
19
|
+
context: Context,
|
|
20
|
+
private val moduleRegistryDelegate: ModuleRegistryDelegate = ModuleRegistryDelegate()
|
|
21
|
+
) : ExportedModule(context), LifecycleEventListener {
|
|
22
|
+
|
|
23
|
+
private inline fun <reified T> moduleRegistry() =
|
|
24
|
+
moduleRegistryDelegate.getFromModuleRegistry<T>()
|
|
25
|
+
|
|
26
|
+
private val uiManager: UIManager by moduleRegistry()
|
|
27
|
+
private val delayedUtterances: Queue<Utterance> = ArrayDeque()
|
|
28
|
+
|
|
29
|
+
// Module basic definitions
|
|
30
|
+
override fun getName() = "ExponentSpeech"
|
|
31
|
+
override fun getConstants() = mapOf(
|
|
32
|
+
"maxSpeechInputLength" to TextToSpeech.getMaxSpeechInputLength()
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// Module methods
|
|
36
|
+
|
|
37
|
+
@ExpoMethod
|
|
38
|
+
fun isSpeaking(promise: Promise) = promise.resolve(textToSpeech.isSpeaking)
|
|
39
|
+
|
|
40
|
+
@ExpoMethod
|
|
41
|
+
fun getVoices(promise: Promise) {
|
|
42
|
+
var nativeVoices: List<Voice> = emptyList()
|
|
43
|
+
try {
|
|
44
|
+
nativeVoices = textToSpeech.voices.toList()
|
|
45
|
+
} catch (e: Exception) {}
|
|
46
|
+
|
|
47
|
+
val voices = nativeVoices.map {
|
|
48
|
+
val quality = if (it.quality > Voice.QUALITY_NORMAL) {
|
|
49
|
+
"Enhanced"
|
|
50
|
+
} else {
|
|
51
|
+
"Default"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Bundle().apply {
|
|
55
|
+
putString("identifier", it.name)
|
|
56
|
+
putString("name", it.name)
|
|
57
|
+
putString("quality", quality)
|
|
58
|
+
putString("language", LanguageUtils.getISOCode(it.locale))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
promise.resolve(voices)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@ExpoMethod
|
|
66
|
+
fun stop(promise: Promise) {
|
|
67
|
+
textToSpeech.stop()
|
|
68
|
+
promise.resolve(null)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@ExpoMethod
|
|
72
|
+
fun speak(id: String, text: String, options: Map<String, Any>?, promise: Promise) {
|
|
73
|
+
val speechOptions = SpeechOptions.optionsFromMap(options, promise) ?: return
|
|
74
|
+
|
|
75
|
+
if (text.length > TextToSpeech.getMaxSpeechInputLength()) {
|
|
76
|
+
promise.reject(
|
|
77
|
+
"ERR_SPEECH_INPUT_LENGTH",
|
|
78
|
+
"Speech input text is too long! Limit of input length is: " + TextToSpeech.getMaxSpeechInputLength()
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isTextToSpeechReady) {
|
|
84
|
+
speakOut(id, text, speechOptions)
|
|
85
|
+
} else {
|
|
86
|
+
delayedUtterances.add(Utterance(id, text, speechOptions))
|
|
87
|
+
|
|
88
|
+
// init TTS, speaking will be available only after onInit
|
|
89
|
+
textToSpeech
|
|
90
|
+
}
|
|
91
|
+
promise.resolve(null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun speakOut(id: String, text: String, options: SpeechOptions) {
|
|
95
|
+
options.pitch?.let(textToSpeech::setPitch)
|
|
96
|
+
options.rate?.let(textToSpeech::setSpeechRate)
|
|
97
|
+
|
|
98
|
+
textToSpeech.language = options.language?.let {
|
|
99
|
+
val locale = Locale(it)
|
|
100
|
+
val languageAvailable = textToSpeech.isLanguageAvailable(locale)
|
|
101
|
+
|
|
102
|
+
return@let if (
|
|
103
|
+
languageAvailable != TextToSpeech.LANG_MISSING_DATA &&
|
|
104
|
+
languageAvailable != TextToSpeech.LANG_NOT_SUPPORTED
|
|
105
|
+
) {
|
|
106
|
+
locale
|
|
107
|
+
} else {
|
|
108
|
+
Locale.getDefault()
|
|
109
|
+
}
|
|
110
|
+
} ?: Locale.getDefault()
|
|
111
|
+
|
|
112
|
+
options.voice?.let { voiceName ->
|
|
113
|
+
textToSpeech.voices
|
|
114
|
+
.firstOrNull { it.name == voiceName }
|
|
115
|
+
?.let(textToSpeech::setVoice)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
textToSpeech.speak(
|
|
119
|
+
text,
|
|
120
|
+
TextToSpeech.QUEUE_ADD,
|
|
121
|
+
null,
|
|
122
|
+
id
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// TextToSpeech object related code
|
|
127
|
+
|
|
128
|
+
private val isTextToSpeechReady
|
|
129
|
+
get() = _ttsReady
|
|
130
|
+
|
|
131
|
+
private val textToSpeech: TextToSpeech by lazy {
|
|
132
|
+
val newTtsInstance = TextToSpeech(context) { status: Int ->
|
|
133
|
+
if (status == TextToSpeech.SUCCESS) {
|
|
134
|
+
// synchronize because in some cases this runs on another thread and _textToSpeech is null
|
|
135
|
+
synchronized(this@SpeechModule) {
|
|
136
|
+
_ttsReady = true
|
|
137
|
+
_textToSpeech!!.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
|
138
|
+
private val emitter by moduleRegistry<EventEmitter>()
|
|
139
|
+
|
|
140
|
+
override fun onStart(utteranceId: String) {
|
|
141
|
+
emitter.emit("Exponent.speakingStarted", idToMap(utteranceId))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun onDone(utteranceId: String) {
|
|
145
|
+
emitter.emit("Exponent.speakingDone", idToMap(utteranceId))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override fun onStop(utteranceId: String, interrupted: Boolean) {
|
|
149
|
+
emitter.emit("Exponent.speakingStopped", idToMap(utteranceId))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
override fun onError(utteranceId: String) {
|
|
153
|
+
emitter.emit("Exponent.speakingError", idToMap(utteranceId))
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
for ((id, text, options) in delayedUtterances) {
|
|
157
|
+
speakOut(id, text, options)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
_textToSpeech = newTtsInstance
|
|
163
|
+
newTtsInstance
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Lifecycle methods
|
|
167
|
+
override fun onCreate(moduleRegistry: ModuleRegistry) {
|
|
168
|
+
moduleRegistryDelegate.onCreate(moduleRegistry)
|
|
169
|
+
uiManager.registerLifecycleEventListener(this)
|
|
170
|
+
}
|
|
171
|
+
override fun onHostPause() {}
|
|
172
|
+
override fun onHostResume() {}
|
|
173
|
+
override fun onHostDestroy() {
|
|
174
|
+
textToSpeech.shutdown()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Helpers
|
|
178
|
+
private fun idToMap(id: String) = Bundle().apply {
|
|
179
|
+
putString("id", id)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private data class Utterance(
|
|
183
|
+
val id: String,
|
|
184
|
+
val text: String,
|
|
185
|
+
val options: SpeechOptions
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// do not refer to these - they're only needed when initializing `textToSpeech`
|
|
189
|
+
private var _textToSpeech: TextToSpeech? = null
|
|
190
|
+
private var _ttsReady = false
|
|
191
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package expo.modules.speech
|
|
2
|
+
|
|
3
|
+
import expo.modules.core.Promise
|
|
4
|
+
import java.util.*
|
|
5
|
+
|
|
6
|
+
data class SpeechOptions(
|
|
7
|
+
val language: String?,
|
|
8
|
+
val pitch: Float?,
|
|
9
|
+
val rate: Float?,
|
|
10
|
+
val voice: String?
|
|
11
|
+
) {
|
|
12
|
+
companion object {
|
|
13
|
+
fun optionsFromMap(options: Map<String, Any?>?, promise: Promise): SpeechOptions? {
|
|
14
|
+
|
|
15
|
+
if (options == null) {
|
|
16
|
+
return SpeechOptions(null, null, null, null)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
val language = options["language"]?.let {
|
|
20
|
+
if (it is String) {
|
|
21
|
+
return@let it
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
promise.reject("ERR_INVALID_OPTION", "Language must be a string")
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
val pitch = options["pitch"]?.let {
|
|
29
|
+
if (it is Number) {
|
|
30
|
+
return@let it.toFloat()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
promise.reject("ERR_INVALID_OPTION", "Pitch must be a number")
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
val rate = options["rate"]?.let {
|
|
38
|
+
if (it is Number) {
|
|
39
|
+
return@let it.toFloat()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
promise.reject("ERR_INVALID_OPTION", "Rate must be a number")
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
val voice = options["voice"]?.let {
|
|
47
|
+
if (it is String) {
|
|
48
|
+
return@let it
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
promise.reject("ERR_INVALID_OPTION", "Voice name must be a string")
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return SpeechOptions(language, pitch, rate, voice)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("
|
|
1
|
+
declare const _default: import("expo-modules-core").ProxyNativeModule;
|
|
2
2
|
export default _default;
|
package/build/ExponentSpeech.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExponentSpeech.js","sourceRoot":"","sources":["../src/Speech/ExponentSpeech.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ExponentSpeech.js","sourceRoot":"","sources":["../src/Speech/ExponentSpeech.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,eAAe,kBAAkB,CAAC,cAAc,CAAC","sourcesContent":["import { NativeModulesProxy } from 'expo-modules-core';\nexport default NativeModulesProxy.ExponentSpeech;\n"]}
|
|
@@ -2,7 +2,7 @@ import { SpeechOptions, WebVoice } from './Speech.types';
|
|
|
2
2
|
declare const _default: {
|
|
3
3
|
readonly name: string;
|
|
4
4
|
speak(id: string, text: string, options: SpeechOptions): Promise<SpeechSynthesisUtterance>;
|
|
5
|
-
getVoices(): WebVoice[]
|
|
5
|
+
getVoices(): Promise<WebVoice[]>;
|
|
6
6
|
isSpeaking(): Promise<boolean>;
|
|
7
7
|
stop(): Promise<void>;
|
|
8
8
|
pause(): Promise<void>;
|
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
import { SyntheticPlatformEmitter, CodedError } from '
|
|
1
|
+
import { SyntheticPlatformEmitter, CodedError } from 'expo-modules-core';
|
|
2
2
|
import { VoiceQuality } from './Speech.types';
|
|
3
3
|
//https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/text
|
|
4
4
|
const MAX_SPEECH_INPUT_LENGTH = 32767;
|
|
5
|
+
async function getVoices() {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const voices = window.speechSynthesis.getVoices();
|
|
8
|
+
if (voices.length > 0) {
|
|
9
|
+
resolve(voices);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
// when a page loads it takes some amount of time to populate the voices list
|
|
13
|
+
// see https://stackoverflow.com/a/52005323/4337317
|
|
14
|
+
window.speechSynthesis.onvoiceschanged = function () {
|
|
15
|
+
const voices = window.speechSynthesis.getVoices();
|
|
16
|
+
resolve(voices);
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
}
|
|
5
20
|
export default {
|
|
6
21
|
get name() {
|
|
7
22
|
return 'ExponentSpeech';
|
|
@@ -24,9 +39,14 @@ export default {
|
|
|
24
39
|
message.volume = options.volume;
|
|
25
40
|
}
|
|
26
41
|
if ('_voiceIndex' in options && options._voiceIndex != null) {
|
|
27
|
-
const voices =
|
|
42
|
+
const voices = await getVoices();
|
|
28
43
|
message.voice = voices[Math.min(voices.length - 1, Math.max(0, options._voiceIndex))];
|
|
29
44
|
}
|
|
45
|
+
if (typeof options.voice === 'string') {
|
|
46
|
+
const voices = await getVoices();
|
|
47
|
+
message.voice =
|
|
48
|
+
voices[Math.max(0, voices.findIndex((voice) => voice.voiceURI === options.voice))];
|
|
49
|
+
}
|
|
30
50
|
if (typeof options.onResume === 'function') {
|
|
31
51
|
message.onresume = options.onResume;
|
|
32
52
|
}
|
|
@@ -52,9 +72,9 @@ export default {
|
|
|
52
72
|
window.speechSynthesis.speak(message);
|
|
53
73
|
return message;
|
|
54
74
|
},
|
|
55
|
-
getVoices() {
|
|
56
|
-
const voices =
|
|
57
|
-
return voices.map(voice => ({
|
|
75
|
+
async getVoices() {
|
|
76
|
+
const voices = await getVoices();
|
|
77
|
+
return voices.map((voice) => ({
|
|
58
78
|
identifier: voice.voiceURI,
|
|
59
79
|
quality: VoiceQuality.Default,
|
|
60
80
|
isDefault: voice.default,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExponentSpeech.web.js","sourceRoot":"","sources":["../src/Speech/ExponentSpeech.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ExponentSpeech.web.js","sourceRoot":"","sources":["../src/Speech/ExponentSpeech.web.ts"],"names":[],"mappings":"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","sourcesContent":["import { SyntheticPlatformEmitter, CodedError } from 'expo-modules-core';\n\nimport { SpeechOptions, WebVoice, VoiceQuality } from './Speech.types';\n\n//https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/text\nconst MAX_SPEECH_INPUT_LENGTH = 32767;\n\nasync function getVoices(): Promise<SpeechSynthesisVoice[]> {\n return new Promise<SpeechSynthesisVoice[]>((resolve) => {\n const voices = window.speechSynthesis.getVoices();\n\n if (voices.length > 0) {\n resolve(voices);\n return;\n }\n\n // when a page loads it takes some amount of time to populate the voices list\n // see https://stackoverflow.com/a/52005323/4337317\n window.speechSynthesis.onvoiceschanged = function () {\n const voices = window.speechSynthesis.getVoices();\n resolve(voices);\n };\n });\n}\n\nexport default {\n get name(): string {\n return 'ExponentSpeech';\n },\n async speak(id: string, text: string, options: SpeechOptions): Promise<SpeechSynthesisUtterance> {\n if (text.length > MAX_SPEECH_INPUT_LENGTH) {\n throw new CodedError(\n 'ERR_SPEECH_INPUT_LENGTH',\n 'Speech input text is too long! Limit of input length is: ' + MAX_SPEECH_INPUT_LENGTH\n );\n }\n\n const message = new SpeechSynthesisUtterance();\n\n if (typeof options.rate === 'number') {\n message.rate = options.rate;\n }\n if (typeof options.pitch === 'number') {\n message.pitch = options.pitch;\n }\n if (typeof options.language === 'string') {\n message.lang = options.language;\n }\n if (typeof options.volume === 'number') {\n message.volume = options.volume;\n }\n if ('_voiceIndex' in options && options._voiceIndex != null) {\n const voices = await getVoices();\n message.voice = voices[Math.min(voices.length - 1, Math.max(0, options._voiceIndex))];\n }\n if (typeof options.voice === 'string') {\n const voices = await getVoices();\n message.voice =\n voices[\n Math.max(\n 0,\n voices.findIndex((voice) => voice.voiceURI === options.voice)\n )\n ];\n }\n if (typeof options.onResume === 'function') {\n message.onresume = options.onResume;\n }\n if (typeof options.onMark === 'function') {\n message.onmark = options.onMark;\n }\n if (typeof options.onBoundary === 'function') {\n message.onboundary = options.onBoundary;\n }\n\n message.onstart = (nativeEvent: SpeechSynthesisEvent) => {\n SyntheticPlatformEmitter.emit('Exponent.speakingStarted', { id, nativeEvent });\n };\n message.onend = (nativeEvent: SpeechSynthesisEvent) => {\n SyntheticPlatformEmitter.emit('Exponent.speakingDone', { id, nativeEvent });\n };\n message.onpause = (nativeEvent: SpeechSynthesisEvent) => {\n SyntheticPlatformEmitter.emit('Exponent.speakingStopped', { id, nativeEvent });\n };\n message.onerror = (nativeEvent: SpeechSynthesisErrorEvent) => {\n SyntheticPlatformEmitter.emit('Exponent.speakingError', { id, nativeEvent });\n };\n\n message.text = text;\n\n window.speechSynthesis.speak(message);\n\n return message;\n },\n async getVoices(): Promise<WebVoice[]> {\n const voices = await getVoices();\n return voices.map((voice) => ({\n identifier: voice.voiceURI,\n quality: VoiceQuality.Default,\n isDefault: voice.default,\n language: voice.lang,\n localService: voice.localService,\n name: voice.name,\n voiceURI: voice.voiceURI,\n }));\n },\n async isSpeaking(): Promise<boolean> {\n return window.speechSynthesis.speaking;\n },\n async stop(): Promise<void> {\n return window.speechSynthesis.cancel();\n },\n async pause(): Promise<void> {\n return window.speechSynthesis.pause();\n },\n async resume(): Promise<void> {\n return window.speechSynthesis.resume();\n },\n maxSpeechInputLength: MAX_SPEECH_INPUT_LENGTH,\n};\n"]}
|
package/build/Speech.d.ts
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import { SpeechOptions, SpeechEventCallback, VoiceQuality, Voice } from './Speech.types';
|
|
2
|
-
export { SpeechOptions, SpeechEventCallback, VoiceQuality, Voice };
|
|
1
|
+
import { SpeechOptions, SpeechEventCallback, VoiceQuality, Voice, WebVoice } from './Speech.types';
|
|
2
|
+
export { SpeechOptions, SpeechEventCallback, VoiceQuality, Voice, WebVoice };
|
|
3
|
+
/**
|
|
4
|
+
* Speak out loud the text given options. Calling this when another text is being spoken adds
|
|
5
|
+
* an utterance to queue.
|
|
6
|
+
* @param text The text to be spoken. Cannot be longer than [`Speech.maxSpeechInputLength`](#speechmaxspeechinputlength).
|
|
7
|
+
* @param options A `SpeechOptions` object.
|
|
8
|
+
*/
|
|
3
9
|
export declare function speak(text: string, options?: SpeechOptions): void;
|
|
10
|
+
/**
|
|
11
|
+
* Returns list of all available voices.
|
|
12
|
+
* @return List of `Voice` objects.
|
|
13
|
+
*/
|
|
4
14
|
export declare function getAvailableVoicesAsync(): Promise<Voice[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Determine whether the Text-to-speech utility is currently speaking. Will return `true` if speaker
|
|
17
|
+
* is paused.
|
|
18
|
+
* @return Returns a Promise that fulfils with a boolean, `true` if speaking, `false` if not.
|
|
19
|
+
*/
|
|
5
20
|
export declare function isSpeakingAsync(): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Interrupts current speech and deletes all in queue.
|
|
23
|
+
*/
|
|
6
24
|
export declare function stop(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Pauses current speech. This method is not available on Android.
|
|
27
|
+
*/
|
|
7
28
|
export declare function pause(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Resumes speaking previously paused speech or does nothing if there's none. This method is not
|
|
31
|
+
* available on Android.
|
|
32
|
+
*/
|
|
8
33
|
export declare function resume(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Maximum possible text length acceptable by `Speech.speak()` method. It is platform-dependent.
|
|
36
|
+
* On iOS, this returns `Number.MAX_VALUE`.
|
|
37
|
+
*/
|
|
9
38
|
export declare const maxSpeechInputLength: number;
|
package/build/Speech.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UnavailabilityError } from '
|
|
1
|
+
import { UnavailabilityError } from 'expo-modules-core';
|
|
2
2
|
import { NativeEventEmitter } from 'react-native';
|
|
3
3
|
import ExponentSpeech from './ExponentSpeech';
|
|
4
4
|
import { VoiceQuality } from './Speech.types';
|
|
@@ -51,30 +51,61 @@ function _registerListenersIfNeeded() {
|
|
|
51
51
|
_unregisterListenersIfNeeded();
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
+
// @needsAudit
|
|
55
|
+
/**
|
|
56
|
+
* Speak out loud the text given options. Calling this when another text is being spoken adds
|
|
57
|
+
* an utterance to queue.
|
|
58
|
+
* @param text The text to be spoken. Cannot be longer than [`Speech.maxSpeechInputLength`](#speechmaxspeechinputlength).
|
|
59
|
+
* @param options A `SpeechOptions` object.
|
|
60
|
+
*/
|
|
54
61
|
export function speak(text, options = {}) {
|
|
55
62
|
const id = _nextCallbackId++;
|
|
56
63
|
_CALLBACKS[id] = options;
|
|
57
64
|
_registerListenersIfNeeded();
|
|
58
65
|
ExponentSpeech.speak(String(id), text, options);
|
|
59
66
|
}
|
|
67
|
+
// @needsAudit
|
|
68
|
+
/**
|
|
69
|
+
* Returns list of all available voices.
|
|
70
|
+
* @return List of `Voice` objects.
|
|
71
|
+
*/
|
|
60
72
|
export async function getAvailableVoicesAsync() {
|
|
61
73
|
if (!ExponentSpeech.getVoices) {
|
|
62
74
|
throw new UnavailabilityError('Speech', 'getVoices');
|
|
63
75
|
}
|
|
64
76
|
return ExponentSpeech.getVoices();
|
|
65
77
|
}
|
|
78
|
+
//@needsAudit
|
|
79
|
+
/**
|
|
80
|
+
* Determine whether the Text-to-speech utility is currently speaking. Will return `true` if speaker
|
|
81
|
+
* is paused.
|
|
82
|
+
* @return Returns a Promise that fulfils with a boolean, `true` if speaking, `false` if not.
|
|
83
|
+
*/
|
|
66
84
|
export async function isSpeakingAsync() {
|
|
67
85
|
return ExponentSpeech.isSpeaking();
|
|
68
86
|
}
|
|
87
|
+
// @needsAudit
|
|
88
|
+
/**
|
|
89
|
+
* Interrupts current speech and deletes all in queue.
|
|
90
|
+
*/
|
|
69
91
|
export async function stop() {
|
|
70
92
|
return ExponentSpeech.stop();
|
|
71
93
|
}
|
|
94
|
+
// @needsAudit
|
|
95
|
+
/**
|
|
96
|
+
* Pauses current speech. This method is not available on Android.
|
|
97
|
+
*/
|
|
72
98
|
export async function pause() {
|
|
73
99
|
if (!ExponentSpeech.pause) {
|
|
74
100
|
throw new UnavailabilityError('Speech', 'pause');
|
|
75
101
|
}
|
|
76
102
|
return ExponentSpeech.pause();
|
|
77
103
|
}
|
|
104
|
+
// @needsAudit
|
|
105
|
+
/**
|
|
106
|
+
* Resumes speaking previously paused speech or does nothing if there's none. This method is not
|
|
107
|
+
* available on Android.
|
|
108
|
+
*/
|
|
78
109
|
export async function resume() {
|
|
79
110
|
if (!ExponentSpeech.resume) {
|
|
80
111
|
throw new UnavailabilityError('Speech', 'resume');
|
|
@@ -82,7 +113,13 @@ export async function resume() {
|
|
|
82
113
|
return ExponentSpeech.resume();
|
|
83
114
|
}
|
|
84
115
|
function setSpeakingListener(eventName, callback) {
|
|
85
|
-
|
|
116
|
+
// @ts-ignore: the EventEmitter interface has been changed in react-native@0.64.0
|
|
117
|
+
const listenerCount = SpeechEventEmitter.listenerCount
|
|
118
|
+
? // @ts-ignore: this is available since 0.64
|
|
119
|
+
SpeechEventEmitter.listenerCount(eventName)
|
|
120
|
+
: // @ts-ignore: this is available in older versions
|
|
121
|
+
SpeechEventEmitter.listeners(eventName).length;
|
|
122
|
+
if (listenerCount > 0) {
|
|
86
123
|
SpeechEventEmitter.removeAllListeners(eventName);
|
|
87
124
|
}
|
|
88
125
|
SpeechEventEmitter.addListener(eventName, callback);
|
|
@@ -90,5 +127,10 @@ function setSpeakingListener(eventName, callback) {
|
|
|
90
127
|
function removeSpeakingListener(eventName) {
|
|
91
128
|
SpeechEventEmitter.removeAllListeners(eventName);
|
|
92
129
|
}
|
|
130
|
+
// @needsAudit
|
|
131
|
+
/**
|
|
132
|
+
* Maximum possible text length acceptable by `Speech.speak()` method. It is platform-dependent.
|
|
133
|
+
* On iOS, this returns `Number.MAX_VALUE`.
|
|
134
|
+
*/
|
|
93
135
|
export const maxSpeechInputLength = ExponentSpeech.maxSpeechInputLength || Number.MAX_VALUE;
|
|
94
136
|
//# sourceMappingURL=Speech.js.map
|