@focus8/expo-acapela-tts 0.1.0

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.
@@ -0,0 +1,43 @@
1
+ apply plugin: 'com.android.library'
2
+
3
+ group = 'expo.modules.acapelatts'
4
+ version = '0.1.0'
5
+
6
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
+ apply from: expoModulesCorePlugin
8
+ applyKotlinExpoModulesCorePlugin()
9
+ useCoreDependencies()
10
+ useExpoPublishing()
11
+
12
+ def useManagedAndroidSdkVersions = false
13
+ if (useManagedAndroidSdkVersions) {
14
+ useDefaultAndroidSdkVersions()
15
+ } else {
16
+ buildscript {
17
+ ext.safeExtGet = { prop, fallback ->
18
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
19
+ }
20
+ }
21
+ project.android {
22
+ compileSdkVersion safeExtGet("compileSdkVersion", 34)
23
+ defaultConfig {
24
+ minSdkVersion safeExtGet("minSdkVersion", 21)
25
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
26
+ }
27
+ }
28
+ }
29
+
30
+ android {
31
+ namespace "expo.modules.acapelatts"
32
+ defaultConfig {
33
+ versionCode 1
34
+ versionName "0.1.0"
35
+ }
36
+ lintOptions {
37
+ abortOnError false
38
+ }
39
+ }
40
+
41
+ dependencies {
42
+ implementation fileTree(dir: 'libs', include: ['*.aar'])
43
+ }
@@ -0,0 +1,272 @@
1
+ package expo.modules.acapelatts
2
+
3
+ import android.util.Log
4
+ import com.acapelagroup.android.tts.acattsandroid
5
+ import com.acapelagroup.android.tts.acattsandroid.iTTSEventsCallback
6
+ import expo.modules.kotlin.modules.Module
7
+ import expo.modules.kotlin.modules.ModuleDefinition
8
+ import java.io.File
9
+
10
+ class ExpoAcapelaTtsModule : Module(), iTTSEventsCallback {
11
+
12
+ private var tts: acattsandroid? = null
13
+ private var isInitialized = false
14
+ private var currentVoice: String? = null
15
+
16
+ companion object {
17
+ private const val TAG = "ExpoAcapelaTts"
18
+ private const val VOICES_PATH = "/system/media/voices"
19
+
20
+ private const val LICENSE_USER_ID = 0x34307771L
21
+ private const val LICENSE_PASSWORD = 0x0008d44fL
22
+ private const val LICENSE_KEY = "\"1976 0 qw04 #EVALUATION#Focus8\"\n" +
23
+ "R%NUNVGz28KZdGZestTLeuatIwSRJoV8wn33qep6qR##\n" +
24
+ "VOO8rYtC9QJpPfoX!8wlLrI@JXk3KnFJ%fhyL4AlArB!xoRq\n" +
25
+ "UCNfz!@aiT9Sp88aLHnq\n"
26
+
27
+ private var nativeLibLoaded = false
28
+
29
+ init {
30
+ try {
31
+ System.loadLibrary("acattsandroid")
32
+ nativeLibLoaded = true
33
+ Log.i(TAG, "Native library loaded")
34
+ } catch (e: UnsatisfiedLinkError) {
35
+ Log.w(TAG, "Failed to load native library: ${e.message}")
36
+ }
37
+ }
38
+ }
39
+
40
+ override fun definition() = ModuleDefinition {
41
+
42
+ Name("ExpoAcapelaTts")
43
+
44
+ Events("onSpeechStart", "onSpeechEnd", "onError")
45
+
46
+ Function("isAvailable") {
47
+ if (!nativeLibLoaded) {
48
+ return@Function false
49
+ }
50
+ try {
51
+ val voicesDir = File(VOICES_PATH)
52
+ val available = voicesDir.exists() && voicesDir.isDirectory &&
53
+ (voicesDir.list()?.isNotEmpty() == true)
54
+ logInfo("isAvailable: $available (path: $VOICES_PATH)")
55
+ available
56
+ } catch (e: Exception) {
57
+ logError("isAvailable check failed", e)
58
+ false
59
+ }
60
+ }
61
+
62
+ AsyncFunction("initializeAsync") {
63
+ if (!nativeLibLoaded) {
64
+ logWarning("Cannot initialize: native library not loaded")
65
+ return@AsyncFunction false
66
+ }
67
+
68
+ if (isInitialized && tts != null) {
69
+ logInfo("Already initialized")
70
+ return@AsyncFunction true
71
+ }
72
+
73
+ try {
74
+ val context = appContext.reactContext ?: run {
75
+ logWarning("ReactContext not available")
76
+ return@AsyncFunction false
77
+ }
78
+
79
+ tts = acattsandroid(context, this@ExpoAcapelaTtsModule, null)
80
+ tts!!.setLog(true)
81
+ tts!!.setLicense(LICENSE_USER_ID, LICENSE_PASSWORD, LICENSE_KEY)
82
+
83
+ val version = tts!!.version
84
+ logInfo("Initialized, SDK version: $version")
85
+
86
+ isInitialized = true
87
+ true
88
+ } catch (e: Exception) {
89
+ logError("Failed to initialize", e)
90
+ tts = null
91
+ isInitialized = false
92
+ false
93
+ }
94
+ }
95
+
96
+ AsyncFunction("getVoicesAsync") {
97
+ if (tts == null) {
98
+ logWarning("getVoicesAsync: TTS not initialized")
99
+ return@AsyncFunction emptyList<Map<String, Any>>()
100
+ }
101
+
102
+ try {
103
+ val voiceDirPaths = arrayOf(VOICES_PATH)
104
+ val voicesList = tts!!.getVoicesList(voiceDirPaths) ?: emptyArray()
105
+ logInfo("Found ${voicesList.size} voices")
106
+
107
+ val voices = voicesList.map { voiceName ->
108
+ val info = tts!!.getVoiceInfo(voiceName) ?: emptyMap()
109
+ val genderValue = info["gender"] ?: "0"
110
+ mapOf(
111
+ "name" to voiceName,
112
+ "locale" to (info["locale"] ?: ""),
113
+ "language" to (info["language"] ?: ""),
114
+ "speaker" to (info["name"]?.let { extractSpeakerName(it) } ?: ""),
115
+ "gender" to if (genderValue == "1") "female" else "male",
116
+ "quality" to (info["quality"] ?: ""),
117
+ "age" to (info["age"] ?: ""),
118
+ "frequency" to (info["frequency"] ?: "")
119
+ )
120
+ }
121
+
122
+ // Reload the current voice since getVoicesList unloads any loaded voice
123
+ if (currentVoice != null) {
124
+ tts!!.load(currentVoice, "MODE=prep_full")
125
+ logInfo("Reloaded voice after listing: $currentVoice")
126
+ }
127
+
128
+ voices
129
+ } catch (e: Exception) {
130
+ logError("Failed to get voices", e)
131
+ emptyList<Map<String, Any>>()
132
+ }
133
+ }
134
+
135
+ AsyncFunction("loadVoiceAsync") { voiceName: String ->
136
+ if (tts == null) {
137
+ logWarning("loadVoiceAsync: TTS not initialized")
138
+ return@AsyncFunction false
139
+ }
140
+
141
+ try {
142
+ val result = tts!!.load(voiceName, "MODE=prep_full")
143
+ if (result == 0) {
144
+ currentVoice = voiceName
145
+ logInfo("Voice loaded: $voiceName")
146
+ true
147
+ } else {
148
+ logWarning("Failed to load voice: $voiceName, error: $result")
149
+ false
150
+ }
151
+ } catch (e: Exception) {
152
+ logError("Failed to load voice: $voiceName", e)
153
+ false
154
+ }
155
+ }
156
+
157
+ AsyncFunction("speakAsync") { text: String ->
158
+ if (tts == null) {
159
+ logWarning("speakAsync: TTS not initialized")
160
+ return@AsyncFunction -1
161
+ }
162
+
163
+ try {
164
+ if (tts!!.isSpeaking) {
165
+ tts!!.stop()
166
+ }
167
+ val result = tts!!.speak(text)
168
+ if (result < 0) {
169
+ logWarning("speak returned error: $result")
170
+ }
171
+ result
172
+ } catch (e: Exception) {
173
+ logError("Failed to speak", e)
174
+ -1
175
+ }
176
+ }
177
+
178
+ Function("stop") {
179
+ try {
180
+ tts?.stop()
181
+ } catch (e: Exception) {
182
+ logError("Failed to stop", e)
183
+ }
184
+ }
185
+
186
+ Function("isSpeaking") {
187
+ try {
188
+ tts?.isSpeaking ?: false
189
+ } catch (e: Exception) {
190
+ false
191
+ }
192
+ }
193
+
194
+ Function("setSpeechRate") { rate: Float ->
195
+ try {
196
+ // JS sends 0.5-2.0, Acapela expects 30-400 (percentage)
197
+ val acapelaRate = rate * 100f
198
+ tts?.setSpeechRate(acapelaRate)
199
+ } catch (e: Exception) {
200
+ logError("Failed to set speech rate", e)
201
+ }
202
+ }
203
+
204
+ Function("setPitch") { pitch: Float ->
205
+ try {
206
+ tts?.setPitch(pitch)
207
+ } catch (e: Exception) {
208
+ logError("Failed to set pitch", e)
209
+ }
210
+ }
211
+
212
+ Function("setAudioBoost") { boost: Int ->
213
+ try {
214
+ tts?.setAudioBoost(boost)
215
+ } catch (e: Exception) {
216
+ logError("Failed to set audio boost", e)
217
+ }
218
+ }
219
+
220
+ AsyncFunction("shutdownAsync") {
221
+ try {
222
+ tts?.stop()
223
+ tts?.shutdown()
224
+ tts = null
225
+ isInitialized = false
226
+ currentVoice = null
227
+ logInfo("Shutdown complete")
228
+ } catch (e: Exception) {
229
+ logError("Failed to shutdown", e)
230
+ }
231
+ }
232
+ }
233
+
234
+ // iTTSEventsCallback implementation
235
+ override fun ttsevents(type: Long, param1: Long, param2: Long, param3: Long, param4: Long) {
236
+ try {
237
+ when (type) {
238
+ acattsandroid.EVENT_AUDIO_START.toLong() -> {
239
+ logInfo("Speech started (text index: $param1)")
240
+ sendEvent("onSpeechStart", emptyMap<String, Any>())
241
+ }
242
+ acattsandroid.EVENT_AUDIO_END.toLong() -> {
243
+ logInfo("Speech ended (text index: $param1, samples: $param2)")
244
+ sendEvent("onSpeechEnd", emptyMap<String, Any>())
245
+ }
246
+ acattsandroid.EVENT_TEXT_START.toLong() -> {
247
+ logInfo("Text processing started (index: $param1)")
248
+ }
249
+ acattsandroid.EVENT_TEXT_END.toLong() -> {
250
+ logInfo("Text processing ended (index: $param1)")
251
+ }
252
+ acattsandroid.EVENT_WORD_POS.toLong() -> {
253
+ // Word position event - available for future text highlighting
254
+ }
255
+ }
256
+ } catch (e: Exception) {
257
+ logError("Error in ttsevents callback", e)
258
+ }
259
+ }
260
+
261
+ private fun extractSpeakerName(voiceFileName: String): String {
262
+ // Voice file names like "non_kari_22k_ns.qvcu" -> "Kari"
263
+ // Or voice names like "hq-ref-Norwegian-Kari-22khz" -> "Kari"
264
+ val parts = voiceFileName.split("-", "_")
265
+ return parts.find { it.length > 2 && it[0].isUpperCase() && it.all { c -> c.isLetter() } }
266
+ ?: voiceFileName
267
+ }
268
+
269
+ private fun logInfo(msg: String) = Log.i(TAG, msg)
270
+ private fun logWarning(msg: String) = Log.w(TAG, msg)
271
+ private fun logError(msg: String, e: Throwable) = Log.e(TAG, msg, e)
272
+ }
@@ -0,0 +1,19 @@
1
+ export type AcapelaVoice = {
2
+ name: string;
3
+ locale: string;
4
+ language: string;
5
+ speaker: string;
6
+ gender: 'male' | 'female';
7
+ quality: string;
8
+ age: string;
9
+ frequency: string;
10
+ };
11
+ export type ExpoAcapelaTtsModuleEvents = {
12
+ onSpeechStart: () => void;
13
+ onSpeechEnd: () => void;
14
+ onError: (event: {
15
+ message: string;
16
+ code: number;
17
+ }) => void;
18
+ };
19
+ //# sourceMappingURL=ExpoAcapelaTts.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTts.types.d.ts","sourceRoot":"","sources":["../src/ExpoAcapelaTts.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,OAAO,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7D,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpoAcapelaTts.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTts.types.js","sourceRoot":"","sources":["../src/ExpoAcapelaTts.types.ts"],"names":[],"mappings":"","sourcesContent":["export type AcapelaVoice = {\n name: string;\n locale: string;\n language: string;\n speaker: string;\n gender: 'male' | 'female';\n quality: string;\n age: string;\n frequency: string;\n};\n\nexport type ExpoAcapelaTtsModuleEvents = {\n onSpeechStart: () => void;\n onSpeechEnd: () => void;\n onError: (event: { message: string; code: number }) => void;\n};\n"]}
@@ -0,0 +1,18 @@
1
+ import { NativeModule } from 'expo';
2
+ import { AcapelaVoice, ExpoAcapelaTtsModuleEvents } from './ExpoAcapelaTts.types';
3
+ declare class ExpoAcapelaTtsModule extends NativeModule<ExpoAcapelaTtsModuleEvents> {
4
+ isAvailable(): boolean;
5
+ initializeAsync(): Promise<boolean>;
6
+ getVoicesAsync(): Promise<AcapelaVoice[]>;
7
+ loadVoiceAsync(voiceName: string): Promise<boolean>;
8
+ speakAsync(text: string): Promise<number>;
9
+ stop(): void;
10
+ isSpeaking(): boolean;
11
+ setSpeechRate(rate: number): void;
12
+ setPitch(pitch: number): void;
13
+ setAudioBoost(boost: number): void;
14
+ shutdownAsync(): Promise<void>;
15
+ }
16
+ declare const _default: ExpoAcapelaTtsModule;
17
+ export default _default;
18
+ //# sourceMappingURL=ExpoAcapelaTtsModule.android.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTtsModule.android.d.ts","sourceRoot":"","sources":["../src/ExpoAcapelaTtsModule.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC3B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,OAAO,oBAAqB,SAAQ,YAAY,CAAC,0BAA0B,CAAC;IACjF,WAAW,IAAI,OAAO;IACtB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IACnC,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IACzC,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IACnD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IACzC,IAAI,IAAI,IAAI;IACZ,UAAU,IAAI,OAAO;IACrB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAC7B,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAClC,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAC/B;;AAED,wBAA2E"}
@@ -0,0 +1,3 @@
1
+ import { requireNativeModule } from 'expo';
2
+ export default requireNativeModule('ExpoAcapelaTts');
3
+ //# sourceMappingURL=ExpoAcapelaTtsModule.android.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTtsModule.android.js","sourceRoot":"","sources":["../src/ExpoAcapelaTtsModule.android.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAqBzD,eAAe,mBAAmB,CAAuB,gBAAgB,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from 'expo';\n\nimport {\n AcapelaVoice,\n ExpoAcapelaTtsModuleEvents,\n} from './ExpoAcapelaTts.types';\n\ndeclare class ExpoAcapelaTtsModule extends NativeModule<ExpoAcapelaTtsModuleEvents> {\n isAvailable(): boolean;\n initializeAsync(): Promise<boolean>;\n getVoicesAsync(): Promise<AcapelaVoice[]>;\n loadVoiceAsync(voiceName: string): Promise<boolean>;\n speakAsync(text: string): Promise<number>;\n stop(): void;\n isSpeaking(): boolean;\n setSpeechRate(rate: number): void;\n setPitch(pitch: number): void;\n setAudioBoost(boost: number): void;\n shutdownAsync(): Promise<void>;\n}\n\nexport default requireNativeModule<ExpoAcapelaTtsModule>('ExpoAcapelaTts');\n"]}
@@ -0,0 +1,19 @@
1
+ declare const _default: {
2
+ isAvailable(): boolean;
3
+ initializeAsync(): Promise<boolean>;
4
+ getVoicesAsync(): Promise<never[]>;
5
+ loadVoiceAsync(_voiceName: string): Promise<boolean>;
6
+ speakAsync(_text: string): Promise<number>;
7
+ stop(): void;
8
+ isSpeaking(): boolean;
9
+ setSpeechRate(_rate: number): void;
10
+ setPitch(_pitch: number): void;
11
+ setAudioBoost(_boost: number): void;
12
+ shutdownAsync(): Promise<void>;
13
+ addListener(): {
14
+ remove(): void;
15
+ };
16
+ removeListeners(): void;
17
+ };
18
+ export default _default;
19
+ //# sourceMappingURL=ExpoAcapelaTtsModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTtsModule.d.ts","sourceRoot":"","sources":["../src/ExpoAcapelaTtsModule.ts"],"names":[],"mappings":";mBAEiB,OAAO;uBAGG,OAAO,CAAC,OAAO,CAAC;;+BAMR,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;sBAGlC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;;kBAIlC,OAAO;yBAGA,MAAM;qBACV,MAAM;0BACD,MAAM;;;;;;;AAtB9B,wBA4BE"}
@@ -0,0 +1,31 @@
1
+ // No-op stub for non-Android platforms
2
+ export default {
3
+ isAvailable() {
4
+ return false;
5
+ },
6
+ async initializeAsync() {
7
+ return false;
8
+ },
9
+ async getVoicesAsync() {
10
+ return [];
11
+ },
12
+ async loadVoiceAsync(_voiceName) {
13
+ return false;
14
+ },
15
+ async speakAsync(_text) {
16
+ return -1;
17
+ },
18
+ stop() { },
19
+ isSpeaking() {
20
+ return false;
21
+ },
22
+ setSpeechRate(_rate) { },
23
+ setPitch(_pitch) { },
24
+ setAudioBoost(_boost) { },
25
+ async shutdownAsync() { },
26
+ addListener() {
27
+ return { remove() { } };
28
+ },
29
+ removeListeners() { },
30
+ };
31
+ //# sourceMappingURL=ExpoAcapelaTtsModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAcapelaTtsModule.js","sourceRoot":"","sources":["../src/ExpoAcapelaTtsModule.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,eAAe;IACb,WAAW;QACT,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,CAAC,eAAe;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,CAAC,cAAc;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,UAAkB;QACrC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,KAAa;QAC5B,OAAO,CAAC,CAAC,CAAC;IACZ,CAAC;IACD,IAAI,KAAI,CAAC;IACT,UAAU;QACR,OAAO,KAAK,CAAC;IACf,CAAC;IACD,aAAa,CAAC,KAAa,IAAG,CAAC;IAC/B,QAAQ,CAAC,MAAc,IAAG,CAAC;IAC3B,aAAa,CAAC,MAAc,IAAG,CAAC;IAChC,KAAK,CAAC,aAAa,KAAI,CAAC;IACxB,WAAW;QACT,OAAO,EAAE,MAAM,KAAI,CAAC,EAAE,CAAC;IACzB,CAAC;IACD,eAAe,KAAI,CAAC;CACrB,CAAC","sourcesContent":["// No-op stub for non-Android platforms\nexport default {\n isAvailable(): boolean {\n return false;\n },\n async initializeAsync(): Promise<boolean> {\n return false;\n },\n async getVoicesAsync() {\n return [];\n },\n async loadVoiceAsync(_voiceName: string): Promise<boolean> {\n return false;\n },\n async speakAsync(_text: string): Promise<number> {\n return -1;\n },\n stop() {},\n isSpeaking(): boolean {\n return false;\n },\n setSpeechRate(_rate: number) {},\n setPitch(_pitch: number) {},\n setAudioBoost(_boost: number) {},\n async shutdownAsync() {},\n addListener() {\n return { remove() {} };\n },\n removeListeners() {},\n};\n"]}
@@ -0,0 +1,3 @@
1
+ export { default } from './ExpoAcapelaTtsModule';
2
+ export * from './ExpoAcapelaTts.types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,cAAc,wBAAwB,CAAC"}
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { default } from './ExpoAcapelaTtsModule';
2
+ export * from './ExpoAcapelaTts.types';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,cAAc,wBAAwB,CAAC","sourcesContent":["export { default } from './ExpoAcapelaTtsModule';\nexport * from './ExpoAcapelaTts.types';\n"]}
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["android"],
3
+ "android": {
4
+ "modules": ["expo.modules.acapelatts.ExpoAcapelaTtsModule"]
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@focus8/expo-acapela-tts",
3
+ "version": "0.1.0",
4
+ "description": "Acapela TTS integration for Expo on Android",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "react-native",
21
+ "expo",
22
+ "acapela",
23
+ "tts",
24
+ "text-to-speech"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/focus8-no/expo-acapela-tts.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/focus8-no/expo-acapela-tts/issues"
32
+ },
33
+ "homepage": "https://github.com/focus8-no/expo-acapela-tts#readme",
34
+ "author": "Kjartan <kjartan@focus8.no>",
35
+ "license": "MIT",
36
+ "dependencies": {},
37
+ "devDependencies": {
38
+ "@types/react": "~19.1.0",
39
+ "expo-module-scripts": "^5.0.7",
40
+ "expo": "^54.0.12",
41
+ "react-native": "0.81.4"
42
+ },
43
+ "peerDependencies": {
44
+ "expo": "*",
45
+ "react": "*",
46
+ "react-native": "*"
47
+ }
48
+ }
@@ -0,0 +1,16 @@
1
+ export type AcapelaVoice = {
2
+ name: string;
3
+ locale: string;
4
+ language: string;
5
+ speaker: string;
6
+ gender: 'male' | 'female';
7
+ quality: string;
8
+ age: string;
9
+ frequency: string;
10
+ };
11
+
12
+ export type ExpoAcapelaTtsModuleEvents = {
13
+ onSpeechStart: () => void;
14
+ onSpeechEnd: () => void;
15
+ onError: (event: { message: string; code: number }) => void;
16
+ };
@@ -0,0 +1,22 @@
1
+ import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ import {
4
+ AcapelaVoice,
5
+ ExpoAcapelaTtsModuleEvents,
6
+ } from './ExpoAcapelaTts.types';
7
+
8
+ declare class ExpoAcapelaTtsModule extends NativeModule<ExpoAcapelaTtsModuleEvents> {
9
+ isAvailable(): boolean;
10
+ initializeAsync(): Promise<boolean>;
11
+ getVoicesAsync(): Promise<AcapelaVoice[]>;
12
+ loadVoiceAsync(voiceName: string): Promise<boolean>;
13
+ speakAsync(text: string): Promise<number>;
14
+ stop(): void;
15
+ isSpeaking(): boolean;
16
+ setSpeechRate(rate: number): void;
17
+ setPitch(pitch: number): void;
18
+ setAudioBoost(boost: number): void;
19
+ shutdownAsync(): Promise<void>;
20
+ }
21
+
22
+ export default requireNativeModule<ExpoAcapelaTtsModule>('ExpoAcapelaTts');
@@ -0,0 +1,30 @@
1
+ // No-op stub for non-Android platforms
2
+ export default {
3
+ isAvailable(): boolean {
4
+ return false;
5
+ },
6
+ async initializeAsync(): Promise<boolean> {
7
+ return false;
8
+ },
9
+ async getVoicesAsync() {
10
+ return [];
11
+ },
12
+ async loadVoiceAsync(_voiceName: string): Promise<boolean> {
13
+ return false;
14
+ },
15
+ async speakAsync(_text: string): Promise<number> {
16
+ return -1;
17
+ },
18
+ stop() {},
19
+ isSpeaking(): boolean {
20
+ return false;
21
+ },
22
+ setSpeechRate(_rate: number) {},
23
+ setPitch(_pitch: number) {},
24
+ setAudioBoost(_boost: number) {},
25
+ async shutdownAsync() {},
26
+ addListener() {
27
+ return { remove() {} };
28
+ },
29
+ removeListeners() {},
30
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from './ExpoAcapelaTtsModule';
2
+ export * from './ExpoAcapelaTts.types';
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "expo-module-scripts/tsconfig.base",
3
+ "compilerOptions": {
4
+ "outDir": "./build"
5
+ },
6
+ "include": ["./src"]
7
+ }