@myscheme/voice-navigation-sdk 0.1.9 → 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 +8 -2
- package/dist/microphone-handler.d.ts.map +1 -1
- package/dist/microphone-handler.js +89 -2
- package/dist/navigation-controller.d.ts +2 -0
- package/dist/navigation-controller.d.ts.map +1 -1
- package/dist/navigation-controller.js +58 -22
- 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,4 +1,82 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
|
+
import { evaluateGate } from "./security.js";
|
|
3
|
+
const MAX_OUTPUT_TOKENS = 512;
|
|
4
|
+
const DEFAULT_OUTPUT_TOKENS = 256;
|
|
5
|
+
const MAX_MESSAGES = 6;
|
|
6
|
+
const MAX_SYSTEM_CHARS = 20000;
|
|
7
|
+
const MAX_TEXT_CHARS = 20000;
|
|
8
|
+
const MAX_TOTAL_INPUT_CHARS = 40000;
|
|
9
|
+
const clampNumber = (value, min, max, fallback) => {
|
|
10
|
+
const num = typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
11
|
+
return Math.min(max, Math.max(min, Math.floor(num)));
|
|
12
|
+
};
|
|
13
|
+
const sanitizeContent = (content) => {
|
|
14
|
+
if (typeof content === "string") {
|
|
15
|
+
return [{ type: "text", text: content.slice(0, MAX_TEXT_CHARS) }];
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(content)) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (const part of content) {
|
|
22
|
+
if (part && typeof part === "object" && part.type === "text") {
|
|
23
|
+
const text = part.text;
|
|
24
|
+
if (typeof text === "string") {
|
|
25
|
+
parts.push({ type: "text", text: text.slice(0, MAX_TEXT_CHARS) });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return parts;
|
|
30
|
+
};
|
|
31
|
+
function sanitizeClaudeRequest(input) {
|
|
32
|
+
const raw = input && typeof input === "object" && "body" in input
|
|
33
|
+
? input.body
|
|
34
|
+
: input;
|
|
35
|
+
if (!raw || typeof raw !== "object") {
|
|
36
|
+
return { ok: false };
|
|
37
|
+
}
|
|
38
|
+
const source = raw;
|
|
39
|
+
const rawMessages = Array.isArray(source.messages) ? source.messages : [];
|
|
40
|
+
if (rawMessages.length === 0) {
|
|
41
|
+
return { ok: false };
|
|
42
|
+
}
|
|
43
|
+
let totalChars = 0;
|
|
44
|
+
const messages = [];
|
|
45
|
+
for (const message of rawMessages.slice(0, MAX_MESSAGES)) {
|
|
46
|
+
if (!message || typeof message !== "object") {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const role = message.role;
|
|
50
|
+
if (role !== "user" && role !== "assistant") {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const content = sanitizeContent(message.content);
|
|
54
|
+
if (content.length === 0) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const part of content) {
|
|
58
|
+
totalChars += part.text.length;
|
|
59
|
+
}
|
|
60
|
+
messages.push({ role, content });
|
|
61
|
+
}
|
|
62
|
+
if (messages.length === 0 || totalChars > MAX_TOTAL_INPUT_CHARS) {
|
|
63
|
+
return { ok: false };
|
|
64
|
+
}
|
|
65
|
+
const body = {
|
|
66
|
+
anthropic_version: typeof source.anthropic_version === "string"
|
|
67
|
+
? source.anthropic_version
|
|
68
|
+
: "bedrock-2023-05-31",
|
|
69
|
+
max_tokens: clampNumber(source.max_tokens, 1, MAX_OUTPUT_TOKENS, DEFAULT_OUTPUT_TOKENS),
|
|
70
|
+
messages,
|
|
71
|
+
};
|
|
72
|
+
if (typeof source.system === "string") {
|
|
73
|
+
body.system = source.system.slice(0, MAX_SYSTEM_CHARS);
|
|
74
|
+
}
|
|
75
|
+
if (source.temperature != null) {
|
|
76
|
+
body.temperature = clampNumber(source.temperature, 0, 1, 0);
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, body };
|
|
79
|
+
}
|
|
2
80
|
function sign(key, msg) {
|
|
3
81
|
return crypto.createHmac("sha256", key).update(msg, "utf8").digest();
|
|
4
82
|
}
|
|
@@ -129,43 +207,70 @@ export async function bedrockHandler(req, res) {
|
|
|
129
207
|
res.status(405).json({ error: "Method not allowed" });
|
|
130
208
|
return;
|
|
131
209
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
210
|
+
const gate = evaluateGate(req, { maxRequests: 30, windowMs: 60 * 1000, bucket: "bedrock" });
|
|
211
|
+
if (!gate.ok) {
|
|
212
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const sanitized = sanitizeClaudeRequest(req.body);
|
|
216
|
+
if (!sanitized.ok) {
|
|
217
|
+
res.status(400).json({ error: "Invalid request body" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const result = await invokeBedrock(sanitized.body);
|
|
134
221
|
if (result.success && result.data) {
|
|
135
222
|
res.status(result.statusCode).json(result.data);
|
|
136
223
|
}
|
|
137
224
|
else {
|
|
138
|
-
res
|
|
225
|
+
res
|
|
226
|
+
.status(result.statusCode >= 500 ? 502 : result.statusCode)
|
|
227
|
+
.json({ error: "Bedrock request failed" });
|
|
139
228
|
}
|
|
140
229
|
}
|
|
141
230
|
export async function BedrockPOST(request) {
|
|
231
|
+
const headers = { "Content-Type": "application/json" };
|
|
232
|
+
const gateReq = {
|
|
233
|
+
method: request.method,
|
|
234
|
+
headers: {
|
|
235
|
+
origin: request.headers.get("origin") ?? undefined,
|
|
236
|
+
referer: request.headers.get("referer") ?? undefined,
|
|
237
|
+
host: request.headers.get("host") ?? undefined,
|
|
238
|
+
"x-forwarded-for": request.headers.get("x-forwarded-for") ?? undefined,
|
|
239
|
+
"x-real-ip": request.headers.get("x-real-ip") ?? undefined,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
const gate = evaluateGate(gateReq, { maxRequests: 30, windowMs: 60 * 1000, bucket: "bedrock" });
|
|
243
|
+
if (!gate.ok) {
|
|
244
|
+
return new Response(JSON.stringify({ error: gate.error }), {
|
|
245
|
+
status: gate.status ?? 403,
|
|
246
|
+
headers,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
142
249
|
try {
|
|
143
250
|
const body = await request.json();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
};
|
|
149
|
-
if (result.success && result.data) {
|
|
150
|
-
return new Response(JSON.stringify(result.data), {
|
|
151
|
-
status: result.statusCode,
|
|
251
|
+
const sanitized = sanitizeClaudeRequest(body);
|
|
252
|
+
if (!sanitized.ok) {
|
|
253
|
+
return new Response(JSON.stringify({ error: "Invalid request body" }), {
|
|
254
|
+
status: 400,
|
|
152
255
|
headers,
|
|
153
256
|
});
|
|
154
257
|
}
|
|
155
|
-
|
|
156
|
-
|
|
258
|
+
const result = await invokeBedrock(sanitized.body);
|
|
259
|
+
if (result.success && result.data) {
|
|
260
|
+
return new Response(JSON.stringify(result.data), {
|
|
157
261
|
status: result.statusCode,
|
|
158
262
|
headers,
|
|
159
263
|
});
|
|
160
264
|
}
|
|
265
|
+
return new Response(JSON.stringify({ error: "Bedrock request failed" }), {
|
|
266
|
+
status: result.statusCode >= 500 ? 502 : result.statusCode,
|
|
267
|
+
headers,
|
|
268
|
+
});
|
|
161
269
|
}
|
|
162
|
-
catch
|
|
163
|
-
return new Response(JSON.stringify({
|
|
164
|
-
error: "Invalid request",
|
|
165
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
166
|
-
}), {
|
|
270
|
+
catch {
|
|
271
|
+
return new Response(JSON.stringify({ error: "Invalid request" }), {
|
|
167
272
|
status: 400,
|
|
168
|
-
headers
|
|
273
|
+
headers,
|
|
169
274
|
});
|
|
170
275
|
}
|
|
171
276
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,4 +7,9 @@ export { BedrockPOST } from "./bedrock-handler.js";
|
|
|
7
7
|
export { BedrockEmbeddingPOST } from "./bedrock-embedding-handler.js";
|
|
8
8
|
export { createOpenSearchProxyHandler, DEFAULT_OPENSEARCH_PROXY_PATH, } from "./opensearch-handler.js";
|
|
9
9
|
export type { VectorSearchProxyRequest, VectorSearchProxyResponse, } from "./opensearch-handler.js";
|
|
10
|
+
export { attachSarvamSttRelay } from "./sarvam-stt-relay.js";
|
|
11
|
+
export type { SarvamSttRelayOptions } from "./sarvam-stt-relay.js";
|
|
12
|
+
export { evaluateGate, isOriginAllowed, getClientId, checkRateLimit, isTrustProxyEnabled, parseAllowedOrigins, } from "./security.js";
|
|
13
|
+
export type { GateRequestLike, GateOptions, GateResult } from "./security.js";
|
|
14
|
+
export { SARVAM_TTS_LANGUAGES, SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE, } from "../languages.js";
|
|
10
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EACL,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAGtE,OAAO,EACL,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAInE,OAAO,EACL,YAAY,EACZ,eAAe,EACf,WAAW,EACX,cAAc,EACd,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG9E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -6,3 +6,6 @@ export { POST } from "./azure-speech-handler.js";
|
|
|
6
6
|
export { BedrockPOST } from "./bedrock-handler.js";
|
|
7
7
|
export { BedrockEmbeddingPOST } from "./bedrock-embedding-handler.js";
|
|
8
8
|
export { createOpenSearchProxyHandler, DEFAULT_OPENSEARCH_PROXY_PATH, } from "./opensearch-handler.js";
|
|
9
|
+
export { attachSarvamSttRelay } from "./sarvam-stt-relay.js";
|
|
10
|
+
export { evaluateGate, isOriginAllowed, getClientId, checkRateLimit, isTrustProxyEnabled, parseAllowedOrigins, } from "./security.js";
|
|
11
|
+
export { SARVAM_TTS_LANGUAGES, SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE, } from "../languages.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opensearch-env-handler.d.ts","sourceRoot":"","sources":["../../src/server/opensearch-env-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"opensearch-env-handler.d.ts","sourceRoot":"","sources":["../../src/server/opensearch-env-handler.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAgB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGnE,KAAK,cAAc,GAAG,eAAe,GAAG;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAC;IAC7C,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC1C,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC1B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC;CAC7D,CAAC;AAuCF,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Hf;AAMD,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
|
|
@@ -1,32 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const now = Date.now();
|
|
4
|
-
const record = rateLimitMap.get(identifier);
|
|
5
|
-
if (!record || now > record.resetTime) {
|
|
6
|
-
rateLimitMap.set(identifier, {
|
|
7
|
-
count: 1,
|
|
8
|
-
resetTime: now + windowMs,
|
|
9
|
-
});
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
if (record.count >= maxRequests) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
record.count++;
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
function getClientIdentifier(req) {
|
|
19
|
-
const forwardedFor = req.headers?.["x-forwarded-for"];
|
|
20
|
-
if (forwardedFor) {
|
|
21
|
-
const ip = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
22
|
-
return ip.split(",")[0].trim();
|
|
23
|
-
}
|
|
24
|
-
const realIp = req.headers?.["x-real-ip"];
|
|
25
|
-
if (realIp) {
|
|
26
|
-
return Array.isArray(realIp) ? realIp[0] : realIp;
|
|
27
|
-
}
|
|
28
|
-
return req.socket?.remoteAddress || "unknown";
|
|
29
|
-
}
|
|
1
|
+
import { evaluateGate } from "./security.js";
|
|
2
|
+
const SEARCH_SOURCE_FIELDS = ["name", "path", "keywords", "description"];
|
|
30
3
|
let opensearchClient = null;
|
|
31
4
|
async function getOpenSearchClient() {
|
|
32
5
|
if (!opensearchClient) {
|
|
@@ -56,12 +29,13 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
56
29
|
res.status(405).json({ error: "Method not allowed" });
|
|
57
30
|
return;
|
|
58
31
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
32
|
+
const gate = evaluateGate(req, {
|
|
33
|
+
maxRequests: 20,
|
|
34
|
+
windowMs: 60 * 1000,
|
|
35
|
+
bucket: "ossearch",
|
|
36
|
+
});
|
|
37
|
+
if (!gate.ok) {
|
|
38
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
65
39
|
return;
|
|
66
40
|
}
|
|
67
41
|
const { query, vector, size = 10 } = req.body || {};
|
|
@@ -122,14 +96,15 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
122
96
|
body: {
|
|
123
97
|
size: Math.min(size, 100),
|
|
124
98
|
query: {
|
|
125
|
-
|
|
99
|
+
simple_query_string: {
|
|
126
100
|
query: query,
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
fields: ["name^3", "keywords^2", "description", "text"],
|
|
102
|
+
default_operator: "and",
|
|
103
|
+
flags: "NONE",
|
|
129
104
|
lenient: true,
|
|
130
105
|
},
|
|
131
106
|
},
|
|
132
|
-
_source:
|
|
107
|
+
_source: SEARCH_SOURCE_FIELDS,
|
|
133
108
|
},
|
|
134
109
|
});
|
|
135
110
|
}
|
|
@@ -146,10 +121,7 @@ export async function openSearchSearchHandler(req, res) {
|
|
|
146
121
|
}
|
|
147
122
|
catch (error) {
|
|
148
123
|
console.error("[VoiceNavigation] OpenSearch search error:", error);
|
|
149
|
-
res.status(500).json({
|
|
150
|
-
error: "Search failed",
|
|
151
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
152
|
-
});
|
|
124
|
+
res.status(500).json({ error: "Search failed" });
|
|
153
125
|
}
|
|
154
126
|
}
|
|
155
127
|
export async function openSearchSuggestHandler(req, res) {
|
|
@@ -158,12 +130,13 @@ export async function openSearchSuggestHandler(req, res) {
|
|
|
158
130
|
res.status(405).json({ error: "Method not allowed" });
|
|
159
131
|
return;
|
|
160
132
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
133
|
+
const gate = evaluateGate(req, {
|
|
134
|
+
maxRequests: 30,
|
|
135
|
+
windowMs: 60 * 1000,
|
|
136
|
+
bucket: "ossuggest",
|
|
137
|
+
});
|
|
138
|
+
if (!gate.ok) {
|
|
139
|
+
res.status(gate.status ?? 403).json({ error: gate.error });
|
|
167
140
|
return;
|
|
168
141
|
}
|
|
169
142
|
const { query } = req.body || {};
|
|
@@ -213,9 +186,6 @@ export async function openSearchSuggestHandler(req, res) {
|
|
|
213
186
|
}
|
|
214
187
|
catch (error) {
|
|
215
188
|
console.error("[VoiceNavigation] OpenSearch suggest error:", error);
|
|
216
|
-
res.status(500).json({
|
|
217
|
-
error: "Suggestion failed",
|
|
218
|
-
message: error instanceof Error ? error.message : "Unknown error",
|
|
219
|
-
});
|
|
189
|
+
res.status(500).json({ error: "Suggestion failed" });
|
|
220
190
|
}
|
|
221
191
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Server as HttpServer } from "http";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
export interface SarvamSttRelayOptions {
|
|
4
|
+
path?: string;
|
|
5
|
+
connectTimeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function attachSarvamSttRelay(server: HttpServer, options?: SarvamSttRelayOptions): WebSocketServer;
|
|
8
|
+
//# sourceMappingURL=sarvam-stt-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sarvam-stt-relay.d.ts","sourceRoot":"","sources":["../../src/server/sarvam-stt-relay.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAmB,MAAM,MAAM,CAAC;AAElE,OAAO,EAA8B,eAAe,EAAE,MAAM,IAAI,CAAC;AAiDjE,MAAM,WAAW,qBAAqB;IAEpC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAUD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,qBAA0B,GAClC,eAAe,CA2HjB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { WebSocket as NodeWebSocket, WebSocketServer } from "ws";
|
|
2
|
+
import { isOriginAllowed, getClientId, checkRateLimit } from "./security.js";
|
|
3
|
+
import { SARVAM_STT_LANGUAGES, DEFAULT_LANGUAGE } from "../languages.js";
|
|
4
|
+
const ALLOWED_STT_MODELS = new Set(["saaras:v3", "saarika:v2.5"]);
|
|
5
|
+
const ALLOWED_SAMPLE_RATES = new Set(["8000", "16000"]);
|
|
6
|
+
const ALLOWED_BOOLEANS = new Set(["true", "false"]);
|
|
7
|
+
function buildSafeUpstreamQuery(clientParams) {
|
|
8
|
+
const out = new URLSearchParams();
|
|
9
|
+
const language = clientParams.get("language-code") ?? "";
|
|
10
|
+
out.set("language-code", SARVAM_STT_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE);
|
|
11
|
+
const model = clientParams.get("model") ?? "";
|
|
12
|
+
out.set("model", ALLOWED_STT_MODELS.has(model) ? model : "saaras:v3");
|
|
13
|
+
const sampleRate = clientParams.get("sample_rate") ?? "";
|
|
14
|
+
out.set("sample_rate", ALLOWED_SAMPLE_RATES.has(sampleRate) ? sampleRate : "16000");
|
|
15
|
+
out.set("input_audio_codec", "wav");
|
|
16
|
+
for (const key of ["high_vad_sensitivity", "vad_signals", "flush_signal"]) {
|
|
17
|
+
const value = clientParams.get(key);
|
|
18
|
+
if (value !== null && ALLOWED_BOOLEANS.has(value)) {
|
|
19
|
+
out.set(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out.toString();
|
|
23
|
+
}
|
|
24
|
+
function rejectUpgrade(socket, status, reason) {
|
|
25
|
+
try {
|
|
26
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
socket.destroy();
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_PATH = "/api/speech/sarvam-stt";
|
|
33
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
34
|
+
const SARVAM_STT_WS_URL = "wss://api.sarvam.ai/speech-to-text/ws";
|
|
35
|
+
export function attachSarvamSttRelay(server, options = {}) {
|
|
36
|
+
const path = options.path ?? DEFAULT_PATH;
|
|
37
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
38
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
39
|
+
server.on("upgrade", (req, socket, head) => {
|
|
40
|
+
const requestUrl = new URL(req.url ?? "", "http://localhost");
|
|
41
|
+
if (requestUrl.pathname !== path) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!isOriginAllowed(req)) {
|
|
45
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!checkRateLimit(`stt:${getClientId(req)}`, 15, 60 * 1000)) {
|
|
49
|
+
rejectUpgrade(socket, 429, "Too Many Requests");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const apiKey = process.env.SARVAM_API_KEY;
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
console.error("[VoiceNavigation] Missing SARVAM_API_KEY. Set it in your .env file to enable the Sarvam STT relay.");
|
|
55
|
+
socket.destroy();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const safeQuery = buildSafeUpstreamQuery(requestUrl.searchParams);
|
|
59
|
+
const upstream = new NodeWebSocket(`${SARVAM_STT_WS_URL}?${safeQuery}`, [`api-subscription-key.${apiKey}`], { headers: { "Api-Subscription-Key": apiKey } });
|
|
60
|
+
const connectTimeout = setTimeout(() => {
|
|
61
|
+
upstream.terminate();
|
|
62
|
+
}, connectTimeoutMs);
|
|
63
|
+
upstream.once("open", () => {
|
|
64
|
+
clearTimeout(connectTimeout);
|
|
65
|
+
wss.handleUpgrade(req, socket, head, (clientSocket) => {
|
|
66
|
+
let closedByClient = false;
|
|
67
|
+
let closedByUpstream = false;
|
|
68
|
+
upstream.on("message", (data, isBinary) => {
|
|
69
|
+
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
70
|
+
clientSocket.send(data, { binary: isBinary });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
upstream.on("close", () => {
|
|
74
|
+
closedByUpstream = true;
|
|
75
|
+
if (!closedByClient &&
|
|
76
|
+
clientSocket.readyState === clientSocket.OPEN) {
|
|
77
|
+
clientSocket.close();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
upstream.on("error", (error) => {
|
|
81
|
+
console.error("[VoiceNavigation] Sarvam STT upstream error:", error);
|
|
82
|
+
if (clientSocket.readyState === clientSocket.OPEN) {
|
|
83
|
+
clientSocket.close();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
clientSocket.on("message", (data, isBinary) => {
|
|
87
|
+
if (upstream.readyState === upstream.OPEN) {
|
|
88
|
+
upstream.send(data, { binary: isBinary });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
clientSocket.on("close", () => {
|
|
92
|
+
closedByClient = true;
|
|
93
|
+
if (!closedByUpstream && upstream.readyState === upstream.OPEN) {
|
|
94
|
+
upstream.close();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
clientSocket.on("error", () => {
|
|
98
|
+
if (upstream.readyState === upstream.OPEN) {
|
|
99
|
+
upstream.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
upstream.once("error", (error) => {
|
|
105
|
+
clearTimeout(connectTimeout);
|
|
106
|
+
console.error("[VoiceNavigation] Failed to connect to Sarvam STT upstream:", error);
|
|
107
|
+
socket.destroy();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
return wss;
|
|
111
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface GateRequestLike {
|
|
2
|
+
method?: string;
|
|
3
|
+
headers?: {
|
|
4
|
+
[key: string]: string | string[] | undefined;
|
|
5
|
+
};
|
|
6
|
+
socket?: {
|
|
7
|
+
remoteAddress?: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface GateOptions {
|
|
11
|
+
maxRequests: number;
|
|
12
|
+
windowMs: number;
|
|
13
|
+
bucket?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface GateResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
status?: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function isTrustProxyEnabled(): boolean;
|
|
21
|
+
export declare function parseAllowedOrigins(): string[];
|
|
22
|
+
export declare function getClientId(req: GateRequestLike): string;
|
|
23
|
+
export declare function isOriginAllowed(req: GateRequestLike): boolean;
|
|
24
|
+
export declare function checkRateLimit(identifier: string, maxRequests: number, windowMs: number): boolean;
|
|
25
|
+
export declare function evaluateGate(req: GateRequestLike, options: GateOptions): GateResult;
|
|
26
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/server/security.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IAC3D,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAE1B,WAAW,EAAE,MAAM,CAAC;IAEpB,QAAQ,EAAE,MAAM,CAAC;IAKjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4BD,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAGD,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAK9C;AAOD,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAYxD;AAQD,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAqB7D;AAMD,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CA0BT;AAMD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,GACnB,UAAU,CAWZ"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const rateLimitMap = new Map();
|
|
2
|
+
const MAX_TRACKED_CLIENTS = 20000;
|
|
3
|
+
function headerString(value) {
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return value[0] ?? "";
|
|
6
|
+
}
|
|
7
|
+
return value ?? "";
|
|
8
|
+
}
|
|
9
|
+
function hostOf(urlOrOrigin) {
|
|
10
|
+
try {
|
|
11
|
+
return new URL(urlOrOrigin).host;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isTrustProxyEnabled() {
|
|
18
|
+
return String(process.env.VOICE_NAV_TRUST_PROXY || "").toLowerCase() === "true";
|
|
19
|
+
}
|
|
20
|
+
export function parseAllowedOrigins() {
|
|
21
|
+
return String(process.env.VOICE_NAV_ALLOWED_ORIGINS || "")
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((entry) => entry.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
export function getClientId(req) {
|
|
27
|
+
if (isTrustProxyEnabled()) {
|
|
28
|
+
const forwardedFor = headerString(req.headers?.["x-forwarded-for"]);
|
|
29
|
+
if (forwardedFor) {
|
|
30
|
+
return forwardedFor.split(",")[0].trim();
|
|
31
|
+
}
|
|
32
|
+
const realIp = headerString(req.headers?.["x-real-ip"]);
|
|
33
|
+
if (realIp) {
|
|
34
|
+
return realIp;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return req.socket?.remoteAddress || "unknown";
|
|
38
|
+
}
|
|
39
|
+
export function isOriginAllowed(req) {
|
|
40
|
+
const origin = headerString(req.headers?.origin);
|
|
41
|
+
const referer = headerString(req.headers?.referer);
|
|
42
|
+
const host = headerString(req.headers?.host);
|
|
43
|
+
const candidateHost = origin
|
|
44
|
+
? hostOf(origin)
|
|
45
|
+
: referer
|
|
46
|
+
? hostOf(referer)
|
|
47
|
+
: null;
|
|
48
|
+
if (!candidateHost) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const allowed = parseAllowedOrigins();
|
|
52
|
+
if (allowed.length > 0) {
|
|
53
|
+
return allowed.some((entry) => (hostOf(entry) || entry) === candidateHost);
|
|
54
|
+
}
|
|
55
|
+
return host ? candidateHost === host : false;
|
|
56
|
+
}
|
|
57
|
+
export function checkRateLimit(identifier, maxRequests, windowMs) {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (rateLimitMap.size > MAX_TRACKED_CLIENTS) {
|
|
60
|
+
for (const [key, record] of rateLimitMap) {
|
|
61
|
+
if (now > record.resetTime) {
|
|
62
|
+
rateLimitMap.delete(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (rateLimitMap.size > MAX_TRACKED_CLIENTS) {
|
|
66
|
+
rateLimitMap.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const record = rateLimitMap.get(identifier);
|
|
70
|
+
if (!record || now > record.resetTime) {
|
|
71
|
+
rateLimitMap.set(identifier, { count: 1, resetTime: now + windowMs });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (record.count >= maxRequests) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
record.count++;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
export function evaluateGate(req, options) {
|
|
81
|
+
if (!isOriginAllowed(req)) {
|
|
82
|
+
return { ok: false, status: 403, error: "Origin not allowed" };
|
|
83
|
+
}
|
|
84
|
+
const key = `${options.bucket ?? "default"}:${getClientId(req)}`;
|
|
85
|
+
if (!checkRateLimit(key, options.maxRequests, options.windowMs)) {
|
|
86
|
+
return { ok: false, status: 429, error: "Too many requests" };
|
|
87
|
+
}
|
|
88
|
+
return { ok: true };
|
|
89
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bedrock.d.ts","sourceRoot":"","sources":["../../src/services/bedrock.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAE5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;
|
|
1
|
+
{"version":3,"file":"bedrock.d.ts","sourceRoot":"","sources":["../../src/services/bedrock.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,aAAa,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,WAAW,aAAa;IAE5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAiED,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,gBAAgB,CAAgB;IACxC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,oBAAoB,CAAuB;IACnD,OAAO,CAAC,QAAQ,CAAkB;gBAEtB,MAAM,EAAE,aAAa;IA6CjC,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAO7C,OAAO,CAAC,iBAAiB;IA+BnB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAWtD,qBAAqB;YA2ErB,mBAAmB;IAyEjC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc;IAO9C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAWlC,iBAAiB;YAkFjB,eAAe;IAwFvB,uBAAuB,CAC3B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC;YA2BJ,wBAAwB;YAwExB,sBAAsB;CA+DrC"}
|
package/dist/services/bedrock.js
CHANGED
|
@@ -11,12 +11,24 @@ CRITICAL RULES:
|
|
|
11
11
|
6. FOR PAGE NAVIGATION, USE THE EXACT "navigate_<id>" ACTION PROVIDED FOR THAT PAGE
|
|
12
12
|
7. IF UNCERTAIN, RESPOND WITH {"action": "unknown"}
|
|
13
13
|
|
|
14
|
+
MULTILINGUAL INPUT:
|
|
15
|
+
- The user may speak in ANY supported Indian language (e.g. Hindi, Tamil, Telugu, Bengali, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, Assamese, Urdu, and others), or mix languages.
|
|
16
|
+
- Understand the MEANING of the command regardless of the input language and map it to the correct action.
|
|
17
|
+
- The "action" value, action IDs, and "navigate_<id>" page IDs are ALWAYS in English/ASCII and MUST NOT be translated or transliterated.
|
|
18
|
+
- For "query" (search) and "text" (type) fields, preserve the user's own words as spoken.
|
|
19
|
+
|
|
14
20
|
RESPONSE FORMAT:
|
|
15
21
|
- Core actions: {"action": "action_name"}
|
|
16
22
|
- Search: {"action": "search_content", "query": "keywords"}
|
|
17
23
|
- Type text: {"action": "type_text", "text": "text to type"}
|
|
18
24
|
- Page navigation: {"action": "navigate_<page_id>"}
|
|
19
25
|
|
|
26
|
+
MULTILINGUAL EXAMPLES:
|
|
27
|
+
- User says "नीचे स्क्रॉल करो" (Hindi: scroll down) → {"action": "scroll_down"}
|
|
28
|
+
- User says "डैशबोर्ड खोलो" (Hindi: open dashboard) → {"action": "navigate_dashboard"}
|
|
29
|
+
- User says "மேலே செல்" (Tamil: go up) → {"action": "scroll_up"}
|
|
30
|
+
- User says "వెనక్కి వెళ్ళు" (Telugu: go back) → {"action": "go_back"}
|
|
31
|
+
|
|
20
32
|
EXAMPLES:
|
|
21
33
|
- User says "go to dashboard" → {"action": "navigate_dashboard"}
|
|
22
34
|
- User says "show me the team page" → {"action": "navigate_team"}
|
|
@@ -362,6 +374,7 @@ export class BedrockService {
|
|
|
362
374
|
Generate brief, natural, conversational feedback messages (maximum 10 words).
|
|
363
375
|
Be specific and clear about what action was performed.
|
|
364
376
|
Use the language: ${language}
|
|
377
|
+
Never output internal action identifiers, underscores, technical codes, or JSON — describe the action only in plain, everyday words a non-technical listener would understand.
|
|
365
378
|
Respond with ONLY the feedback message text, no JSON, no explanations.`;
|
|
366
379
|
const userPrompt = prompt;
|
|
367
380
|
console.log("[Bedrock] Using proxy mode:", this.useProxy);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SarvamSpeechConfig {
|
|
2
|
+
ttsUrl?: string;
|
|
3
|
+
sttUrl?: string;
|
|
4
|
+
sttModel?: "saaras:v3" | "saarika:v2.5";
|
|
5
|
+
ttsModel?: "bulbul:v3" | "bulbul:v2";
|
|
6
|
+
speaker?: string;
|
|
7
|
+
sttSampleRate?: 8000 | 16000;
|
|
8
|
+
ttsSampleRate?: 8000 | 16000 | 22050 | 24000 | 32000 | 44100 | 48000;
|
|
9
|
+
}
|
|
10
|
+
export interface SarvamSttHandlers {
|
|
11
|
+
onPartial?: (text: string) => void;
|
|
12
|
+
onFinal?: (text: string) => void;
|
|
13
|
+
onSpeechStart?: () => void;
|
|
14
|
+
onSpeechEnd?: () => void;
|
|
15
|
+
onError?: (error: Error) => void;
|
|
16
|
+
shouldSendAudio?: () => boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class SarvamSpeechService {
|
|
19
|
+
private readonly ttsUrl;
|
|
20
|
+
private readonly sttUrl;
|
|
21
|
+
private readonly sttModel;
|
|
22
|
+
private readonly ttsModel;
|
|
23
|
+
private readonly speaker;
|
|
24
|
+
private readonly sttSampleRate;
|
|
25
|
+
private readonly ttsSampleRate;
|
|
26
|
+
private sttSocket;
|
|
27
|
+
private sttContext;
|
|
28
|
+
private sttMediaStream;
|
|
29
|
+
private sttSourceNode;
|
|
30
|
+
private sttProcessorNode;
|
|
31
|
+
private audioElement;
|
|
32
|
+
private currentBlobUrl;
|
|
33
|
+
private speechGeneration;
|
|
34
|
+
private listeningGeneration;
|
|
35
|
+
constructor(config?: SarvamSpeechConfig);
|
|
36
|
+
startListening(language: string, handlers: SarvamSttHandlers): Promise<void>;
|
|
37
|
+
private sendFlush;
|
|
38
|
+
stopListening(): Promise<void>;
|
|
39
|
+
speak(text: string, opts?: {
|
|
40
|
+
language?: string;
|
|
41
|
+
onAudioStart?: () => void;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
stopSpeaking(): void;
|
|
44
|
+
dispose(): Promise<void>;
|
|
45
|
+
private cleanupAudioElement;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=sarvam-speech.d.ts.map
|