@capgo/capacitor-speech-synthesis 7.0.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,438 @@
1
+ package ee.forgr.plugin.speechsynthesis;
2
+
3
+ import android.os.Build;
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 com.getcapacitor.JSArray;
9
+ import com.getcapacitor.JSObject;
10
+ import com.getcapacitor.Plugin;
11
+ import com.getcapacitor.PluginCall;
12
+ import com.getcapacitor.PluginMethod;
13
+ import com.getcapacitor.annotation.CapacitorPlugin;
14
+ import java.io.File;
15
+ import java.util.ArrayList;
16
+ import java.util.HashMap;
17
+ import java.util.HashSet;
18
+ import java.util.List;
19
+ import java.util.Locale;
20
+ import java.util.Set;
21
+ import org.json.JSONException;
22
+
23
+ @CapacitorPlugin(name = "SpeechSynthesis")
24
+ public class SpeechSynthesisPlugin extends Plugin {
25
+
26
+ private final String pluginVersion = "7.0.0";
27
+ private TextToSpeech tts;
28
+ private int utteranceIdCounter = 0;
29
+ private boolean ttsInitialized = false;
30
+
31
+ @Override
32
+ public void load() {
33
+ super.load();
34
+ initializeTTS();
35
+ }
36
+
37
+ private void initializeTTS() {
38
+ tts = new TextToSpeech(getContext(), (status) -> {
39
+ if (status == TextToSpeech.SUCCESS) {
40
+ ttsInitialized = true;
41
+ setupUtteranceProgressListener();
42
+ }
43
+ });
44
+ }
45
+
46
+ private void setupUtteranceProgressListener() {
47
+ tts.setOnUtteranceProgressListener(
48
+ new UtteranceProgressListener() {
49
+ @Override
50
+ public void onStart(String utteranceId) {
51
+ JSObject data = new JSObject();
52
+ data.put("utteranceId", utteranceId);
53
+ notifyListeners("start", data);
54
+ }
55
+
56
+ @Override
57
+ public void onDone(String utteranceId) {
58
+ JSObject data = new JSObject();
59
+ data.put("utteranceId", utteranceId);
60
+ notifyListeners("end", data);
61
+ }
62
+
63
+ @Override
64
+ @Deprecated
65
+ public void onError(String utteranceId) {
66
+ JSObject data = new JSObject();
67
+ data.put("utteranceId", utteranceId);
68
+ data.put("error", "Speech synthesis error");
69
+ notifyListeners("error", data);
70
+ }
71
+
72
+ @Override
73
+ public void onError(String utteranceId, int errorCode) {
74
+ JSObject data = new JSObject();
75
+ data.put("utteranceId", utteranceId);
76
+ data.put("error", getErrorMessage(errorCode));
77
+ notifyListeners("error", data);
78
+ }
79
+
80
+ @Override
81
+ public void onRangeStart(String utteranceId, int start, int end, int frame) {
82
+ JSObject data = new JSObject();
83
+ data.put("utteranceId", utteranceId);
84
+ data.put("charIndex", start);
85
+ data.put("charLength", end - start);
86
+ notifyListeners("boundary", data);
87
+ }
88
+ }
89
+ );
90
+ }
91
+
92
+ private String getErrorMessage(int errorCode) {
93
+ switch (errorCode) {
94
+ case TextToSpeech.ERROR_SYNTHESIS:
95
+ return "Synthesis error";
96
+ case TextToSpeech.ERROR_SERVICE:
97
+ return "Service error";
98
+ case TextToSpeech.ERROR_OUTPUT:
99
+ return "Output error";
100
+ case TextToSpeech.ERROR_NETWORK:
101
+ return "Network error";
102
+ case TextToSpeech.ERROR_NETWORK_TIMEOUT:
103
+ return "Network timeout";
104
+ case TextToSpeech.ERROR_INVALID_REQUEST:
105
+ return "Invalid request";
106
+ case TextToSpeech.ERROR_NOT_INSTALLED_YET:
107
+ return "TTS not installed yet";
108
+ default:
109
+ return "Unknown error";
110
+ }
111
+ }
112
+
113
+ @PluginMethod
114
+ public void speak(PluginCall call) {
115
+ if (!ttsInitialized) {
116
+ call.reject("Text-to-Speech engine not initialized");
117
+ return;
118
+ }
119
+
120
+ String text = call.getString("text");
121
+ if (text == null || text.isEmpty()) {
122
+ call.reject("Text is required");
123
+ return;
124
+ }
125
+
126
+ String utteranceId = "android-utterance-" + (utteranceIdCounter++);
127
+
128
+ // Set language or voice
129
+ String language = call.getString("language");
130
+ String voiceId = call.getString("voiceId");
131
+
132
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && voiceId != null) {
133
+ Voice voice = findVoiceById(voiceId);
134
+ if (voice != null) {
135
+ tts.setVoice(voice);
136
+ }
137
+ } else if (language != null) {
138
+ Locale locale = Locale.forLanguageTag(language);
139
+ tts.setLanguage(locale);
140
+ }
141
+
142
+ // Set speech parameters
143
+ Float pitch = call.getFloat("pitch", 1.0f);
144
+ Float rate = call.getFloat("rate", 1.0f);
145
+
146
+ tts.setPitch(pitch);
147
+ tts.setSpeechRate(rate);
148
+
149
+ // Handle queue strategy
150
+ String queueStrategy = call.getString("queueStrategy", "Add");
151
+ int queueMode = queueStrategy.equals("Flush") ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD;
152
+
153
+ // Create parameters bundle
154
+ Bundle params = new Bundle();
155
+ Float volume = call.getFloat("volume");
156
+ if (volume != null) {
157
+ params.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume);
158
+ }
159
+ params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
160
+
161
+ // Speak
162
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
163
+ tts.speak(text, queueMode, params, utteranceId);
164
+ } else {
165
+ HashMap<String, String> paramsMap = new HashMap<>();
166
+ paramsMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
167
+ if (volume != null) {
168
+ paramsMap.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, String.valueOf(volume));
169
+ }
170
+ tts.speak(text, queueMode, paramsMap);
171
+ }
172
+
173
+ JSObject result = new JSObject();
174
+ result.put("utteranceId", utteranceId);
175
+ call.resolve(result);
176
+ }
177
+
178
+ @PluginMethod
179
+ public void synthesizeToFile(PluginCall call) {
180
+ if (!ttsInitialized) {
181
+ call.reject("Text-to-Speech engine not initialized");
182
+ return;
183
+ }
184
+
185
+ String text = call.getString("text");
186
+ if (text == null || text.isEmpty()) {
187
+ call.reject("Text is required");
188
+ return;
189
+ }
190
+
191
+ String utteranceId = "android-file-" + (utteranceIdCounter++);
192
+
193
+ // Set language or voice
194
+ String language = call.getString("language");
195
+ String voiceId = call.getString("voiceId");
196
+
197
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && voiceId != null) {
198
+ Voice voice = findVoiceById(voiceId);
199
+ if (voice != null) {
200
+ tts.setVoice(voice);
201
+ }
202
+ } else if (language != null) {
203
+ Locale locale = Locale.forLanguageTag(language);
204
+ tts.setLanguage(locale);
205
+ }
206
+
207
+ // Set speech parameters
208
+ Float pitch = call.getFloat("pitch", 1.0f);
209
+ Float rate = call.getFloat("rate", 1.0f);
210
+
211
+ tts.setPitch(pitch);
212
+ tts.setSpeechRate(rate);
213
+
214
+ // Create output file
215
+ File outputFile = new File(getContext().getFilesDir(), utteranceId + ".wav");
216
+
217
+ // Synthesize to file
218
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
219
+ Bundle params = new Bundle();
220
+ params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
221
+
222
+ int result = tts.synthesizeToFile(text, params, outputFile, utteranceId);
223
+
224
+ if (result == TextToSpeech.SUCCESS) {
225
+ JSObject response = new JSObject();
226
+ response.put("filePath", outputFile.getAbsolutePath());
227
+ response.put("utteranceId", utteranceId);
228
+ call.resolve(response);
229
+ } else {
230
+ call.reject("Failed to synthesize to file");
231
+ }
232
+ } else {
233
+ HashMap<String, String> params = new HashMap<>();
234
+ params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId);
235
+
236
+ int result = tts.synthesizeToFile(text, params, outputFile.getAbsolutePath());
237
+
238
+ if (result == TextToSpeech.SUCCESS) {
239
+ JSObject response = new JSObject();
240
+ response.put("filePath", outputFile.getAbsolutePath());
241
+ response.put("utteranceId", utteranceId);
242
+ call.resolve(response);
243
+ } else {
244
+ call.reject("Failed to synthesize to file");
245
+ }
246
+ }
247
+ }
248
+
249
+ @PluginMethod
250
+ public void cancel(PluginCall call) {
251
+ if (tts != null) {
252
+ tts.stop();
253
+ }
254
+ call.resolve();
255
+ }
256
+
257
+ @PluginMethod
258
+ public void pause(PluginCall call) {
259
+ // Android TTS doesn't support pause/resume natively
260
+ // We'll stop instead as a fallback
261
+ if (tts != null) {
262
+ tts.stop();
263
+ }
264
+ call.resolve();
265
+ }
266
+
267
+ @PluginMethod
268
+ public void resume(PluginCall call) {
269
+ // Android TTS doesn't support pause/resume natively
270
+ call.resolve();
271
+ }
272
+
273
+ @PluginMethod
274
+ public void isSpeaking(PluginCall call) {
275
+ boolean isSpeaking = tts != null && tts.isSpeaking();
276
+ JSObject result = new JSObject();
277
+ result.put("isSpeaking", isSpeaking);
278
+ call.resolve(result);
279
+ }
280
+
281
+ @PluginMethod
282
+ public void isAvailable(PluginCall call) {
283
+ JSObject result = new JSObject();
284
+ result.put("isAvailable", ttsInitialized);
285
+ call.resolve(result);
286
+ }
287
+
288
+ @PluginMethod
289
+ public void getVoices(PluginCall call) {
290
+ JSArray voicesArray = new JSArray();
291
+
292
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && tts != null) {
293
+ Set<Voice> voices = tts.getVoices();
294
+ if (voices != null) {
295
+ for (Voice voice : voices) {
296
+ JSObject voiceInfo = new JSObject();
297
+ voiceInfo.put("id", voice.getName());
298
+ voiceInfo.put("name", voice.getName());
299
+ voiceInfo.put("language", voice.getLocale().toLanguageTag());
300
+ voiceInfo.put("isNetworkConnectionRequired", voice.isNetworkConnectionRequired());
301
+
302
+ voicesArray.put(voiceInfo);
303
+ }
304
+ }
305
+ } else {
306
+ // Fallback for older Android versions
307
+ Locale[] locales = Locale.getAvailableLocales();
308
+ for (Locale locale : locales) {
309
+ if (tts != null && tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
310
+ JSObject voiceInfo = new JSObject();
311
+ String tag = locale.toLanguageTag();
312
+ voiceInfo.put("id", tag);
313
+ voiceInfo.put("name", locale.getDisplayName());
314
+ voiceInfo.put("language", tag);
315
+ voicesArray.put(voiceInfo);
316
+ }
317
+ }
318
+ }
319
+
320
+ JSObject result = new JSObject();
321
+ result.put("voices", voicesArray);
322
+ call.resolve(result);
323
+ }
324
+
325
+ @PluginMethod
326
+ public void getLanguages(PluginCall call) {
327
+ Set<String> languageSet = new HashSet<>();
328
+
329
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && tts != null) {
330
+ Set<Voice> voices = tts.getVoices();
331
+ if (voices != null) {
332
+ for (Voice voice : voices) {
333
+ languageSet.add(voice.getLocale().toLanguageTag());
334
+ }
335
+ }
336
+ } else {
337
+ // Fallback for older Android versions
338
+ Locale[] locales = Locale.getAvailableLocales();
339
+ for (Locale locale : locales) {
340
+ if (tts != null && tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE) {
341
+ languageSet.add(locale.toLanguageTag());
342
+ }
343
+ }
344
+ }
345
+
346
+ List<String> languages = new ArrayList<>(languageSet);
347
+ JSArray languagesArray = new JSArray(languages);
348
+
349
+ JSObject result = new JSObject();
350
+ result.put("languages", languagesArray);
351
+ call.resolve(result);
352
+ }
353
+
354
+ @PluginMethod
355
+ public void isLanguageAvailable(PluginCall call) {
356
+ String language = call.getString("language");
357
+ if (language == null) {
358
+ call.reject("Language is required");
359
+ return;
360
+ }
361
+
362
+ Locale locale = Locale.forLanguageTag(language);
363
+ boolean isAvailable = tts != null && tts.isLanguageAvailable(locale) >= TextToSpeech.LANG_AVAILABLE;
364
+
365
+ JSObject result = new JSObject();
366
+ result.put("isAvailable", isAvailable);
367
+ call.resolve(result);
368
+ }
369
+
370
+ @PluginMethod
371
+ public void isVoiceAvailable(PluginCall call) {
372
+ String voiceId = call.getString("voiceId");
373
+ if (voiceId == null) {
374
+ call.reject("Voice ID is required");
375
+ return;
376
+ }
377
+
378
+ boolean isAvailable = false;
379
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
380
+ Voice voice = findVoiceById(voiceId);
381
+ isAvailable = voice != null;
382
+ }
383
+
384
+ JSObject result = new JSObject();
385
+ result.put("isAvailable", isAvailable);
386
+ call.resolve(result);
387
+ }
388
+
389
+ @PluginMethod
390
+ public void initialize(PluginCall call) {
391
+ if (!ttsInitialized) {
392
+ initializeTTS();
393
+ }
394
+ call.resolve();
395
+ }
396
+
397
+ @PluginMethod
398
+ public void activateAudioSession(PluginCall call) {
399
+ // Not applicable on Android
400
+ call.resolve();
401
+ }
402
+
403
+ @PluginMethod
404
+ public void deactivateAudioSession(PluginCall call) {
405
+ // Not applicable on Android
406
+ call.resolve();
407
+ }
408
+
409
+ @PluginMethod
410
+ public void getPluginVersion(PluginCall call) {
411
+ JSObject result = new JSObject();
412
+ result.put("version", pluginVersion);
413
+ call.resolve(result);
414
+ }
415
+
416
+ private Voice findVoiceById(String voiceId) {
417
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && tts != null) {
418
+ Set<Voice> voices = tts.getVoices();
419
+ if (voices != null) {
420
+ for (Voice voice : voices) {
421
+ if (voice.getName().equals(voiceId)) {
422
+ return voice;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ return null;
428
+ }
429
+
430
+ @Override
431
+ protected void handleOnDestroy() {
432
+ if (tts != null) {
433
+ tts.stop();
434
+ tts.shutdown();
435
+ }
436
+ super.handleOnDestroy();
437
+ }
438
+ }