@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.
- package/CapgoCapacitorSpeechSynthesis.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +507 -0
- package/android/build.gradle +58 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/ee/forgr/plugin/speechsynthesis/SpeechSynthesisPlugin.java +438 -0
- package/dist/docs.json +1089 -0
- package/dist/esm/definitions.d.ts +519 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +35 -0
- package/dist/esm/web.js +153 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +167 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +170 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/SpeechSynthesisPlugin/SpeechSynthesisPlugin.swift +338 -0
- package/ios/Tests/SpeechSynthesisPluginTests/SpeechSynthesisPluginTests.swift +10 -0
- package/package.json +86 -0
|
@@ -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
|
+
}
|