@myscheme/voice-navigation-sdk 0.1.8 → 0.1.10
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 +130 -17
- package/dist/actions.d.ts +1 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +51 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -1
- package/dist/languages.d.ts +17 -0
- package/dist/languages.d.ts.map +1 -0
- package/dist/languages.js +86 -0
- package/dist/microphone-handler.d.ts +9 -2
- package/dist/microphone-handler.d.ts.map +1 -1
- package/dist/microphone-handler.js +104 -6
- package/dist/navigation-controller.d.ts +5 -0
- package/dist/navigation-controller.d.ts.map +1 -1
- package/dist/navigation-controller.js +149 -35
- package/dist/server/azure-speech-handler.d.ts +3 -2
- package/dist/server/azure-speech-handler.d.ts.map +1 -1
- package/dist/server/azure-speech-handler.js +26 -2
- package/dist/server/bedrock-embedding-handler.d.ts +2 -1
- package/dist/server/bedrock-embedding-handler.d.ts.map +1 -1
- package/dist/server/bedrock-embedding-handler.js +73 -19
- package/dist/server/bedrock-handler.d.ts +2 -1
- package/dist/server/bedrock-handler.d.ts.map +1 -1
- package/dist/server/bedrock-handler.js +124 -19
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/opensearch-env-handler.d.ts +2 -1
- package/dist/server/opensearch-env-handler.d.ts.map +1 -1
- package/dist/server/opensearch-env-handler.js +23 -53
- package/dist/server/sarvam-stt-relay.d.ts +8 -0
- package/dist/server/sarvam-stt-relay.d.ts.map +1 -0
- package/dist/server/sarvam-stt-relay.js +111 -0
- package/dist/server/security.d.ts +26 -0
- package/dist/server/security.d.ts.map +1 -0
- package/dist/server/security.js +89 -0
- package/dist/services/bedrock.d.ts.map +1 -1
- package/dist/services/bedrock.js +13 -0
- package/dist/services/sarvam-speech.d.ts +47 -0
- package/dist/services/sarvam-speech.d.ts.map +1 -0
- package/dist/services/sarvam-speech.js +418 -0
- package/dist/services/voice-feedback.d.ts +7 -2
- package/dist/services/voice-feedback.d.ts.map +1 -1
- package/dist/services/voice-feedback.js +45 -5
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"microphone-handler.d.ts","sourceRoot":"","sources":["../src/microphone-handler.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,GAAG,MAAM,wCAAwC,CAAC;AAC9D,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"microphone-handler.d.ts","sourceRoot":"","sources":["../src/microphone-handler.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,GAAG,MAAM,wCAAwC,CAAC;AAC9D,OAAO,KAAK,EACV,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE,UAAU,gBAAgB;IACxB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,mBAAmB,CAAa;IACxC,OAAO,CAAC,8BAA8B,CAEtB;IAChB,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,uBAAuB,CAAkB;gBAErC,MAAM,EAAE,gBAAgB;IAqBpC,uBAAuB,IAAI,OAAO;IAIlC,uBAAuB,IAAI,OAAO;IAO5B,kBAAkB,IAAI,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAoB1D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBzC,kBAAkB,CAAC,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDzD,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC;IA2BrD,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAYnC,wBAAwB,IAAI,IAAI;IAO1B,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAoBnC,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAkBzC,iBAAiB,IAAI,IAAI;IAUzB,iBAAiB,IAAI,MAAM;IAO3B,oBAAoB,CAAC,SAAS,GAAE,mBAAwB,GAAG,IAAI;IAiB/D,sBAAsB,IAAI,IAAI;IAmB9B,OAAO,CAAC,sBAAsB;IA4B9B,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,eAAe;YAUT,4BAA4B;IAwCpC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAiBpE,iBAAiB,IAAI,IAAI;IAiDnB,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAmBjD,cAAc,CAAC,SAAS,GAAE,mBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC;YA8J7D,oBAAoB;IAqD5B,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;WAgEzB,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC;CAYvD"}
|
|
@@ -14,7 +14,16 @@ export class MicrophoneHandler {
|
|
|
14
14
|
this.lastRecognitionTime = 0;
|
|
15
15
|
this.recognitionHealthCheckInterval = null;
|
|
16
16
|
this.isRecovering = false;
|
|
17
|
-
this.
|
|
17
|
+
this.isStoppingIntentionally = false;
|
|
18
|
+
this.speechProvider = config.speechProvider ?? "sarvam";
|
|
19
|
+
this.azureSpeechService = config.azureSpeechService ?? null;
|
|
20
|
+
this.sarvamSpeechService = config.sarvamSpeechService ?? null;
|
|
21
|
+
if (this.speechProvider === "azure" && !this.azureSpeechService) {
|
|
22
|
+
throw new Error("Azure speech service is required for azure provider");
|
|
23
|
+
}
|
|
24
|
+
if (this.speechProvider === "sarvam" && !this.sarvamSpeechService) {
|
|
25
|
+
throw new Error("Sarvam speech service is required for sarvam provider");
|
|
26
|
+
}
|
|
18
27
|
this.bedrockService = config.bedrockService;
|
|
19
28
|
this.language = config.language || "en-IN";
|
|
20
29
|
this.silenceTimeout = config.silenceTimeout || 2000;
|
|
@@ -126,6 +135,9 @@ export class MicrophoneHandler {
|
|
|
126
135
|
this.transcriptionBuffer = [];
|
|
127
136
|
}
|
|
128
137
|
async fetchAzureToken() {
|
|
138
|
+
if (!this.azureSpeechService) {
|
|
139
|
+
throw new Error("Azure speech service is not configured");
|
|
140
|
+
}
|
|
129
141
|
try {
|
|
130
142
|
const response = await this.azureSpeechService.fetchToken();
|
|
131
143
|
this.token = response.token;
|
|
@@ -173,7 +185,7 @@ export class MicrophoneHandler {
|
|
|
173
185
|
console.warn("[MicrophoneHandler] Cannot reschedule - not recording");
|
|
174
186
|
return;
|
|
175
187
|
}
|
|
176
|
-
if (!this.recognizer) {
|
|
188
|
+
if (this.speechProvider === "azure" && !this.recognizer) {
|
|
177
189
|
console.error("[MicrophoneHandler] Cannot reschedule - no recognizer");
|
|
178
190
|
return;
|
|
179
191
|
}
|
|
@@ -219,6 +231,9 @@ export class MicrophoneHandler {
|
|
|
219
231
|
}
|
|
220
232
|
this.isRecovering = true;
|
|
221
233
|
console.log("[MicrophoneHandler] Attempting to recover from stalled recognizer");
|
|
234
|
+
if (this.speechProvider === "sarvam" && this.sarvamSpeechService) {
|
|
235
|
+
await this.sarvamSpeechService.stopListening();
|
|
236
|
+
}
|
|
222
237
|
this.disposeRecognizer();
|
|
223
238
|
this.isRecording = false;
|
|
224
239
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
@@ -253,6 +268,14 @@ export class MicrophoneHandler {
|
|
|
253
268
|
}
|
|
254
269
|
disposeRecognizer() {
|
|
255
270
|
this.stopHealthCheck();
|
|
271
|
+
if (this.speechProvider === "sarvam") {
|
|
272
|
+
if (this.sarvamSpeechService) {
|
|
273
|
+
void this.sarvamSpeechService.stopListening().catch((error) => {
|
|
274
|
+
console.error("[MicrophoneHandler] Error stopping Sarvam STT:", error);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
256
279
|
if (this.recognizer) {
|
|
257
280
|
try {
|
|
258
281
|
this.recognizer.stopContinuousRecognitionAsync(() => {
|
|
@@ -296,6 +319,7 @@ export class MicrophoneHandler {
|
|
|
296
319
|
console.warn("[MicrophoneHandler] Recovery in progress, cannot start");
|
|
297
320
|
return false;
|
|
298
321
|
}
|
|
322
|
+
this.isStoppingIntentionally = false;
|
|
299
323
|
this.activeCallbacks = callbacks;
|
|
300
324
|
if (!this.hasPermission) {
|
|
301
325
|
const granted = await this.requestMicrophonePermission();
|
|
@@ -306,6 +330,9 @@ export class MicrophoneHandler {
|
|
|
306
330
|
return false;
|
|
307
331
|
}
|
|
308
332
|
}
|
|
333
|
+
if (this.speechProvider === "sarvam") {
|
|
334
|
+
return this.startSarvamRecording(callbacks);
|
|
335
|
+
}
|
|
309
336
|
const sdkReady = await this.ensureSpeechSdk();
|
|
310
337
|
if (!sdkReady) {
|
|
311
338
|
if (callbacks.onError) {
|
|
@@ -369,12 +396,16 @@ export class MicrophoneHandler {
|
|
|
369
396
|
}
|
|
370
397
|
};
|
|
371
398
|
this.recognizer.sessionStopped = (_s, _e) => {
|
|
372
|
-
|
|
399
|
+
if (this.isStoppingIntentionally) {
|
|
400
|
+
console.log("[MicrophoneHandler] Session stopped (user-initiated)");
|
|
401
|
+
this.isRecording = false;
|
|
402
|
+
this.stopHealthCheck();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
console.warn("[MicrophoneHandler] Session stopped unexpectedly by SDK - attempting auto-restart");
|
|
373
406
|
this.isRecording = false;
|
|
374
407
|
this.stopHealthCheck();
|
|
375
|
-
|
|
376
|
-
this.activeCallbacks.onError(new Error("Speech recognition session stopped unexpectedly"));
|
|
377
|
-
}
|
|
408
|
+
void this.recoverFromStalledRecognizer();
|
|
378
409
|
};
|
|
379
410
|
this.recognizer.startContinuousRecognitionAsync(() => {
|
|
380
411
|
console.log("[MicrophoneHandler] Continuous recognition started");
|
|
@@ -392,27 +423,94 @@ export class MicrophoneHandler {
|
|
|
392
423
|
});
|
|
393
424
|
return true;
|
|
394
425
|
}
|
|
426
|
+
async startSarvamRecording(callbacks) {
|
|
427
|
+
if (!this.sarvamSpeechService) {
|
|
428
|
+
callbacks.onError?.(new Error("Sarvam speech service not configured"));
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
this.resetTranscriptionBuffer();
|
|
432
|
+
try {
|
|
433
|
+
await this.sarvamSpeechService.startListening(this.language, {
|
|
434
|
+
shouldSendAudio: () => this.isRecording,
|
|
435
|
+
onPartial: (text) => {
|
|
436
|
+
this.lastRecognitionTime = Date.now();
|
|
437
|
+
this.activeCallbacks.onPartial?.(text);
|
|
438
|
+
},
|
|
439
|
+
onFinal: (text) => {
|
|
440
|
+
this.lastRecognitionTime = Date.now();
|
|
441
|
+
this.transcriptionBuffer.push(text);
|
|
442
|
+
this.activeCallbacks.onSegment?.(text, {});
|
|
443
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
444
|
+
},
|
|
445
|
+
onSpeechStart: () => {
|
|
446
|
+
this.lastRecognitionTime = Date.now();
|
|
447
|
+
},
|
|
448
|
+
onSpeechEnd: () => {
|
|
449
|
+
this.lastRecognitionTime = Date.now();
|
|
450
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
451
|
+
},
|
|
452
|
+
onError: (error) => {
|
|
453
|
+
this.isRecording = false;
|
|
454
|
+
this.stopHealthCheck();
|
|
455
|
+
this.activeCallbacks.onError?.(error);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
this.isRecording = true;
|
|
459
|
+
this.audioContextUnlocked = true;
|
|
460
|
+
this.lastRecognitionTime = Date.now();
|
|
461
|
+
this.startHealthCheck();
|
|
462
|
+
this.scheduleSilenceCheck(this.activeCallbacks);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
this.isRecording = false;
|
|
467
|
+
callbacks.onError?.(error);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
395
471
|
async stopRecording() {
|
|
396
472
|
this.clearSilenceTimer();
|
|
397
473
|
this.stopHealthCheck();
|
|
474
|
+
this.isStoppingIntentionally = true;
|
|
475
|
+
if (this.speechProvider === "sarvam") {
|
|
476
|
+
if (!this.isRecording || !this.sarvamSpeechService) {
|
|
477
|
+
console.warn("[MicrophoneHandler] Not currently recording");
|
|
478
|
+
this.isStoppingIntentionally = false;
|
|
479
|
+
return this.getAggregatedText();
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
await this.sarvamSpeechService.stopListening();
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error("[MicrophoneHandler] Error stopping Sarvam STT:", error);
|
|
486
|
+
}
|
|
487
|
+
this.isRecording = false;
|
|
488
|
+
this.isStoppingIntentionally = false;
|
|
489
|
+
const finalText = this.getAggregatedText();
|
|
490
|
+
return finalText;
|
|
491
|
+
}
|
|
398
492
|
if (!this.isRecording || !this.recognizer) {
|
|
399
493
|
console.warn("[MicrophoneHandler] Not currently recording");
|
|
494
|
+
this.isStoppingIntentionally = false;
|
|
400
495
|
return this.getAggregatedText();
|
|
401
496
|
}
|
|
402
497
|
return new Promise((resolve) => {
|
|
403
498
|
if (!this.recognizer) {
|
|
499
|
+
this.isStoppingIntentionally = false;
|
|
404
500
|
resolve(this.getAggregatedText());
|
|
405
501
|
return;
|
|
406
502
|
}
|
|
407
503
|
this.recognizer.stopContinuousRecognitionAsync(() => {
|
|
408
504
|
console.log("[MicrophoneHandler] Recognition stopped successfully");
|
|
409
505
|
this.isRecording = false;
|
|
506
|
+
this.isStoppingIntentionally = false;
|
|
410
507
|
const finalText = this.getAggregatedText();
|
|
411
508
|
this.disposeRecognizer();
|
|
412
509
|
resolve(finalText);
|
|
413
510
|
}, (error) => {
|
|
414
511
|
console.error("[MicrophoneHandler] Error stopping recognition:", error);
|
|
415
512
|
this.isRecording = false;
|
|
513
|
+
this.isStoppingIntentionally = false;
|
|
416
514
|
const finalText = this.getAggregatedText();
|
|
417
515
|
this.disposeRecognizer();
|
|
418
516
|
resolve(finalText);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { NavigationConfig } from "./types.js";
|
|
2
2
|
export declare class VoiceNavigationController {
|
|
3
3
|
private config;
|
|
4
|
+
private speechProvider;
|
|
4
5
|
private microphoneHandler;
|
|
5
6
|
private azureSpeechService;
|
|
7
|
+
private sarvamSpeechService;
|
|
6
8
|
private bedrockService;
|
|
7
9
|
private voiceFeedbackService;
|
|
8
10
|
private pageRegistry;
|
|
@@ -17,6 +19,8 @@ export declare class VoiceNavigationController {
|
|
|
17
19
|
private pagesInitialized;
|
|
18
20
|
private uiInitialized;
|
|
19
21
|
private isSyncingButtonState;
|
|
22
|
+
private hasNavigatedAway;
|
|
23
|
+
private removeNavigationListener;
|
|
20
24
|
constructor(config: NavigationConfig);
|
|
21
25
|
private initializeUIAsync;
|
|
22
26
|
private shouldAutoStart;
|
|
@@ -33,6 +37,7 @@ export declare class VoiceNavigationController {
|
|
|
33
37
|
stop(options?: {
|
|
34
38
|
triggeredByUser?: boolean;
|
|
35
39
|
}): Promise<void>;
|
|
40
|
+
private trackNavigationAway;
|
|
36
41
|
private syncCustomButtonState;
|
|
37
42
|
private handlePartial;
|
|
38
43
|
private handleSegment;
|
|
@@ -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,EAOjB,MAAM,YAAY,CAAC;AA8DpB,qBAAa,yBAAyB;IACpC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,kBAAkB,CAA4B;IACtD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,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;IACrC,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,gBAAgB,CAAkB;IAC1C,OAAO,CAAC,wBAAwB,CAA6B;gBAEjD,MAAM,EAAE,gBAAgB;YAgMtB,iBAAiB;IAoD/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;IA8BhB,KAAK,CAChB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAO,GACzC,OAAO,CAAC,IAAI,CAAC;IAuFH,IAAI,CACf,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAC1C,OAAO,CAAC,IAAI,CAAC;IAqEhB,OAAO,CAAC,mBAAmB;IAoD3B,OAAO,CAAC,qBAAqB;IA4C7B,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,aAAa;YAWP,aAAa;YAOb,wBAAwB;IA2CtC,OAAO,CAAC,WAAW;YAuBL,iBAAiB;YAwBjB,iBAAiB;IA6RxB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IASnC,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;IAqD/B,OAAO,CAAC,sBAAsB;IAW9B,OAAO,CAAC,wBAAwB;CAmBjC"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { MicrophoneHandler } from "./microphone-handler.js";
|
|
2
2
|
import { AzureSpeechService } from "./services/azure-speech.js";
|
|
3
3
|
import { BedrockService } from "./services/bedrock.js";
|
|
4
|
+
import { SarvamSpeechService } from "./services/sarvam-speech.js";
|
|
4
5
|
import { PageRegistry } from "./services/page-registry.js";
|
|
5
6
|
import { VoiceFeedbackService } from "./services/voice-feedback.js";
|
|
6
7
|
import { loadNavigationPages } from "./services/xml-parser.js";
|
|
8
|
+
import { normalizeLanguageCode } from "./languages.js";
|
|
7
9
|
import { VectorSearchService, } from "./services/vector-search.js";
|
|
8
10
|
import { performAgentAction, formatActionLabel, extractAgentAction, setPageRegistry, } from "./actions.js";
|
|
9
11
|
import { createFloatingControl, attachToCustomButton, setState, updateStatus, updateTranscript, updateActionIndicator, showError, emitEvent, } from "./ui.js";
|
|
@@ -39,21 +41,34 @@ export class VoiceNavigationController {
|
|
|
39
41
|
this.removeAutoStartListeners = null;
|
|
40
42
|
this.vectorSearchService = null;
|
|
41
43
|
this.isSyncingButtonState = false;
|
|
44
|
+
this.hasNavigatedAway = false;
|
|
45
|
+
this.removeNavigationListener = null;
|
|
42
46
|
const openSearchConfig = resolveOpenSearchConfig(config);
|
|
43
47
|
this.config = {
|
|
44
48
|
...config,
|
|
45
49
|
opensearch: openSearchConfig,
|
|
50
|
+
speechProvider: config.speechProvider ?? "sarvam",
|
|
46
51
|
};
|
|
52
|
+
this.speechProvider = this.config.speechProvider ?? "sarvam";
|
|
47
53
|
console.log("[Voice Navigation Controller] Constructor called with UI config:", this.config.ui);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.config.azure
|
|
52
|
-
|
|
54
|
+
if (this.speechProvider === "azure") {
|
|
55
|
+
const hasAzureEndpoint = !!this.config.azure?.tokenEndpoint;
|
|
56
|
+
const hasAzureCredentials = !!(this.config.azure?.subscriptionKey && this.config.azure?.region);
|
|
57
|
+
if (!this.config.azure) {
|
|
58
|
+
this.config.azure = { tokenEndpoint: "/api/speech/token" };
|
|
59
|
+
console.log("[VoiceNavigation] Auto-configured Azure endpoint: /api/speech/token");
|
|
60
|
+
}
|
|
61
|
+
else if (!hasAzureEndpoint && !hasAzureCredentials) {
|
|
62
|
+
this.config.azure.tokenEndpoint = "/api/speech/token";
|
|
63
|
+
console.log("[VoiceNavigation] Auto-configured Azure endpoint: /api/speech/token");
|
|
64
|
+
}
|
|
53
65
|
}
|
|
54
|
-
|
|
55
|
-
this.config.
|
|
56
|
-
|
|
66
|
+
if (this.speechProvider === "sarvam") {
|
|
67
|
+
this.config.sarvam = {
|
|
68
|
+
...(this.config.sarvam ?? {}),
|
|
69
|
+
ttsUrl: this.config.sarvam?.ttsUrl ?? "/api/speech/sarvam",
|
|
70
|
+
sttUrl: this.config.sarvam?.sttUrl ?? "/api/speech/sarvam-stt",
|
|
71
|
+
};
|
|
57
72
|
}
|
|
58
73
|
const hasAwsEndpoint = !!this.config.aws?.bedrockEndpoint;
|
|
59
74
|
const hasAwsCredentials = !!(this.config.aws?.accessKeyId &&
|
|
@@ -78,11 +93,29 @@ export class VoiceNavigationController {
|
|
|
78
93
|
openSearchConfig.suggestEndpoint = "/api/opensearch/suggest";
|
|
79
94
|
console.log("[VoiceNavigation] Auto-configured OpenSearch endpoints: /api/opensearch/search, /api/opensearch/suggest");
|
|
80
95
|
}
|
|
81
|
-
this.azureSpeechService =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
this.azureSpeechService = null;
|
|
97
|
+
this.sarvamSpeechService = null;
|
|
98
|
+
if (this.speechProvider === "azure") {
|
|
99
|
+
if (!this.config.azure) {
|
|
100
|
+
throw new Error("Azure configuration is required for azure provider");
|
|
101
|
+
}
|
|
102
|
+
this.azureSpeechService = new AzureSpeechService({
|
|
103
|
+
tokenEndpoint: this.config.azure.tokenEndpoint,
|
|
104
|
+
subscriptionKey: this.config.azure.subscriptionKey,
|
|
105
|
+
region: this.config.azure.region,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.sarvamSpeechService = new SarvamSpeechService({
|
|
110
|
+
ttsUrl: this.config.sarvam?.ttsUrl,
|
|
111
|
+
sttUrl: this.config.sarvam?.sttUrl,
|
|
112
|
+
sttModel: this.config.sarvam?.sttModel,
|
|
113
|
+
ttsModel: this.config.sarvam?.ttsModel,
|
|
114
|
+
speaker: this.config.sarvam?.speaker,
|
|
115
|
+
sttSampleRate: this.config.sarvam?.sttSampleRate,
|
|
116
|
+
ttsSampleRate: this.config.sarvam?.ttsSampleRate,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
86
119
|
this.bedrockService = new BedrockService({
|
|
87
120
|
bedrockEndpoint: this.config.aws.bedrockEndpoint,
|
|
88
121
|
embeddingEndpoint: this.config.aws.embeddingEndpoint,
|
|
@@ -97,16 +130,20 @@ export class VoiceNavigationController {
|
|
|
97
130
|
this.pageRegistry = new PageRegistry([]);
|
|
98
131
|
this.pagesInitialized = this.initializePages();
|
|
99
132
|
this.microphoneHandler = new MicrophoneHandler({
|
|
100
|
-
|
|
133
|
+
speechProvider: this.speechProvider,
|
|
134
|
+
azureSpeechService: this.azureSpeechService ?? undefined,
|
|
135
|
+
sarvamSpeechService: this.sarvamSpeechService ?? undefined,
|
|
101
136
|
bedrockService: this.bedrockService,
|
|
102
|
-
language: this.config.language
|
|
137
|
+
language: normalizeLanguageCode(this.config.language),
|
|
103
138
|
});
|
|
104
139
|
const voiceFeedbackEnabled = this.config.voiceFeedback?.enabled !== false;
|
|
105
140
|
console.log("[VoiceNavigation] Initializing voice feedback service - enabled:", voiceFeedbackEnabled);
|
|
106
141
|
this.voiceFeedbackService = new VoiceFeedbackService({
|
|
107
142
|
enabled: voiceFeedbackEnabled,
|
|
108
|
-
language: this.config.language
|
|
109
|
-
|
|
143
|
+
language: normalizeLanguageCode(this.config.language),
|
|
144
|
+
speechProvider: this.speechProvider,
|
|
145
|
+
azureSpeechService: this.azureSpeechService ?? undefined,
|
|
146
|
+
sarvamSpeechService: this.sarvamSpeechService ?? undefined,
|
|
110
147
|
bedrockService: this.bedrockService,
|
|
111
148
|
});
|
|
112
149
|
console.log("[VoiceNavigation] Voice feedback service initialized successfully");
|
|
@@ -120,6 +157,9 @@ export class VoiceNavigationController {
|
|
|
120
157
|
}
|
|
121
158
|
}
|
|
122
159
|
this.resumeAfterNavigation = this.consumeResumeState();
|
|
160
|
+
if (this.resumeAfterNavigation) {
|
|
161
|
+
this.hasNavigatedAway = true;
|
|
162
|
+
}
|
|
123
163
|
this.uiInitialized = this.initializeUIAsync();
|
|
124
164
|
if (this.resumeAfterNavigation) {
|
|
125
165
|
void this.start({ reason: "auto" });
|
|
@@ -138,7 +178,7 @@ export class VoiceNavigationController {
|
|
|
138
178
|
customButtonSelector,
|
|
139
179
|
buttonPlacement,
|
|
140
180
|
});
|
|
141
|
-
if (
|
|
181
|
+
if (customButtonSelector) {
|
|
142
182
|
console.log(`[Voice Navigation] Attempting to use custom button: ${customButtonSelector}`);
|
|
143
183
|
try {
|
|
144
184
|
this.ui = await attachToCustomButton(customButtonSelector);
|
|
@@ -146,11 +186,11 @@ export class VoiceNavigationController {
|
|
|
146
186
|
}
|
|
147
187
|
catch (error) {
|
|
148
188
|
console.error("Failed to attach to custom button:", error);
|
|
149
|
-
console.warn("Falling back to default button
|
|
189
|
+
console.warn("Falling back to default button - custom button selector was not found in the DOM");
|
|
150
190
|
this.ui = createFloatingControl(buttonPlacement);
|
|
151
191
|
}
|
|
152
192
|
}
|
|
153
|
-
else if (!showDefaultButton
|
|
193
|
+
else if (!showDefaultButton) {
|
|
154
194
|
console.warn("showDefaultButton is false but no customButtonSelector provided. Creating default button.");
|
|
155
195
|
this.ui = createFloatingControl(buttonPlacement);
|
|
156
196
|
}
|
|
@@ -309,6 +349,7 @@ export class VoiceNavigationController {
|
|
|
309
349
|
});
|
|
310
350
|
if (started) {
|
|
311
351
|
this.clearAutoStartFallback();
|
|
352
|
+
this.trackNavigationAway();
|
|
312
353
|
setState(this.ui, "listening");
|
|
313
354
|
emitEvent("state-change", { state: "listening" });
|
|
314
355
|
}
|
|
@@ -344,8 +385,13 @@ export class VoiceNavigationController {
|
|
|
344
385
|
emitEvent("state-change", { state: "idle" });
|
|
345
386
|
updateTranscript(this.ui, "");
|
|
346
387
|
if (!triggeredByUser && this.ui.isCustomButton) {
|
|
347
|
-
|
|
348
|
-
|
|
388
|
+
if (!this.hasNavigatedAway) {
|
|
389
|
+
console.log("[VoiceNavigation] Programmatically clicking custom button to sync external UI state");
|
|
390
|
+
this.syncCustomButtonState();
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
console.log("[VoiceNavigation] Skipping custom button click - user has navigated away from start page");
|
|
394
|
+
}
|
|
349
395
|
}
|
|
350
396
|
}
|
|
351
397
|
catch (error) {
|
|
@@ -354,27 +400,80 @@ export class VoiceNavigationController {
|
|
|
354
400
|
setState(this.ui, "error");
|
|
355
401
|
emitEvent("error", { error });
|
|
356
402
|
if (!triggeredByUser && this.ui.isCustomButton) {
|
|
357
|
-
|
|
358
|
-
|
|
403
|
+
if (!this.hasNavigatedAway) {
|
|
404
|
+
console.log("[VoiceNavigation] Programmatically clicking custom button after error");
|
|
405
|
+
this.syncCustomButtonState();
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
console.log("[VoiceNavigation] Skipping custom button click after error - user has navigated away from start page");
|
|
409
|
+
}
|
|
359
410
|
}
|
|
360
411
|
}
|
|
361
412
|
}
|
|
413
|
+
trackNavigationAway() {
|
|
414
|
+
if (typeof window === "undefined") {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (this.removeNavigationListener) {
|
|
418
|
+
this.removeNavigationListener();
|
|
419
|
+
this.removeNavigationListener = null;
|
|
420
|
+
}
|
|
421
|
+
const startUrl = window.location.href;
|
|
422
|
+
const markNavigatedAway = () => {
|
|
423
|
+
if (window.location.href !== startUrl) {
|
|
424
|
+
this.hasNavigatedAway = true;
|
|
425
|
+
console.log("[VoiceNavigation] User navigated away from start page - custom button sync disabled");
|
|
426
|
+
if (this.removeNavigationListener) {
|
|
427
|
+
this.removeNavigationListener();
|
|
428
|
+
this.removeNavigationListener = null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
window.addEventListener("popstate", markNavigatedAway);
|
|
433
|
+
const originalPushState = history.pushState.bind(history);
|
|
434
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
435
|
+
history.pushState = (...args) => {
|
|
436
|
+
originalPushState(...args);
|
|
437
|
+
markNavigatedAway();
|
|
438
|
+
};
|
|
439
|
+
history.replaceState = (...args) => {
|
|
440
|
+
originalReplaceState(...args);
|
|
441
|
+
markNavigatedAway();
|
|
442
|
+
};
|
|
443
|
+
this.removeNavigationListener = () => {
|
|
444
|
+
window.removeEventListener("popstate", markNavigatedAway);
|
|
445
|
+
history.pushState = originalPushState;
|
|
446
|
+
history.replaceState = originalReplaceState;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
362
449
|
syncCustomButtonState() {
|
|
363
|
-
if (!this.ui.isCustomButton) {
|
|
450
|
+
if (!this.ui.isCustomButton || !this.ui.button) {
|
|
364
451
|
return;
|
|
365
452
|
}
|
|
453
|
+
console.log("[VoiceNavigation] Syncing custom button state");
|
|
366
454
|
try {
|
|
367
455
|
this.isSyncingButtonState = true;
|
|
456
|
+
this.ui.button.setAttribute("aria-pressed", "false");
|
|
457
|
+
this.ui.button.setAttribute("data-voice-state", "idle");
|
|
458
|
+
this.ui.button.classList.remove("active", "recording", "listening");
|
|
459
|
+
this.ui.button.classList.add("inactive", "stopped");
|
|
460
|
+
console.log("[VoiceNavigation] Updated button attributes");
|
|
461
|
+
const event = new CustomEvent("voiceNavigation:stopped", {
|
|
462
|
+
bubbles: true,
|
|
463
|
+
detail: { stopped: true },
|
|
464
|
+
});
|
|
465
|
+
this.ui.button.dispatchEvent(event);
|
|
466
|
+
console.log("[VoiceNavigation] Dispatched voiceNavigation:stopped event");
|
|
368
467
|
this.ui.button.click();
|
|
369
|
-
console.log("[VoiceNavigation]
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
console.error("[VoiceNavigation] Failed to click custom button:", error);
|
|
373
|
-
}
|
|
374
|
-
finally {
|
|
468
|
+
console.log("[VoiceNavigation] Clicked button once to sync state");
|
|
375
469
|
setTimeout(() => {
|
|
376
470
|
this.isSyncingButtonState = false;
|
|
377
|
-
|
|
471
|
+
console.log("[VoiceNavigation] Button state sync complete");
|
|
472
|
+
}, 500);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
console.error("[VoiceNavigation] Error syncing button state:", error);
|
|
476
|
+
this.isSyncingButtonState = false;
|
|
378
477
|
}
|
|
379
478
|
}
|
|
380
479
|
handlePartial(text) {
|
|
@@ -491,6 +590,8 @@ export class VoiceNavigationController {
|
|
|
491
590
|
await this.provideActionFeedback(action, actionResult, originalText);
|
|
492
591
|
console.log("[VoiceNavigation] Search feedback complete, now navigating to:", actionResult.info.navigationUrl);
|
|
493
592
|
if (actionResult.info.navigationUrl) {
|
|
593
|
+
if (this.ui.isCustomButton)
|
|
594
|
+
this.syncCustomButtonState();
|
|
494
595
|
window.location.href = actionResult.info.navigationUrl;
|
|
495
596
|
actionResult.info.navigated = true;
|
|
496
597
|
actionResult.info.pendingNavigation = false;
|
|
@@ -499,6 +600,8 @@ export class VoiceNavigationController {
|
|
|
499
600
|
catch (err) {
|
|
500
601
|
console.error("[VoiceNavigation] Voice feedback error (search):", err);
|
|
501
602
|
if (actionResult.info.navigationUrl) {
|
|
603
|
+
if (this.ui.isCustomButton)
|
|
604
|
+
this.syncCustomButtonState();
|
|
502
605
|
window.location.href = actionResult.info.navigationUrl;
|
|
503
606
|
}
|
|
504
607
|
}
|
|
@@ -538,7 +641,7 @@ export class VoiceNavigationController {
|
|
|
538
641
|
}
|
|
539
642
|
const actionResult = performAgentAction(action, {
|
|
540
643
|
handler: this,
|
|
541
|
-
onStop: () => this.stop(),
|
|
644
|
+
onStop: () => this.stop({ triggeredByUser: false }),
|
|
542
645
|
transcript: originalText,
|
|
543
646
|
parameters: {
|
|
544
647
|
text: actionData.text || query,
|
|
@@ -562,24 +665,32 @@ export class VoiceNavigationController {
|
|
|
562
665
|
if (actionResult.info?.pendingNavigation) {
|
|
563
666
|
if (actionResult.info.navigationUrl) {
|
|
564
667
|
console.log("[VoiceNavigation] Feedback complete, now navigating to:", actionResult.info.navigationUrl);
|
|
668
|
+
if (this.ui.isCustomButton)
|
|
669
|
+
this.syncCustomButtonState();
|
|
565
670
|
window.location.href = actionResult.info.navigationUrl;
|
|
566
671
|
actionResult.info.navigated = true;
|
|
567
672
|
actionResult.info.pendingNavigation = false;
|
|
568
673
|
}
|
|
569
674
|
else if (actionResult.info.navigationType === "back") {
|
|
570
675
|
console.log("[VoiceNavigation] Feedback complete, navigating back");
|
|
676
|
+
if (this.ui.isCustomButton)
|
|
677
|
+
this.syncCustomButtonState();
|
|
571
678
|
window.history.back();
|
|
572
679
|
actionResult.info.navigated = true;
|
|
573
680
|
actionResult.info.pendingNavigation = false;
|
|
574
681
|
}
|
|
575
682
|
else if (actionResult.info.navigationType === "forward") {
|
|
576
683
|
console.log("[VoiceNavigation] Feedback complete, navigating forward");
|
|
684
|
+
if (this.ui.isCustomButton)
|
|
685
|
+
this.syncCustomButtonState();
|
|
577
686
|
window.history.forward();
|
|
578
687
|
actionResult.info.navigated = true;
|
|
579
688
|
actionResult.info.pendingNavigation = false;
|
|
580
689
|
}
|
|
581
690
|
else if (actionResult.info.navigationType === "reload") {
|
|
582
691
|
console.log("[VoiceNavigation] Feedback complete, reloading page");
|
|
692
|
+
if (this.ui.isCustomButton)
|
|
693
|
+
this.syncCustomButtonState();
|
|
583
694
|
window.location.reload();
|
|
584
695
|
actionResult.info.reloaded = true;
|
|
585
696
|
actionResult.info.pendingNavigation = false;
|
|
@@ -589,6 +700,8 @@ export class VoiceNavigationController {
|
|
|
589
700
|
catch (err) {
|
|
590
701
|
console.error("[VoiceNavigation] Voice feedback error (critical action):", err);
|
|
591
702
|
if (actionResult.info?.pendingNavigation) {
|
|
703
|
+
if (this.ui.isCustomButton)
|
|
704
|
+
this.syncCustomButtonState();
|
|
592
705
|
if (actionResult.info.navigationUrl) {
|
|
593
706
|
console.log("[VoiceNavigation] Feedback failed, but proceeding with navigation:", actionResult.info.navigationUrl);
|
|
594
707
|
window.location.href = actionResult.info.navigationUrl;
|
|
@@ -641,8 +754,9 @@ export class VoiceNavigationController {
|
|
|
641
754
|
});
|
|
642
755
|
}
|
|
643
756
|
setLanguage(language) {
|
|
644
|
-
|
|
645
|
-
this.
|
|
757
|
+
const normalized = normalizeLanguageCode(language);
|
|
758
|
+
this.microphoneHandler.setLanguage(normalized);
|
|
759
|
+
this.voiceFeedbackService.setLanguage(normalized);
|
|
646
760
|
}
|
|
647
761
|
setVoiceFeedback(enabled) {
|
|
648
762
|
this.voiceFeedbackService.setEnabled(enabled);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
type
|
|
1
|
+
import { type GateRequestLike } from "./security.js";
|
|
2
|
+
type NextApiRequest = GateRequestLike & {
|
|
2
3
|
method?: string;
|
|
3
4
|
body?: unknown;
|
|
4
5
|
};
|
|
@@ -8,6 +9,6 @@ type NextApiResponse = {
|
|
|
8
9
|
setHeader: (name: string, value: string | string[]) => void;
|
|
9
10
|
};
|
|
10
11
|
export declare function azureSpeechTokenHandler(req: NextApiRequest, res: NextApiResponse): Promise<void>;
|
|
11
|
-
export declare function POST(
|
|
12
|
+
export declare function POST(request: Request): Promise<Response>;
|
|
12
13
|
export {};
|
|
13
14
|
//# sourceMappingURL=azure-speech-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"azure-speech-handler.d.ts","sourceRoot":"","sources":["../../src/server/azure-speech-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"azure-speech-handler.d.ts","sourceRoot":"","sources":["../../src/server/azure-speech-handler.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGnE,KAAK,cAAc,GAAG,eAAe,GAAG;IACtC,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,CAgCf;AAMD,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqC9D"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { evaluateGate } from "./security.js";
|
|
1
2
|
async function getAzureSpeechToken() {
|
|
2
3
|
const key = process.env.AZURE_SPEECH_KEY;
|
|
3
4
|
const region = process.env.AZURE_SPEECH_REGION;
|
|
@@ -49,6 +50,12 @@ export async function azureSpeechTokenHandler(req, res) {
|
|
|
49
50
|
res.status(405).json({ error: "Method not allowed" });
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
53
|
+
const gate = evaluateGate(req, { maxRequests: 20, windowMs: 60 * 1000, bucket: "aztoken" });
|
|
54
|
+
if (!gate.ok) {
|
|
55
|
+
res.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate");
|
|
56
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
52
59
|
const result = await getAzureSpeechToken();
|
|
53
60
|
res.setHeader("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate");
|
|
54
61
|
if (result.success && result.data) {
|
|
@@ -58,12 +65,29 @@ export async function azureSpeechTokenHandler(req, res) {
|
|
|
58
65
|
res.status(result.statusCode).json(result.error);
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
|
-
export async function POST(
|
|
62
|
-
const result = await getAzureSpeechToken();
|
|
68
|
+
export async function POST(request) {
|
|
63
69
|
const headers = {
|
|
64
70
|
"Content-Type": "application/json",
|
|
65
71
|
"Cache-Control": "no-store, no-cache, max-age=0, must-revalidate",
|
|
66
72
|
};
|
|
73
|
+
const gateReq = {
|
|
74
|
+
method: request.method,
|
|
75
|
+
headers: {
|
|
76
|
+
origin: request.headers.get("origin") ?? undefined,
|
|
77
|
+
referer: request.headers.get("referer") ?? undefined,
|
|
78
|
+
host: request.headers.get("host") ?? undefined,
|
|
79
|
+
"x-forwarded-for": request.headers.get("x-forwarded-for") ?? undefined,
|
|
80
|
+
"x-real-ip": request.headers.get("x-real-ip") ?? undefined,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const gate = evaluateGate(gateReq, { maxRequests: 20, windowMs: 60 * 1000, bucket: "aztoken" });
|
|
84
|
+
if (!gate.ok) {
|
|
85
|
+
return new Response(JSON.stringify({ error: gate.error }), {
|
|
86
|
+
status: gate.status ?? 403,
|
|
87
|
+
headers,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const result = await getAzureSpeechToken();
|
|
67
91
|
if (result.success && result.data) {
|
|
68
92
|
return new Response(JSON.stringify(result.data), {
|
|
69
93
|
status: result.statusCode,
|