@myscheme/voice-navigation-sdk 0.1.5 → 0.1.7
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/README.md +13 -5
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +184 -44
- package/dist/microphone-handler.d.ts +8 -0
- package/dist/microphone-handler.d.ts.map +1 -1
- package/dist/microphone-handler.js +98 -24
- package/dist/navigation-controller.d.ts +4 -0
- package/dist/navigation-controller.d.ts.map +1 -1
- package/dist/navigation-controller.js +201 -7
- package/dist/server/azure-speech-handler.d.ts +13 -0
- package/dist/server/azure-speech-handler.d.ts.map +1 -0
- package/dist/server/azure-speech-handler.js +79 -0
- package/dist/server/bedrock-embedding-handler.d.ts +16 -0
- package/dist/server/bedrock-embedding-handler.d.ts.map +1 -0
- package/dist/server/bedrock-embedding-handler.js +183 -0
- package/dist/server/bedrock-handler.d.ts +15 -0
- package/dist/server/bedrock-handler.d.ts.map +1 -0
- package/dist/server/bedrock-handler.js +171 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -0
- package/dist/server/opensearch-env-handler.d.ts +22 -0
- package/dist/server/opensearch-env-handler.d.ts.map +1 -0
- package/dist/server/opensearch-env-handler.js +221 -0
- package/dist/services/azure-speech.d.ts +10 -2
- package/dist/services/azure-speech.d.ts.map +1 -1
- package/dist/services/azure-speech.js +66 -3
- package/dist/services/bedrock.d.ts +16 -4
- package/dist/services/bedrock.d.ts.map +1 -1
- package/dist/services/bedrock.js +293 -17
- package/dist/services/vector-search.d.ts.map +1 -1
- package/dist/services/vector-search.js +15 -2
- package/dist/services/voice-feedback.d.ts +46 -0
- package/dist/services/voice-feedback.d.ts.map +1 -0
- package/dist/services/voice-feedback.js +332 -0
- package/dist/types.d.ts +17 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -10,6 +10,10 @@ export class MicrophoneHandler {
|
|
|
10
10
|
this.hasPermission = false;
|
|
11
11
|
this.audioContext = null;
|
|
12
12
|
this.audioContextUnlocked = false;
|
|
13
|
+
this.activeCallbacks = {};
|
|
14
|
+
this.lastRecognitionTime = 0;
|
|
15
|
+
this.recognitionHealthCheckInterval = null;
|
|
16
|
+
this.isRecovering = false;
|
|
13
17
|
this.azureSpeechService = config.azureSpeechService;
|
|
14
18
|
this.bedrockService = config.bedrockService;
|
|
15
19
|
this.language = config.language || "en-IN";
|
|
@@ -156,13 +160,69 @@ export class MicrophoneHandler {
|
|
|
156
160
|
}
|
|
157
161
|
scheduleSilenceCheck(callbacks = {}) {
|
|
158
162
|
this.clearSilenceTimer();
|
|
163
|
+
const callbacksToUse = Object.keys(callbacks).length > 0 ? callbacks : this.activeCallbacks;
|
|
159
164
|
this.silenceTimerId = setTimeout(() => {
|
|
160
165
|
console.log("Silence detected - triggering callback");
|
|
161
|
-
if (
|
|
162
|
-
|
|
166
|
+
if (callbacksToUse.onSilence) {
|
|
167
|
+
callbacksToUse.onSilence();
|
|
163
168
|
}
|
|
164
169
|
}, this.silenceTimeout);
|
|
165
170
|
}
|
|
171
|
+
rescheduleSilenceCheck() {
|
|
172
|
+
if (!this.isRecording) {
|
|
173
|
+
console.warn("[MicrophoneHandler] Cannot reschedule - not recording");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!this.recognizer) {
|
|
177
|
+
console.error("[MicrophoneHandler] Cannot reschedule - no recognizer");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log("[MicrophoneHandler] Rescheduling silence check");
|
|
181
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
182
|
+
}
|
|
183
|
+
startHealthCheck() {
|
|
184
|
+
this.stopHealthCheck();
|
|
185
|
+
this.recognitionHealthCheckInterval = setInterval(() => {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const timeSinceLastRecognition = now - this.lastRecognitionTime;
|
|
188
|
+
if (this.isRecording && timeSinceLastRecognition > 60000) {
|
|
189
|
+
console.warn("[MicrophoneHandler] Recognizer appears stalled. Last recognition:", timeSinceLastRecognition / 1000, "seconds ago");
|
|
190
|
+
this.recoverFromStalledRecognizer();
|
|
191
|
+
}
|
|
192
|
+
}, 30000);
|
|
193
|
+
}
|
|
194
|
+
stopHealthCheck() {
|
|
195
|
+
if (this.recognitionHealthCheckInterval) {
|
|
196
|
+
clearInterval(this.recognitionHealthCheckInterval);
|
|
197
|
+
this.recognitionHealthCheckInterval = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async recoverFromStalledRecognizer() {
|
|
201
|
+
if (this.isRecovering) {
|
|
202
|
+
console.log("[MicrophoneHandler] Recovery already in progress, skipping");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.isRecovering = true;
|
|
206
|
+
console.log("[MicrophoneHandler] Attempting to recover from stalled recognizer");
|
|
207
|
+
this.disposeRecognizer();
|
|
208
|
+
this.isRecording = false;
|
|
209
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
210
|
+
try {
|
|
211
|
+
const restarted = await this.startRecording(this.activeCallbacks);
|
|
212
|
+
if (restarted) {
|
|
213
|
+
console.log("[MicrophoneHandler] Successfully recovered recognizer");
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
console.error("[MicrophoneHandler] Failed to recover recognizer");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.error("[MicrophoneHandler] Error during recovery:", error);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this.isRecovering = false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
166
226
|
async sendToBedrockAgent(text) {
|
|
167
227
|
if (!text || !text.trim()) {
|
|
168
228
|
return { action: "unknown" };
|
|
@@ -177,16 +237,17 @@ export class MicrophoneHandler {
|
|
|
177
237
|
}
|
|
178
238
|
}
|
|
179
239
|
disposeRecognizer() {
|
|
240
|
+
this.stopHealthCheck();
|
|
180
241
|
if (this.recognizer) {
|
|
181
242
|
try {
|
|
182
243
|
this.recognizer.stopContinuousRecognitionAsync(() => {
|
|
183
|
-
console.log("Recognizer stopped");
|
|
244
|
+
console.log("[MicrophoneHandler] Recognizer stopped");
|
|
184
245
|
if (this.recognizer) {
|
|
185
246
|
this.recognizer.close();
|
|
186
247
|
this.recognizer = null;
|
|
187
248
|
}
|
|
188
249
|
}, (error) => {
|
|
189
|
-
console.error("Error stopping recognizer:", error);
|
|
250
|
+
console.error("[MicrophoneHandler] Error stopping recognizer:", error);
|
|
190
251
|
if (this.recognizer) {
|
|
191
252
|
this.recognizer.close();
|
|
192
253
|
this.recognizer = null;
|
|
@@ -194,7 +255,7 @@ export class MicrophoneHandler {
|
|
|
194
255
|
});
|
|
195
256
|
}
|
|
196
257
|
catch (error) {
|
|
197
|
-
console.error("Error disposing recognizer:", error);
|
|
258
|
+
console.error("[MicrophoneHandler] Error disposing recognizer:", error);
|
|
198
259
|
if (this.recognizer) {
|
|
199
260
|
this.recognizer.close();
|
|
200
261
|
this.recognizer = null;
|
|
@@ -213,9 +274,14 @@ export class MicrophoneHandler {
|
|
|
213
274
|
}
|
|
214
275
|
async startRecording(callbacks = {}) {
|
|
215
276
|
if (this.isRecording) {
|
|
216
|
-
console.warn("Already recording");
|
|
277
|
+
console.warn("[MicrophoneHandler] Already recording");
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (this.isRecovering) {
|
|
281
|
+
console.warn("[MicrophoneHandler] Recovery in progress, cannot start");
|
|
217
282
|
return false;
|
|
218
283
|
}
|
|
284
|
+
this.activeCallbacks = callbacks;
|
|
219
285
|
if (!this.hasPermission) {
|
|
220
286
|
const granted = await this.requestMicrophonePermission();
|
|
221
287
|
if (!granted) {
|
|
@@ -253,57 +319,65 @@ export class MicrophoneHandler {
|
|
|
253
319
|
return false;
|
|
254
320
|
}
|
|
255
321
|
this.recognizer.recognizing = (_s, e) => {
|
|
256
|
-
|
|
257
|
-
|
|
322
|
+
this.lastRecognitionTime = Date.now();
|
|
323
|
+
if (e.result.text && this.activeCallbacks.onPartial) {
|
|
324
|
+
this.activeCallbacks.onPartial(e.result.text);
|
|
258
325
|
}
|
|
259
326
|
};
|
|
260
327
|
this.recognizer.recognized = (_s, e) => {
|
|
328
|
+
this.lastRecognitionTime = Date.now();
|
|
261
329
|
if (e.result.reason === sdk.ResultReason.RecognizedSpeech) {
|
|
262
330
|
const text = e.result.text;
|
|
263
331
|
if (text) {
|
|
332
|
+
console.log("[MicrophoneHandler] Recognized speech:", text);
|
|
264
333
|
this.transcriptionBuffer.push(text);
|
|
265
|
-
if (
|
|
266
|
-
|
|
334
|
+
if (this.activeCallbacks.onSegment) {
|
|
335
|
+
this.activeCallbacks.onSegment(text, {
|
|
267
336
|
duration: e.result.duration,
|
|
268
337
|
offset: e.result.offset,
|
|
269
338
|
});
|
|
270
339
|
}
|
|
271
|
-
this.scheduleSilenceCheck(
|
|
340
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
272
341
|
}
|
|
273
342
|
}
|
|
274
343
|
else if (e.result.reason === sdk.ResultReason.NoMatch) {
|
|
275
|
-
console.log("No speech recognized");
|
|
344
|
+
console.log("[MicrophoneHandler] No speech recognized (NoMatch)");
|
|
276
345
|
}
|
|
277
346
|
};
|
|
278
347
|
this.recognizer.canceled = (_s, e) => {
|
|
279
|
-
console.error("Recognition canceled:", e.reason, e.errorDetails);
|
|
348
|
+
console.error("[MicrophoneHandler] Recognition canceled:", e.reason, e.errorDetails);
|
|
280
349
|
this.isRecording = false;
|
|
281
|
-
|
|
282
|
-
|
|
350
|
+
this.stopHealthCheck();
|
|
351
|
+
if (this.activeCallbacks.onError) {
|
|
352
|
+
this.activeCallbacks.onError(new Error(`Recognition canceled: ${e.reason} - ${e.errorDetails}`));
|
|
283
353
|
}
|
|
284
354
|
};
|
|
285
355
|
this.recognizer.sessionStopped = (_s, _e) => {
|
|
286
|
-
console.log("Session stopped");
|
|
356
|
+
console.log("[MicrophoneHandler] Session stopped");
|
|
287
357
|
this.isRecording = false;
|
|
358
|
+
this.stopHealthCheck();
|
|
288
359
|
};
|
|
289
360
|
this.recognizer.startContinuousRecognitionAsync(() => {
|
|
290
|
-
console.log("Continuous recognition started");
|
|
361
|
+
console.log("[MicrophoneHandler] Continuous recognition started");
|
|
291
362
|
this.isRecording = true;
|
|
292
363
|
this.audioContextUnlocked = true;
|
|
293
|
-
this.
|
|
364
|
+
this.lastRecognitionTime = Date.now();
|
|
365
|
+
this.startHealthCheck();
|
|
366
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
294
367
|
}, (error) => {
|
|
295
|
-
console.error("Failed to start recognition:", error);
|
|
368
|
+
console.error("[MicrophoneHandler] Failed to start recognition:", error);
|
|
296
369
|
this.isRecording = false;
|
|
297
|
-
if (
|
|
298
|
-
|
|
370
|
+
if (this.activeCallbacks.onError) {
|
|
371
|
+
this.activeCallbacks.onError(new Error(`Failed to start recognition: ${error}`));
|
|
299
372
|
}
|
|
300
373
|
});
|
|
301
374
|
return true;
|
|
302
375
|
}
|
|
303
376
|
async stopRecording() {
|
|
304
377
|
this.clearSilenceTimer();
|
|
378
|
+
this.stopHealthCheck();
|
|
305
379
|
if (!this.isRecording || !this.recognizer) {
|
|
306
|
-
console.warn("Not currently recording");
|
|
380
|
+
console.warn("[MicrophoneHandler] Not currently recording");
|
|
307
381
|
return this.getAggregatedText();
|
|
308
382
|
}
|
|
309
383
|
return new Promise((resolve) => {
|
|
@@ -312,13 +386,13 @@ export class MicrophoneHandler {
|
|
|
312
386
|
return;
|
|
313
387
|
}
|
|
314
388
|
this.recognizer.stopContinuousRecognitionAsync(() => {
|
|
315
|
-
console.log("Recognition stopped successfully");
|
|
389
|
+
console.log("[MicrophoneHandler] Recognition stopped successfully");
|
|
316
390
|
this.isRecording = false;
|
|
317
391
|
const finalText = this.getAggregatedText();
|
|
318
392
|
this.disposeRecognizer();
|
|
319
393
|
resolve(finalText);
|
|
320
394
|
}, (error) => {
|
|
321
|
-
console.error("Error stopping recognition:", error);
|
|
395
|
+
console.error("[MicrophoneHandler] Error stopping recognition:", error);
|
|
322
396
|
this.isRecording = false;
|
|
323
397
|
const finalText = this.getAggregatedText();
|
|
324
398
|
this.disposeRecognizer();
|
|
@@ -4,6 +4,7 @@ export declare class VoiceNavigationController {
|
|
|
4
4
|
private microphoneHandler;
|
|
5
5
|
private azureSpeechService;
|
|
6
6
|
private bedrockService;
|
|
7
|
+
private voiceFeedbackService;
|
|
7
8
|
private pageRegistry;
|
|
8
9
|
private ui;
|
|
9
10
|
private lastTranscript;
|
|
@@ -37,6 +38,9 @@ export declare class VoiceNavigationController {
|
|
|
37
38
|
private processTranscript;
|
|
38
39
|
private handleAgentResult;
|
|
39
40
|
setLanguage(language: string): void;
|
|
41
|
+
setVoiceFeedback(enabled: boolean): void;
|
|
42
|
+
isVoiceFeedbackEnabled(): boolean;
|
|
43
|
+
private provideActionFeedback;
|
|
40
44
|
private executeVectorSearchAction;
|
|
41
45
|
private normalizeSearchQuery;
|
|
42
46
|
private normalizeIndianSchemeName;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-controller.d.ts","sourceRoot":"","sources":["../src/navigation-controller.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,
|
|
1
|
+
{"version":3,"file":"navigation-controller.d.ts","sourceRoot":"","sources":["../src/navigation-controller.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAMjB,MAAM,YAAY,CAAC;AA4DpB,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,qBAAqB,CAAkB;IAC/C,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,+BAA+B,CAAkB;IACzD,OAAO,CAAC,wBAAwB,CAA6B;IAC7D,OAAO,CAAC,mBAAmB,CAAoC;IAC/D,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,aAAa,CAAgB;gBAEzB,MAAM,EAAE,gBAAgB;YAqJtB,iBAAiB;IAiD/B,OAAO,CAAC,eAAe;IAoBhB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAepC,oBAAoB,CAAC,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;YAOtD,eAAe;IAmE7B,OAAO,CAAC,kBAAkB;YASZ,eAAe;IAwBhB,KAAK,CAChB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,GACzC,OAAO,CAAC,IAAI,CAAC;IAmFH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiClC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;YAWP,aAAa;YAOb,wBAAwB;IA2CtC,OAAO,CAAC,WAAW;YAoBL,iBAAiB;YAwBjB,iBAAiB;IAoRxB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQnC,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAOxC,sBAAsB,IAAI,OAAO;YAO1B,qBAAqB;YAgFrB,yBAAyB;IAmMvC,OAAO,CAAC,oBAAoB;IAqD5B,OAAO,CAAC,yBAAyB;IAoCjC,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,4BAA4B;IAiB7B,OAAO,IAAI,IAAI;IAgBtB,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,uBAAuB;IAkD/B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,wBAAwB;CAmBjC"}
|
|
@@ -2,6 +2,7 @@ import { MicrophoneHandler } from "./microphone-handler.js";
|
|
|
2
2
|
import { AzureSpeechService } from "./services/azure-speech.js";
|
|
3
3
|
import { BedrockService } from "./services/bedrock.js";
|
|
4
4
|
import { PageRegistry } from "./services/page-registry.js";
|
|
5
|
+
import { VoiceFeedbackService } from "./services/voice-feedback.js";
|
|
5
6
|
import { loadNavigationPages } from "./services/xml-parser.js";
|
|
6
7
|
import { VectorSearchService, } from "./services/vector-search.js";
|
|
7
8
|
import { performAgentAction, formatActionLabel, extractAgentAction, setPageRegistry, } from "./actions.js";
|
|
@@ -43,19 +44,47 @@ export class VoiceNavigationController {
|
|
|
43
44
|
opensearch: openSearchConfig,
|
|
44
45
|
};
|
|
45
46
|
console.log("[Voice Navigation Controller] Constructor called with UI config:", this.config.ui);
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
const hasAzureEndpoint = !!this.config.azure?.tokenEndpoint;
|
|
48
|
+
const hasAzureCredentials = !!(this.config.azure?.subscriptionKey && this.config.azure?.region);
|
|
49
|
+
if (!this.config.azure) {
|
|
50
|
+
this.config.azure = { tokenEndpoint: "/api/speech/token" };
|
|
51
|
+
console.log("[VoiceNavigation] Auto-configured Azure endpoint: /api/speech/token");
|
|
52
|
+
}
|
|
53
|
+
else if (!hasAzureEndpoint && !hasAzureCredentials) {
|
|
54
|
+
this.config.azure.tokenEndpoint = "/api/speech/token";
|
|
55
|
+
console.log("[VoiceNavigation] Auto-configured Azure endpoint: /api/speech/token");
|
|
56
|
+
}
|
|
57
|
+
const hasAwsEndpoint = !!this.config.aws?.bedrockEndpoint;
|
|
58
|
+
const hasAwsCredentials = !!(this.config.aws?.accessKeyId &&
|
|
59
|
+
this.config.aws?.secretAccessKey &&
|
|
60
|
+
this.config.aws?.modelId);
|
|
61
|
+
if (!this.config.aws) {
|
|
62
|
+
this.config.aws = {
|
|
63
|
+
bedrockEndpoint: "/api/speech/bedrock",
|
|
64
|
+
embeddingEndpoint: "/api/speech/bedrock-embedding",
|
|
65
|
+
};
|
|
66
|
+
console.log("[VoiceNavigation] Auto-configured AWS endpoints: /api/speech/bedrock, /api/speech/bedrock-embedding");
|
|
67
|
+
}
|
|
68
|
+
else if (!hasAwsEndpoint && !hasAwsCredentials) {
|
|
69
|
+
this.config.aws.bedrockEndpoint = "/api/speech/bedrock";
|
|
70
|
+
this.config.aws.embeddingEndpoint = "/api/speech/bedrock-embedding";
|
|
71
|
+
console.log("[VoiceNavigation] Auto-configured AWS endpoints: /api/speech/bedrock, /api/speech/bedrock-embedding");
|
|
48
72
|
}
|
|
49
|
-
if (
|
|
50
|
-
!
|
|
51
|
-
!
|
|
52
|
-
|
|
73
|
+
if (openSearchConfig &&
|
|
74
|
+
!openSearchConfig.searchEndpoint &&
|
|
75
|
+
!openSearchConfig.node) {
|
|
76
|
+
openSearchConfig.searchEndpoint = "/api/opensearch/search";
|
|
77
|
+
openSearchConfig.suggestEndpoint = "/api/opensearch/suggest";
|
|
78
|
+
console.log("[VoiceNavigation] Auto-configured OpenSearch endpoints: /api/opensearch/search, /api/opensearch/suggest");
|
|
53
79
|
}
|
|
54
80
|
this.azureSpeechService = new AzureSpeechService({
|
|
81
|
+
tokenEndpoint: this.config.azure.tokenEndpoint,
|
|
55
82
|
subscriptionKey: this.config.azure.subscriptionKey,
|
|
56
83
|
region: this.config.azure.region,
|
|
57
84
|
});
|
|
58
85
|
this.bedrockService = new BedrockService({
|
|
86
|
+
bedrockEndpoint: this.config.aws.bedrockEndpoint,
|
|
87
|
+
embeddingEndpoint: this.config.aws.embeddingEndpoint,
|
|
59
88
|
region: this.config.aws.region || "ap-south-1",
|
|
60
89
|
accessKeyId: this.config.aws.accessKeyId,
|
|
61
90
|
secretAccessKey: this.config.aws.secretAccessKey,
|
|
@@ -71,6 +100,15 @@ export class VoiceNavigationController {
|
|
|
71
100
|
bedrockService: this.bedrockService,
|
|
72
101
|
language: this.config.language || "en-IN",
|
|
73
102
|
});
|
|
103
|
+
const voiceFeedbackEnabled = this.config.voiceFeedback?.enabled !== false;
|
|
104
|
+
console.log("[VoiceNavigation] Initializing voice feedback service - enabled:", voiceFeedbackEnabled);
|
|
105
|
+
this.voiceFeedbackService = new VoiceFeedbackService({
|
|
106
|
+
enabled: voiceFeedbackEnabled,
|
|
107
|
+
language: this.config.language || "en-IN",
|
|
108
|
+
azureSpeechService: this.azureSpeechService,
|
|
109
|
+
bedrockService: this.bedrockService,
|
|
110
|
+
});
|
|
111
|
+
console.log("[VoiceNavigation] Voice feedback service initialized successfully");
|
|
74
112
|
if (openSearchConfig) {
|
|
75
113
|
try {
|
|
76
114
|
this.vectorSearchService = new VectorSearchService(openSearchConfig);
|
|
@@ -350,6 +388,7 @@ export class VoiceNavigationController {
|
|
|
350
388
|
}
|
|
351
389
|
updateTranscript(this.ui, "");
|
|
352
390
|
this.isProcessing = false;
|
|
391
|
+
this.microphoneHandler.rescheduleSilenceCheck();
|
|
353
392
|
}
|
|
354
393
|
}
|
|
355
394
|
handleError(error) {
|
|
@@ -410,6 +449,30 @@ export class VoiceNavigationController {
|
|
|
410
449
|
query,
|
|
411
450
|
originalText,
|
|
412
451
|
});
|
|
452
|
+
if (actionResult.performed && actionResult.info?.pendingNavigation) {
|
|
453
|
+
console.log("[VoiceNavigation] Search result with navigation - waiting for feedback:", action);
|
|
454
|
+
try {
|
|
455
|
+
await this.provideActionFeedback(action, actionResult, originalText);
|
|
456
|
+
console.log("[VoiceNavigation] Search feedback complete, now navigating to:", actionResult.info.navigationUrl);
|
|
457
|
+
if (actionResult.info.navigationUrl) {
|
|
458
|
+
window.location.href = actionResult.info.navigationUrl;
|
|
459
|
+
actionResult.info.navigated = true;
|
|
460
|
+
actionResult.info.pendingNavigation = false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
console.error("[VoiceNavigation] Voice feedback error (search):", err);
|
|
465
|
+
if (actionResult.info.navigationUrl) {
|
|
466
|
+
window.location.href = actionResult.info.navigationUrl;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (actionResult.performed) {
|
|
471
|
+
console.log("[VoiceNavigation] Search result without navigation - non-blocking feedback");
|
|
472
|
+
void this.provideActionFeedback(action, actionResult, originalText).catch((err) => {
|
|
473
|
+
console.error("[VoiceNavigation] Voice feedback error (search):", err);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
413
476
|
if (typeof this.config.actionHandlers?.[action] === "function") {
|
|
414
477
|
try {
|
|
415
478
|
this.config.actionHandlers[action]({
|
|
@@ -441,7 +504,80 @@ export class VoiceNavigationController {
|
|
|
441
504
|
handler: this,
|
|
442
505
|
onStop: () => this.stop(),
|
|
443
506
|
transcript: originalText,
|
|
507
|
+
parameters: {
|
|
508
|
+
text: actionData.text || query,
|
|
509
|
+
query: query,
|
|
510
|
+
},
|
|
444
511
|
});
|
|
512
|
+
console.log("[VoiceNavigation] Action performed:", action, "result:", actionResult.performed);
|
|
513
|
+
const isCriticalAction = action.startsWith("navigate_") ||
|
|
514
|
+
action === "focus_next" ||
|
|
515
|
+
action === "tab_next" ||
|
|
516
|
+
action === "focus_previous" ||
|
|
517
|
+
action === "tab_back" ||
|
|
518
|
+
action === "go_back" ||
|
|
519
|
+
action === "go_forward" ||
|
|
520
|
+
action === "reload_page";
|
|
521
|
+
if (actionResult.performed && isCriticalAction) {
|
|
522
|
+
console.log("[VoiceNavigation] Critical action - waiting for voice feedback to complete:", action);
|
|
523
|
+
try {
|
|
524
|
+
await this.provideActionFeedback(action, actionResult, originalText);
|
|
525
|
+
console.log("[VoiceNavigation] Voice feedback completed for critical action:", action);
|
|
526
|
+
if (actionResult.info?.pendingNavigation) {
|
|
527
|
+
if (actionResult.info.navigationUrl) {
|
|
528
|
+
console.log("[VoiceNavigation] Feedback complete, now navigating to:", actionResult.info.navigationUrl);
|
|
529
|
+
window.location.href = actionResult.info.navigationUrl;
|
|
530
|
+
actionResult.info.navigated = true;
|
|
531
|
+
actionResult.info.pendingNavigation = false;
|
|
532
|
+
}
|
|
533
|
+
else if (actionResult.info.navigationType === "back") {
|
|
534
|
+
console.log("[VoiceNavigation] Feedback complete, navigating back");
|
|
535
|
+
window.history.back();
|
|
536
|
+
actionResult.info.navigated = true;
|
|
537
|
+
actionResult.info.pendingNavigation = false;
|
|
538
|
+
}
|
|
539
|
+
else if (actionResult.info.navigationType === "forward") {
|
|
540
|
+
console.log("[VoiceNavigation] Feedback complete, navigating forward");
|
|
541
|
+
window.history.forward();
|
|
542
|
+
actionResult.info.navigated = true;
|
|
543
|
+
actionResult.info.pendingNavigation = false;
|
|
544
|
+
}
|
|
545
|
+
else if (actionResult.info.navigationType === "reload") {
|
|
546
|
+
console.log("[VoiceNavigation] Feedback complete, reloading page");
|
|
547
|
+
window.location.reload();
|
|
548
|
+
actionResult.info.reloaded = true;
|
|
549
|
+
actionResult.info.pendingNavigation = false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (err) {
|
|
554
|
+
console.error("[VoiceNavigation] Voice feedback error (critical action):", err);
|
|
555
|
+
if (actionResult.info?.pendingNavigation) {
|
|
556
|
+
if (actionResult.info.navigationUrl) {
|
|
557
|
+
console.log("[VoiceNavigation] Feedback failed, but proceeding with navigation:", actionResult.info.navigationUrl);
|
|
558
|
+
window.location.href = actionResult.info.navigationUrl;
|
|
559
|
+
}
|
|
560
|
+
else if (actionResult.info.navigationType === "back") {
|
|
561
|
+
window.history.back();
|
|
562
|
+
}
|
|
563
|
+
else if (actionResult.info.navigationType === "forward") {
|
|
564
|
+
window.history.forward();
|
|
565
|
+
}
|
|
566
|
+
else if (actionResult.info.navigationType === "reload") {
|
|
567
|
+
window.location.reload();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (actionResult.performed) {
|
|
573
|
+
console.log("[VoiceNavigation] Non-critical action - providing feedback non-blocking:", action);
|
|
574
|
+
void this.provideActionFeedback(action, actionResult, originalText).catch((err) => {
|
|
575
|
+
console.error("[VoiceNavigation] Voice feedback error:", err);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
console.log("[VoiceNavigation] Action not performed, skipping voice feedback");
|
|
580
|
+
}
|
|
445
581
|
if (typeof this.config.actionHandlers?.[action] === "function") {
|
|
446
582
|
try {
|
|
447
583
|
this.config.actionHandlers[action]({
|
|
@@ -470,6 +606,62 @@ export class VoiceNavigationController {
|
|
|
470
606
|
}
|
|
471
607
|
setLanguage(language) {
|
|
472
608
|
this.microphoneHandler.setLanguage(language);
|
|
609
|
+
this.voiceFeedbackService.setLanguage(language);
|
|
610
|
+
}
|
|
611
|
+
setVoiceFeedback(enabled) {
|
|
612
|
+
this.voiceFeedbackService.setEnabled(enabled);
|
|
613
|
+
}
|
|
614
|
+
isVoiceFeedbackEnabled() {
|
|
615
|
+
return this.voiceFeedbackService.isEnabled();
|
|
616
|
+
}
|
|
617
|
+
async provideActionFeedback(action, actionResult, transcript) {
|
|
618
|
+
console.log("[VoiceNavigation] provideActionFeedback called for action:", action);
|
|
619
|
+
if (!this.voiceFeedbackService) {
|
|
620
|
+
console.error("[VoiceNavigation] Voice feedback service not initialized!");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
console.log("[VoiceNavigation] Voice feedback service exists, checking if enabled...");
|
|
624
|
+
console.log("[VoiceNavigation] Voice feedback enabled:", this.voiceFeedbackService.isEnabled());
|
|
625
|
+
try {
|
|
626
|
+
if (action.startsWith("navigate_") && actionResult.info?.pageName) {
|
|
627
|
+
console.log("[VoiceNavigation] Handling navigation feedback");
|
|
628
|
+
await this.voiceFeedbackService.provideFeedback({
|
|
629
|
+
action,
|
|
630
|
+
actionResult,
|
|
631
|
+
transcript,
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if ((action === "focus_next" ||
|
|
636
|
+
action === "tab_next" ||
|
|
637
|
+
action === "focus_previous" ||
|
|
638
|
+
action === "tab_back") &&
|
|
639
|
+
actionResult.performed) {
|
|
640
|
+
console.log("[VoiceNavigation] Handling focus feedback");
|
|
641
|
+
const focusedElement = document.activeElement;
|
|
642
|
+
if (focusedElement && focusedElement !== document.body) {
|
|
643
|
+
await this.voiceFeedbackService.announceFocusChange(focusedElement, actionResult.info.direction === "next" ||
|
|
644
|
+
actionResult.info.direction === "forward"
|
|
645
|
+
? "next"
|
|
646
|
+
: "previous");
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (actionResult.performed) {
|
|
651
|
+
console.log("[VoiceNavigation] Handling general action feedback");
|
|
652
|
+
await this.voiceFeedbackService.provideFeedback({
|
|
653
|
+
action,
|
|
654
|
+
actionResult,
|
|
655
|
+
transcript,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
console.log("[VoiceNavigation] Action not performed in feedback check");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
console.error("[VoiceNavigation] Failed to provide voice feedback:", error);
|
|
664
|
+
}
|
|
473
665
|
}
|
|
474
666
|
async executeVectorSearchAction(options) {
|
|
475
667
|
const vectorService = this.vectorSearchService;
|
|
@@ -595,7 +787,9 @@ export class VoiceNavigationController {
|
|
|
595
787
|
});
|
|
596
788
|
this.prepareForNavigation({ target: resolvedUrl });
|
|
597
789
|
baseInfo.message = "Opening the best matching result.";
|
|
598
|
-
|
|
790
|
+
baseInfo.pendingNavigation = true;
|
|
791
|
+
baseInfo.navigationUrl = resolvedUrl;
|
|
792
|
+
baseInfo.navigated = false;
|
|
599
793
|
return {
|
|
600
794
|
performed: true,
|
|
601
795
|
info: baseInfo,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type NextApiRequest = {
|
|
2
|
+
method?: string;
|
|
3
|
+
body?: unknown;
|
|
4
|
+
};
|
|
5
|
+
type NextApiResponse = {
|
|
6
|
+
status: (code: number) => NextApiResponse;
|
|
7
|
+
json: (data: unknown) => void;
|
|
8
|
+
setHeader: (name: string, value: string | string[]) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function azureSpeechTokenHandler(req: NextApiRequest, res: NextApiResponse): Promise<void>;
|
|
11
|
+
export declare function POST(_request: Request): Promise<Response>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=azure-speech-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure-speech-handler.d.ts","sourceRoot":"","sources":["../../src/server/azure-speech-handler.ts"],"names":[],"mappings":"AAwBA,KAAK,cAAc,GAAG;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AA6EF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAMD,wBAAsB,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmB/D"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
async function getAzureSpeechToken() {
|
|
2
|
+
const key = process.env.AZURE_SPEECH_KEY;
|
|
3
|
+
const region = process.env.AZURE_SPEECH_REGION;
|
|
4
|
+
if (!key || !region) {
|
|
5
|
+
console.error("[VoiceNavigation] Missing Azure Speech credentials. Set AZURE_SPEECH_KEY and AZURE_SPEECH_REGION in your .env file");
|
|
6
|
+
return {
|
|
7
|
+
success: false,
|
|
8
|
+
statusCode: 500,
|
|
9
|
+
error: {
|
|
10
|
+
error: "Azure Speech configuration is missing",
|
|
11
|
+
hint: "Set AZURE_SPEECH_KEY and AZURE_SPEECH_REGION environment variables",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const url = `https://${region}.api.cognitive.microsoft.com/sts/v1.0/issueToken`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Ocp-Apim-Subscription-Key": key,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Azure API responded with status ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
const token = await response.text();
|
|
27
|
+
const responseData = { token, region };
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
statusCode: 200,
|
|
31
|
+
data: responseData,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error("[VoiceNavigation] Failed to fetch Azure speech token:", error);
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
statusCode: 500,
|
|
39
|
+
error: {
|
|
40
|
+
error: "Failed to get token",
|
|
41
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function azureSpeechTokenHandler(req, res) {
|
|
47
|
+
if (req.method !== "POST") {
|
|
48
|
+
res.setHeader("Allow", ["POST"]);
|
|
49
|
+
res.status(405).json({ error: "Method not allowed" });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const result = await getAzureSpeechToken();
|
|
53
|
+
res.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate");
|
|
54
|
+
if (result.success && result.data) {
|
|
55
|
+
res.status(result.statusCode).json(result.data);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
res.status(result.statusCode).json(result.error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function POST(_request) {
|
|
62
|
+
const result = await getAzureSpeechToken();
|
|
63
|
+
const headers = {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Cache-Control": "no-store, no-cache, max-age=0, must-revalidate",
|
|
66
|
+
};
|
|
67
|
+
if (result.success && result.data) {
|
|
68
|
+
return new Response(JSON.stringify(result.data), {
|
|
69
|
+
status: result.statusCode,
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return new Response(JSON.stringify(result.error), {
|
|
75
|
+
status: result.statusCode,
|
|
76
|
+
headers,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type NextApiRequest = {
|
|
2
|
+
method?: string;
|
|
3
|
+
body?: {
|
|
4
|
+
text?: string | string[];
|
|
5
|
+
inputType?: string;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
type NextApiResponse = {
|
|
9
|
+
status: (code: number) => NextApiResponse;
|
|
10
|
+
json: (data: unknown) => void;
|
|
11
|
+
setHeader: (name: string, value: string | string[]) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function bedrockEmbeddingHandler(req: NextApiRequest, res: NextApiResponse): Promise<void>;
|
|
14
|
+
export declare function BedrockEmbeddingPOST(request: Request): Promise<Response>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=bedrock-embedding-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bedrock-embedding-handler.d.ts","sourceRoot":"","sources":["../../src/server/bedrock-embedding-handler.ts"],"names":[],"mappings":"AA4BA,KAAK,cAAc,GAAG;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE;QACL,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AA0MF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAMD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,QAAQ,CAAC,CAkCnB"}
|