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