capacitor-microphone 0.0.24 → 0.0.26
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.
|
@@ -17,8 +17,6 @@ import android.speech.RecognitionListener;
|
|
|
17
17
|
import android.speech.RecognizerIntent;
|
|
18
18
|
import android.speech.SpeechRecognizer;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
20
|
import java.util.ArrayList;
|
|
23
21
|
|
|
24
22
|
@CapacitorPlugin(
|
|
@@ -32,24 +30,32 @@ import java.util.ArrayList;
|
|
|
32
30
|
)
|
|
33
31
|
public class CapacitorMicrophonePlugin extends Plugin {
|
|
34
32
|
|
|
35
|
-
private SpeechRecognizer speechRecognizer;
|
|
36
|
-
private PluginCall currentCall;
|
|
37
|
-
private Intent speechIntent;
|
|
38
|
-
private boolean isListening = false;
|
|
39
|
-
private boolean isDestroyed = false;
|
|
40
|
-
private long lastStopTime = 0;
|
|
41
|
-
private static final long RESTART_COOLDOWN_MS = 400;
|
|
42
|
-
private Runnable delayedStartRunnable;
|
|
43
|
-
private
|
|
44
|
-
private
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
private SpeechRecognizer speechRecognizer;
|
|
34
|
+
private PluginCall currentCall;
|
|
35
|
+
private Intent speechIntent;
|
|
36
|
+
private boolean isListening = false;
|
|
37
|
+
private boolean isDestroyed = false;
|
|
38
|
+
private long lastStopTime = 0;
|
|
39
|
+
private static final long RESTART_COOLDOWN_MS = 400;
|
|
40
|
+
private Runnable delayedStartRunnable;
|
|
41
|
+
private Runnable silenceRunnable;
|
|
42
|
+
private static final long SILENCE_TIMEOUT_MS = 3000;
|
|
43
|
+
private boolean shouldClearOnNextResult = false;
|
|
44
|
+
private StringBuilder accumulatedText = new StringBuilder();
|
|
45
|
+
private String lastSentPartial = "";
|
|
46
|
+
private boolean isInSpeech = false;
|
|
47
|
+
private long lastSpeechTime = 0;
|
|
48
|
+
private static final long SPEECH_PAUSE_THRESHOLD = 1500;
|
|
49
|
+
private float lastRmsValue = 0;
|
|
50
|
+
private static final float RMS_THRESHOLD = 2.0f;
|
|
51
|
+
private boolean hasRealSpeech = false;
|
|
52
|
+
private String pendingPartialText = "";
|
|
53
|
+
private long lastPartialTime = 0;
|
|
54
|
+
|
|
55
|
+
// Métodos de permisos (sin cambios)
|
|
49
56
|
@PluginMethod
|
|
50
57
|
public void checkPermission(PluginCall call) {
|
|
51
58
|
PermissionState state = getPermissionState("microphone");
|
|
52
|
-
|
|
53
59
|
JSObject result = new JSObject();
|
|
54
60
|
result.put("granted", state == PermissionState.GRANTED);
|
|
55
61
|
result.put("status",
|
|
@@ -58,7 +64,6 @@ private static final long SILENCE_TIMEOUT_MS = 4000;
|
|
|
58
64
|
);
|
|
59
65
|
result.put("details", "Android checkPermission");
|
|
60
66
|
result.put("errorMessage", "");
|
|
61
|
-
|
|
62
67
|
call.resolve(result);
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -73,14 +78,12 @@ private static final long SILENCE_TIMEOUT_MS = 4000;
|
|
|
73
78
|
call.resolve(result);
|
|
74
79
|
return;
|
|
75
80
|
}
|
|
76
|
-
|
|
77
81
|
requestPermissionForAlias("microphone", call, "permissionCallback");
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
@PluginMethod
|
|
81
85
|
public void checkRequestPermission(PluginCall call) {
|
|
82
86
|
PermissionState state = getPermissionState("microphone");
|
|
83
|
-
|
|
84
87
|
if (state == PermissionState.GRANTED) {
|
|
85
88
|
JSObject result = new JSObject();
|
|
86
89
|
result.put("granted", true);
|
|
@@ -90,223 +93,558 @@ private static final long SILENCE_TIMEOUT_MS = 4000;
|
|
|
90
93
|
call.resolve(result);
|
|
91
94
|
return;
|
|
92
95
|
}
|
|
93
|
-
|
|
94
|
-
if (state == PermissionState.DENIED) {
|
|
95
|
-
requestPermissionForAlias("microphone", call, "permissionCallback");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
96
|
requestPermissionForAlias("microphone", call, "permissionCallback");
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
@PermissionCallback
|
|
103
100
|
private void permissionCallback(PluginCall call) {
|
|
104
101
|
boolean granted = getPermissionState("microphone") == PermissionState.GRANTED;
|
|
105
|
-
|
|
106
102
|
JSObject result = new JSObject();
|
|
107
103
|
result.put("granted", granted);
|
|
108
104
|
result.put("status", granted ? "granted" : "denied");
|
|
109
105
|
result.put("details", "Android permission callback");
|
|
110
106
|
result.put("errorMessage", granted ? "" : "User denied microphone permission");
|
|
111
|
-
|
|
112
107
|
call.resolve(result);
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
@PluginMethod
|
|
116
|
-
public void startListening(PluginCall call) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (delayedStartRunnable != null) {
|
|
120
|
-
getActivity().runOnUiThread(() -> {
|
|
121
|
-
getActivity()
|
|
122
|
-
.getWindow()
|
|
123
|
-
.getDecorView()
|
|
124
|
-
.removeCallbacks(delayedStartRunnable);
|
|
125
|
-
});
|
|
126
|
-
delayedStartRunnable = null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
long now = System.currentTimeMillis();
|
|
130
|
-
long diff = now - lastStopTime;
|
|
131
|
-
|
|
132
|
-
if (diff < RESTART_COOLDOWN_MS) {
|
|
133
|
-
long delay = RESTART_COOLDOWN_MS - diff;
|
|
110
|
+
@PluginMethod
|
|
111
|
+
public void startListening(PluginCall call) {
|
|
112
|
+
String lang = call.getString("lang", "es-MX");
|
|
134
113
|
|
|
135
|
-
delayedStartRunnable
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
114
|
+
if (delayedStartRunnable != null) {
|
|
115
|
+
getActivity().runOnUiThread(() -> {
|
|
116
|
+
getActivity()
|
|
117
|
+
.getWindow()
|
|
118
|
+
.getDecorView()
|
|
119
|
+
.removeCallbacks(delayedStartRunnable);
|
|
120
|
+
});
|
|
141
121
|
delayedStartRunnable = null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
getActivity().runOnUiThread(() -> {
|
|
145
|
-
getActivity()
|
|
146
|
-
.getWindow()
|
|
147
|
-
.getDecorView()
|
|
148
|
-
.postDelayed(delayedStartRunnable, delay);
|
|
149
|
-
});
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
startListeningInternal(call, lang);
|
|
154
|
-
}
|
|
155
|
-
|
|
122
|
+
}
|
|
156
123
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
isDestroyed = false;
|
|
160
|
-
isListening = true;
|
|
124
|
+
long now = System.currentTimeMillis();
|
|
125
|
+
long diff = now - lastStopTime;
|
|
161
126
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
127
|
+
if (diff < RESTART_COOLDOWN_MS) {
|
|
128
|
+
long delay = RESTART_COOLDOWN_MS - diff;
|
|
129
|
+
delayedStartRunnable = () -> {
|
|
130
|
+
if (!isDestroyed) {
|
|
131
|
+
startListeningInternal(call, lang);
|
|
132
|
+
} else {
|
|
133
|
+
call.reject("Listening was cancelled");
|
|
134
|
+
}
|
|
135
|
+
delayedStartRunnable = null;
|
|
136
|
+
};
|
|
166
137
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
138
|
+
getActivity().runOnUiThread(() -> {
|
|
139
|
+
getActivity()
|
|
140
|
+
.getWindow()
|
|
141
|
+
.getDecorView()
|
|
142
|
+
.postDelayed(delayedStartRunnable, delay);
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
171
146
|
|
|
172
|
-
|
|
173
|
-
stopSpeech();
|
|
147
|
+
startListeningInternal(call, lang);
|
|
174
148
|
}
|
|
175
149
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
getActivity().runOnUiThread(() -> {
|
|
186
|
-
try {
|
|
187
|
-
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
|
|
188
|
-
speechRecognizer.setRecognitionListener(new RecognitionListener() {
|
|
189
|
-
|
|
190
|
-
@Override
|
|
191
|
-
public void onReadyForSpeech(Bundle params) {
|
|
192
|
-
scheduleSilenceRestart();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
@Override public void onBeginningOfSpeech() {}
|
|
196
|
-
@Override public void onRmsChanged(float rmsdB) {}
|
|
197
|
-
@Override public void onBufferReceived(byte[] buffer) {}
|
|
198
|
-
@Override public void onEndOfSpeech() {}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@Override
|
|
202
|
-
public void onError(int error) {
|
|
203
|
-
if (isDestroyed || !isListening) return;
|
|
204
|
-
|
|
205
|
-
if (!isDestroyed && isListening && speechRecognizer != null && speechIntent != null) {
|
|
206
|
-
getActivity().runOnUiThread(() -> {
|
|
207
|
-
try {
|
|
208
|
-
speechRecognizer.startListening(speechIntent);
|
|
209
|
-
} catch (Exception ignored) {}
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
150
|
+
private void startListeningInternal(PluginCall call, String lang) {
|
|
151
|
+
// Solo limpiar si es un inicio nuevo, no si es continuación
|
|
152
|
+
if (!isListening) {
|
|
153
|
+
accumulatedText.setLength(0);
|
|
154
|
+
lastSentPartial = "";
|
|
155
|
+
hasRealSpeech = false;
|
|
156
|
+
pendingPartialText = "";
|
|
157
|
+
}
|
|
213
158
|
|
|
159
|
+
shouldClearOnNextResult = false;
|
|
160
|
+
isDestroyed = false;
|
|
161
|
+
isListening = true;
|
|
162
|
+
isInSpeech = false;
|
|
163
|
+
lastRmsValue = 0;
|
|
214
164
|
|
|
215
|
-
|
|
216
|
-
|
|
165
|
+
if (getPermissionState("microphone") != PermissionState.GRANTED) {
|
|
166
|
+
call.reject("Microphone permission not granted");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
217
169
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
170
|
+
if (!SpeechRecognizer.isRecognitionAvailable(getContext())) {
|
|
171
|
+
call.reject("Speech recognition not available");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
222
174
|
|
|
223
|
-
|
|
175
|
+
if (speechRecognizer != null) {
|
|
176
|
+
stopSpeech();
|
|
177
|
+
}
|
|
224
178
|
|
|
225
|
-
|
|
226
|
-
|
|
179
|
+
// Notificar inicio con texto acumulado (si hay)
|
|
180
|
+
if (accumulatedText.length() > 0) {
|
|
181
|
+
notifyListeners(
|
|
182
|
+
"partialResult",
|
|
183
|
+
new JSObject()
|
|
184
|
+
.put("text", accumulatedText.toString())
|
|
185
|
+
.put("isFinal", false)
|
|
186
|
+
.put("hasSpeech", true)
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
// Enviar evento vacío para indicar que está listo pero no hay habla
|
|
190
|
+
notifyListeners(
|
|
191
|
+
"partialResult",
|
|
192
|
+
new JSObject()
|
|
193
|
+
.put("text", "")
|
|
194
|
+
.put("isFinal", false)
|
|
195
|
+
.put("hasSpeech", false)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
227
198
|
|
|
228
|
-
|
|
229
|
-
notifyListeners(
|
|
230
|
-
"partialResult",
|
|
231
|
-
new JSObject()
|
|
232
|
-
.put("text", matches.get(0))
|
|
233
|
-
.put("isFinal", true)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
199
|
+
this.currentCall = call;
|
|
236
200
|
|
|
237
|
-
|
|
201
|
+
getActivity().runOnUiThread(() -> {
|
|
202
|
+
try {
|
|
203
|
+
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
|
|
204
|
+
speechRecognizer.setRecognitionListener(new RecognitionListener() {
|
|
205
|
+
|
|
206
|
+
@Override
|
|
207
|
+
public void onReadyForSpeech(Bundle params) {
|
|
208
|
+
cancelSilenceTimeout();
|
|
209
|
+
scheduleSilenceTimeout();
|
|
210
|
+
// Indicar que está listo pero no hay habla aún
|
|
211
|
+
if (!hasRealSpeech && accumulatedText.length() == 0) {
|
|
212
|
+
notifyListeners(
|
|
213
|
+
"partialResult",
|
|
214
|
+
new JSObject()
|
|
215
|
+
.put("text", "")
|
|
216
|
+
.put("isFinal", false)
|
|
217
|
+
.put("hasSpeech", false)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@Override
|
|
223
|
+
public void onBeginningOfSpeech() {
|
|
224
|
+
isInSpeech = true;
|
|
225
|
+
hasRealSpeech = true;
|
|
226
|
+
lastSpeechTime = System.currentTimeMillis();
|
|
227
|
+
cancelSilenceTimeout();
|
|
228
|
+
|
|
229
|
+
// Notificar que comenzó el habla
|
|
230
|
+
notifyListeners(
|
|
231
|
+
"partialResult",
|
|
232
|
+
new JSObject()
|
|
233
|
+
.put("text", accumulatedText.length() > 0 ? accumulatedText.toString() : "")
|
|
234
|
+
.put("isFinal", false)
|
|
235
|
+
.put("hasSpeech", true)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@Override
|
|
240
|
+
public void onRmsChanged(float rmsdB) {
|
|
241
|
+
lastRmsValue = rmsdB;
|
|
242
|
+
|
|
243
|
+
// Detectar si hay actividad de voz real
|
|
244
|
+
if (rmsdB > RMS_THRESHOLD) {
|
|
245
|
+
isInSpeech = true;
|
|
246
|
+
hasRealSpeech = true;
|
|
247
|
+
lastSpeechTime = System.currentTimeMillis();
|
|
248
|
+
cancelSilenceTimeout();
|
|
249
|
+
scheduleSilenceTimeout();
|
|
250
|
+
} else if (rmsdB < 0.5f && !isInSpeech) {
|
|
251
|
+
// Muy bajo RMS, probablemente silencio
|
|
252
|
+
// No enviar eventos de habla si no hay texto
|
|
253
|
+
if (!hasRealSpeech && pendingPartialText.isEmpty()) {
|
|
254
|
+
notifyListeners(
|
|
255
|
+
"partialResult",
|
|
256
|
+
new JSObject()
|
|
257
|
+
.put("text", "")
|
|
258
|
+
.put("isFinal", false)
|
|
259
|
+
.put("hasSpeech", false)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@Override
|
|
266
|
+
public void onBufferReceived(byte[] buffer) {}
|
|
267
|
+
|
|
268
|
+
@Override
|
|
269
|
+
public void onEndOfSpeech() {
|
|
270
|
+
isInSpeech = false;
|
|
271
|
+
// Programar timeout más corto para pausas entre palabras
|
|
272
|
+
cancelSilenceTimeout();
|
|
273
|
+
getActivity()
|
|
274
|
+
.getWindow()
|
|
275
|
+
.getDecorView()
|
|
276
|
+
.postDelayed(() -> {
|
|
277
|
+
if (!isInSpeech && isListening && !isDestroyed) {
|
|
278
|
+
scheduleSilenceTimeout();
|
|
279
|
+
}
|
|
280
|
+
}, 500);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@Override
|
|
284
|
+
public void onError(int error) {
|
|
285
|
+
if (isDestroyed || !isListening) return;
|
|
286
|
+
|
|
287
|
+
// Manejo diferente para cada tipo de error
|
|
288
|
+
switch (error) {
|
|
289
|
+
case SpeechRecognizer.ERROR_NO_MATCH:
|
|
290
|
+
// No se reconoció voz - no es un error real
|
|
291
|
+
if (hasRealSpeech) {
|
|
292
|
+
// Había habla pero no se reconoció, mantener estado
|
|
293
|
+
restartListeningQuietly();
|
|
294
|
+
} else {
|
|
295
|
+
// Nunca hubo habla, mantener estado silencioso
|
|
296
|
+
notifyListeners(
|
|
297
|
+
"partialResult",
|
|
298
|
+
new JSObject()
|
|
299
|
+
.put("text", "")
|
|
300
|
+
.put("isFinal", false)
|
|
301
|
+
.put("hasSpeech", false)
|
|
302
|
+
);
|
|
303
|
+
restartListeningQuietly();
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
|
|
307
|
+
// Timeout de habla - reiniciar silenciosamente
|
|
308
|
+
restartListeningQuietly();
|
|
309
|
+
break;
|
|
310
|
+
case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
|
|
311
|
+
// Esperar y reintentar
|
|
312
|
+
getActivity()
|
|
313
|
+
.getWindow()
|
|
314
|
+
.getDecorView()
|
|
315
|
+
.postDelayed(() -> restartListeningAfterError(), 300);
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
// Otros errores, intentar reiniciar
|
|
319
|
+
restartListeningAfterError();
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@Override
|
|
325
|
+
public void onResults(Bundle results) {
|
|
326
|
+
if (shouldClearOnNextResult) {
|
|
327
|
+
shouldClearOnNextResult = false;
|
|
328
|
+
accumulatedText.setLength(0);
|
|
329
|
+
lastSentPartial = "";
|
|
330
|
+
hasRealSpeech = false;
|
|
331
|
+
pendingPartialText = "";
|
|
332
|
+
restartListeningQuietly();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!isListening || isDestroyed) return;
|
|
337
|
+
|
|
338
|
+
ArrayList<String> matches =
|
|
339
|
+
results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
|
|
340
|
+
|
|
341
|
+
if (matches != null && !matches.isEmpty()) {
|
|
342
|
+
String finalText = matches.get(0);
|
|
343
|
+
|
|
344
|
+
// Solo procesar si hay texto real
|
|
345
|
+
if (finalText != null && !finalText.trim().isEmpty()) {
|
|
346
|
+
// Actualizar texto acumulado
|
|
347
|
+
if (!accumulatedText.toString().endsWith(finalText)) {
|
|
348
|
+
// Agregar espacio si hay texto previo
|
|
349
|
+
if (accumulatedText.length() > 0 && !accumulatedText.toString().endsWith(" ")) {
|
|
350
|
+
accumulatedText.append(" ");
|
|
351
|
+
}
|
|
352
|
+
accumulatedText.append(finalText);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Enviar resultado final
|
|
356
|
+
notifyListeners(
|
|
357
|
+
"partialResult",
|
|
358
|
+
new JSObject()
|
|
359
|
+
.put("text", accumulatedText.toString())
|
|
360
|
+
.put("isFinal", true)
|
|
361
|
+
.put("hasSpeech", true)
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
lastSentPartial = accumulatedText.toString();
|
|
365
|
+
pendingPartialText = "";
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
// No hay resultados - mantener estado actual
|
|
369
|
+
if (accumulatedText.length() > 0) {
|
|
370
|
+
notifyListeners(
|
|
371
|
+
"partialResult",
|
|
372
|
+
new JSObject()
|
|
373
|
+
.put("text", accumulatedText.toString())
|
|
374
|
+
.put("isFinal", false)
|
|
375
|
+
.put("hasSpeech", true)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Reiniciar para continuar escuchando
|
|
381
|
+
restartListeningQuietly();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@Override
|
|
385
|
+
public void onPartialResults(Bundle partialResults) {
|
|
386
|
+
if (!isListening || isDestroyed) return;
|
|
387
|
+
|
|
388
|
+
ArrayList<String> matches =
|
|
389
|
+
partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
|
|
390
|
+
|
|
391
|
+
if (matches != null && !matches.isEmpty()) {
|
|
392
|
+
String partialText = matches.get(0);
|
|
393
|
+
lastPartialTime = System.currentTimeMillis();
|
|
394
|
+
|
|
395
|
+
// Filtrar resultados vacíos o muy cortos
|
|
396
|
+
if (partialText != null && !partialText.trim().isEmpty() && partialText.length() > 1) {
|
|
397
|
+
pendingPartialText = partialText;
|
|
398
|
+
hasRealSpeech = true;
|
|
399
|
+
|
|
400
|
+
// Construir texto completo: acumulado + nuevo parcial
|
|
401
|
+
String currentText = accumulatedText.toString();
|
|
402
|
+
String displayText;
|
|
403
|
+
|
|
404
|
+
if (currentText.isEmpty()) {
|
|
405
|
+
displayText = partialText;
|
|
406
|
+
} else {
|
|
407
|
+
// Si el texto parcial ya está contenido en el acumulado, usar solo acumulado
|
|
408
|
+
if (currentText.contains(partialText)) {
|
|
409
|
+
displayText = currentText;
|
|
410
|
+
} else {
|
|
411
|
+
// Sino, mostrar acumulado + nuevo
|
|
412
|
+
displayText = currentText + " " + partialText;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Solo enviar si es diferente del último enviado
|
|
417
|
+
if (!displayText.equals(lastSentPartial)) {
|
|
418
|
+
notifyListeners(
|
|
419
|
+
"partialResult",
|
|
420
|
+
new JSObject()
|
|
421
|
+
.put("text", displayText)
|
|
422
|
+
.put("isFinal", false)
|
|
423
|
+
.put("hasSpeech", true)
|
|
424
|
+
);
|
|
425
|
+
lastSentPartial = displayText;
|
|
426
|
+
}
|
|
427
|
+
} else if (hasRealSpeech) {
|
|
428
|
+
// Hay habla previa pero el parcial está vacío
|
|
429
|
+
// Mantener el último texto enviado
|
|
430
|
+
notifyListeners(
|
|
431
|
+
"partialResult",
|
|
432
|
+
new JSObject()
|
|
433
|
+
.put("text", accumulatedText.toString())
|
|
434
|
+
.put("isFinal", false)
|
|
435
|
+
.put("hasSpeech", true)
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
// Nunca ha habido habla, enviar estado silencioso
|
|
439
|
+
notifyListeners(
|
|
440
|
+
"partialResult",
|
|
441
|
+
new JSObject()
|
|
442
|
+
.put("text", "")
|
|
443
|
+
.put("isFinal", false)
|
|
444
|
+
.put("hasSpeech", false)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Reiniciar timeout cuando recibimos resultados
|
|
449
|
+
cancelSilenceTimeout();
|
|
450
|
+
scheduleSilenceTimeout();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@Override
|
|
455
|
+
public void onEvent(int eventType, Bundle params) {}
|
|
456
|
+
});
|
|
238
457
|
|
|
239
|
-
|
|
458
|
+
speechIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
|
|
459
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
|
|
460
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, lang);
|
|
461
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
|
|
462
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
|
|
463
|
+
// Configuración para evitar timeout muy rápidos
|
|
464
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS, 10000);
|
|
465
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS, 4000);
|
|
466
|
+
speechIntent.putExtra(RecognizerIntent.EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS, 3000);
|
|
240
467
|
|
|
241
|
-
|
|
468
|
+
speechRecognizer.startListening(speechIntent);
|
|
469
|
+
} catch (Exception e) {
|
|
470
|
+
if (currentCall != null) {
|
|
471
|
+
currentCall.reject(e.getMessage());
|
|
472
|
+
currentCall = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
242
477
|
|
|
478
|
+
private void restartListeningQuietly() {
|
|
479
|
+
if (isDestroyed || !isListening || speechRecognizer == null || speechIntent == null) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
243
482
|
|
|
483
|
+
getActivity().runOnUiThread(() -> {
|
|
484
|
+
try {
|
|
485
|
+
// Pequeña pausa antes de reiniciar
|
|
486
|
+
getActivity()
|
|
487
|
+
.getWindow()
|
|
488
|
+
.getDecorView()
|
|
489
|
+
.postDelayed(() -> {
|
|
490
|
+
if (!isDestroyed && isListening && speechRecognizer != null) {
|
|
491
|
+
try {
|
|
492
|
+
speechRecognizer.startListening(speechIntent);
|
|
493
|
+
} catch (Exception ignored) {}
|
|
494
|
+
}
|
|
495
|
+
}, 100);
|
|
496
|
+
} catch (Exception ignored) {}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
244
499
|
|
|
245
|
-
|
|
246
|
-
|
|
500
|
+
private void restartListeningAfterError() {
|
|
501
|
+
if (isDestroyed || !isListening) return;
|
|
247
502
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
503
|
+
getActivity().runOnUiThread(() -> {
|
|
504
|
+
if (!isDestroyed && isListening && speechRecognizer != null && speechIntent != null) {
|
|
505
|
+
try {
|
|
506
|
+
// Pausa más larga después de un error
|
|
507
|
+
getActivity()
|
|
508
|
+
.getWindow()
|
|
509
|
+
.getDecorView()
|
|
510
|
+
.postDelayed(() -> {
|
|
511
|
+
if (!isDestroyed && isListening && speechRecognizer != null) {
|
|
512
|
+
try {
|
|
513
|
+
speechRecognizer.startListening(speechIntent);
|
|
514
|
+
} catch (Exception ignored) {}
|
|
515
|
+
}
|
|
516
|
+
}, 800);
|
|
517
|
+
} catch (Exception ignored) {}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
}
|
|
252
521
|
|
|
522
|
+
private void scheduleSilenceTimeout() {
|
|
523
|
+
cancelSilenceTimeout();
|
|
524
|
+
|
|
525
|
+
silenceRunnable = () -> {
|
|
526
|
+
if (!isDestroyed && isListening) {
|
|
527
|
+
// Silencio prolongado detectado
|
|
528
|
+
// Si nunca hubo habla, mantener estado silencioso
|
|
529
|
+
if (!hasRealSpeech) {
|
|
530
|
+
notifyListeners(
|
|
531
|
+
"partialResult",
|
|
532
|
+
new JSObject()
|
|
533
|
+
.put("text", "")
|
|
534
|
+
.put("isFinal", false)
|
|
535
|
+
.put("hasSpeech", false)
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
// Reiniciar el reconocimiento
|
|
539
|
+
restartListeningQuietly();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
253
542
|
|
|
254
|
-
|
|
543
|
+
getActivity()
|
|
544
|
+
.getWindow()
|
|
545
|
+
.getDecorView()
|
|
546
|
+
.postDelayed(silenceRunnable, SILENCE_TIMEOUT_MS);
|
|
547
|
+
}
|
|
255
548
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
549
|
+
private void cancelSilenceTimeout() {
|
|
550
|
+
if (silenceRunnable != null) {
|
|
551
|
+
getActivity().getWindow().getDecorView().removeCallbacks(silenceRunnable);
|
|
552
|
+
silenceRunnable = null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
259
555
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
556
|
+
@PluginMethod
|
|
557
|
+
public void restartListening(PluginCall call) {
|
|
558
|
+
String lang = call.getString("lang", "es-MX");
|
|
559
|
+
|
|
560
|
+
// Marcar para limpiar en el próximo resultado
|
|
561
|
+
shouldClearOnNextResult = true;
|
|
562
|
+
|
|
563
|
+
// Limpiar inmediatamente el texto acumulado
|
|
564
|
+
accumulatedText.setLength(0);
|
|
565
|
+
lastSentPartial = "";
|
|
566
|
+
hasRealSpeech = false;
|
|
567
|
+
pendingPartialText = "";
|
|
568
|
+
|
|
569
|
+
// Notificar limpieza (sin habla)
|
|
570
|
+
notifyListeners(
|
|
571
|
+
"partialResult",
|
|
572
|
+
new JSObject()
|
|
573
|
+
.put("text", "")
|
|
574
|
+
.put("isFinal", false)
|
|
575
|
+
.put("hasSpeech", false)
|
|
576
|
+
);
|
|
268
577
|
|
|
269
|
-
|
|
578
|
+
// Limpiar el estado actual
|
|
579
|
+
isListening = false;
|
|
270
580
|
|
|
271
|
-
|
|
581
|
+
if (delayedStartRunnable != null) {
|
|
582
|
+
getActivity().runOnUiThread(() -> {
|
|
583
|
+
getActivity()
|
|
584
|
+
.getWindow()
|
|
585
|
+
.getDecorView()
|
|
586
|
+
.removeCallbacks(delayedStartRunnable);
|
|
587
|
+
});
|
|
588
|
+
delayedStartRunnable = null;
|
|
589
|
+
}
|
|
272
590
|
|
|
273
|
-
|
|
274
|
-
|
|
591
|
+
// Parar y reiniciar
|
|
592
|
+
stopSpeech();
|
|
275
593
|
|
|
594
|
+
getActivity().runOnUiThread(() -> {
|
|
595
|
+
getActivity()
|
|
596
|
+
.getWindow()
|
|
597
|
+
.getDecorView()
|
|
598
|
+
.postDelayed(() -> {
|
|
599
|
+
isDestroyed = false;
|
|
600
|
+
isListening = true;
|
|
601
|
+
speechIntent = null;
|
|
602
|
+
|
|
603
|
+
// Comenzar de nuevo
|
|
604
|
+
startListeningInternal(call, lang);
|
|
605
|
+
}, 300);
|
|
606
|
+
});
|
|
276
607
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
speechIntent.putExtra(
|
|
280
|
-
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
|
281
|
-
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
|
|
282
|
-
speechIntent.putExtra(
|
|
283
|
-
RecognizerIntent.EXTRA_LANGUAGE, lang);
|
|
284
|
-
speechIntent.putExtra(
|
|
285
|
-
RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
|
|
608
|
+
call.resolve();
|
|
609
|
+
}
|
|
286
610
|
|
|
611
|
+
@PluginMethod
|
|
612
|
+
public void stopListening(PluginCall call) {
|
|
613
|
+
isListening = false;
|
|
614
|
+
isDestroyed = true;
|
|
287
615
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
616
|
+
if (delayedStartRunnable != null) {
|
|
617
|
+
getActivity().runOnUiThread(() -> {
|
|
618
|
+
getActivity()
|
|
619
|
+
.getWindow()
|
|
620
|
+
.getDecorView()
|
|
621
|
+
.removeCallbacks(delayedStartRunnable);
|
|
622
|
+
});
|
|
623
|
+
delayedStartRunnable = null;
|
|
294
624
|
}
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
625
|
|
|
626
|
+
cancelSilenceTimeout();
|
|
627
|
+
currentCall = null;
|
|
628
|
+
stopSpeech();
|
|
299
629
|
|
|
630
|
+
// Enviar resultado final con todo lo acumulado
|
|
631
|
+
if (accumulatedText.length() > 0) {
|
|
632
|
+
notifyListeners(
|
|
633
|
+
"partialResult",
|
|
634
|
+
new JSObject()
|
|
635
|
+
.put("text", accumulatedText.toString())
|
|
636
|
+
.put("isFinal", true)
|
|
637
|
+
.put("hasSpeech", true)
|
|
638
|
+
);
|
|
639
|
+
}
|
|
300
640
|
|
|
641
|
+
call.resolve();
|
|
642
|
+
}
|
|
301
643
|
|
|
302
644
|
private void stopSpeech() {
|
|
303
645
|
try {
|
|
304
646
|
lastStopTime = System.currentTimeMillis();
|
|
305
|
-
|
|
306
|
-
if (silenceRunnable != null) {
|
|
307
|
-
getActivity().getWindow().getDecorView().removeCallbacks(silenceRunnable);
|
|
308
|
-
silenceRunnable = null;
|
|
309
|
-
}
|
|
647
|
+
cancelSilenceTimeout();
|
|
310
648
|
|
|
311
649
|
if (speechRecognizer != null) {
|
|
312
650
|
speechRecognizer.cancel();
|
|
@@ -329,79 +667,11 @@ private void startListeningInternal(PluginCall call, String lang) {
|
|
|
329
667
|
} catch (Exception ignored) {}
|
|
330
668
|
}
|
|
331
669
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
@PluginMethod
|
|
335
|
-
public void restartListening(PluginCall call) {
|
|
336
|
-
String lang = call.getString("lang", "es-MX");
|
|
337
|
-
|
|
338
|
-
isListening = false;
|
|
339
|
-
isDestroyed = true;
|
|
340
|
-
|
|
341
|
-
if (delayedStartRunnable != null) {
|
|
342
|
-
getActivity().runOnUiThread(() -> {
|
|
343
|
-
getActivity()
|
|
344
|
-
.getWindow()
|
|
345
|
-
.getDecorView()
|
|
346
|
-
.removeCallbacks(delayedStartRunnable);
|
|
347
|
-
});
|
|
348
|
-
delayedStartRunnable = null;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
stopSpeech();
|
|
352
|
-
ignoreResultCount = 3;
|
|
353
|
-
getActivity().runOnUiThread(() -> {
|
|
354
|
-
getActivity()
|
|
355
|
-
.getWindow()
|
|
356
|
-
.getDecorView()
|
|
357
|
-
.postDelayed(() -> {
|
|
358
|
-
isDestroyed = false;
|
|
359
|
-
isListening = true;
|
|
360
|
-
speechIntent = null;
|
|
361
|
-
|
|
362
|
-
notifyListeners(
|
|
363
|
-
"partialResult",
|
|
364
|
-
new JSObject()
|
|
365
|
-
.put("text", "")
|
|
366
|
-
.put("isFinal", false)
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
startListeningInternal(call, lang);
|
|
370
|
-
}, 250);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
call.resolve();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
@PluginMethod
|
|
379
|
-
public void stopListening(PluginCall call) {
|
|
380
|
-
isListening = false;
|
|
381
|
-
isDestroyed = true;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (delayedStartRunnable != null) {
|
|
385
|
-
getActivity().runOnUiThread(() -> {
|
|
386
|
-
getActivity()
|
|
387
|
-
.getWindow()
|
|
388
|
-
.getDecorView()
|
|
389
|
-
.removeCallbacks(delayedStartRunnable);
|
|
390
|
-
});
|
|
391
|
-
delayedStartRunnable = null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
currentCall = null;
|
|
395
|
-
stopSpeech();
|
|
396
|
-
call.resolve();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
670
|
@Override
|
|
400
671
|
protected void handleOnDestroy() {
|
|
401
672
|
isDestroyed = true;
|
|
402
673
|
isListening = false;
|
|
403
674
|
|
|
404
|
-
|
|
405
675
|
if (delayedStartRunnable != null) {
|
|
406
676
|
getActivity().runOnUiThread(() -> {
|
|
407
677
|
getActivity()
|
|
@@ -412,40 +682,17 @@ public void restartListening(PluginCall call) {
|
|
|
412
682
|
delayedStartRunnable = null;
|
|
413
683
|
}
|
|
414
684
|
|
|
685
|
+
cancelSilenceTimeout();
|
|
415
686
|
stopSpeech();
|
|
416
687
|
}
|
|
417
688
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: return "Speech timeout";
|
|
426
|
-
default: return "Unknown error";
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
private void scheduleSilenceRestart() {
|
|
431
|
-
if (silenceRunnable != null) {
|
|
432
|
-
getActivity().getWindow().getDecorView().removeCallbacks(silenceRunnable);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
silenceRunnable = () -> {
|
|
436
|
-
if (!isDestroyed && isListening && speechRecognizer != null && speechIntent != null) {
|
|
437
|
-
try {
|
|
438
|
-
speechRecognizer.startListening(speechIntent);
|
|
439
|
-
} catch (Exception ignored) {}
|
|
689
|
+
private String mapError(int error) {
|
|
690
|
+
switch (error) {
|
|
691
|
+
case SpeechRecognizer.ERROR_AUDIO: return "Audio error";
|
|
692
|
+
case SpeechRecognizer.ERROR_NETWORK: return "Network error";
|
|
693
|
+
case SpeechRecognizer.ERROR_NO_MATCH: return "No speech recognized";
|
|
694
|
+
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: return "Speech timeout";
|
|
695
|
+
default: return "Unknown error";
|
|
440
696
|
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
getActivity()
|
|
444
|
-
.getWindow()
|
|
445
|
-
.getDecorView()
|
|
446
|
-
.postDelayed(silenceRunnable, SILENCE_TIMEOUT_MS);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
697
|
+
}
|
|
451
698
|
}
|