mini-chat-bot-widget 0.2.0 → 0.8.0
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/dist/chat-widget.esm.js +2860 -80
- package/dist/chat-widget.esm.min.js +1 -1
- package/dist/chat-widget.umd.js +2860 -80
- package/dist/chat-widget.umd.min.js +1 -1
- package/index.html +56 -0
- package/managed_context/metadata.json +1 -0
- package/package.json +13 -7
- package/src/audio-chat-screen.js +1408 -0
- package/src/bhashiniApi.js +363 -0
- package/src/chat-widget.js +2490 -86
- package/src/text-chat-screen.js +1286 -0
- package/test_suite_analysis/metadata.json +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
export class BhashiniFrontend {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.apiKey = '23588533c1-d990-4a7d-b052-d970c886147e';
|
|
4
|
+
this.userId = '749bd0b0c65e4d17a372be0ad92af981';
|
|
5
|
+
this.callbackUrl = 'https://meity-auth.ulcacontrib.org/ulca/apis/v0/model/getModelsPipeline';
|
|
6
|
+
this.inferenceUrl = null;
|
|
7
|
+
this.inferenceHeaders = {};
|
|
8
|
+
this.asrServiceId = {
|
|
9
|
+
'hi': 'ai4bharat/conformer-hi-gpu--t4',
|
|
10
|
+
'en': 'ai4bharat/whisper-medium-en--gpu--t4'
|
|
11
|
+
};
|
|
12
|
+
this.ttsServiceId = {
|
|
13
|
+
'hi': 'ai4bharat/indic-tts-coqui-indo_aryan-gpu--t4',
|
|
14
|
+
'en': 'ai4bharat/indic-tts-coqui-misc-gpu--t4'
|
|
15
|
+
};
|
|
16
|
+
this.translationServiceId = 'ai4bharat/indictrans-v2-all-gpu--t4';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initialize() {
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(this.callbackUrl, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'userID': this.userId,
|
|
26
|
+
'ulcaApiKey': this.apiKey
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
pipelineTasks: [{ taskType: 'asr' }],
|
|
30
|
+
pipelineRequestConfig: {
|
|
31
|
+
pipelineId: '64392f96daac500b55c543cd'
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
|
|
38
|
+
if (data.pipelineInferenceAPIEndPoint) {
|
|
39
|
+
this.inferenceUrl = data.pipelineInferenceAPIEndPoint.callbackUrl;
|
|
40
|
+
const keyName = data.pipelineInferenceAPIEndPoint.inferenceApiKey.name;
|
|
41
|
+
const keyValue = data.pipelineInferenceAPIEndPoint.inferenceApiKey.value;
|
|
42
|
+
this.inferenceHeaders = {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
[keyName]: keyValue
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
// Fallback to direct endpoint
|
|
48
|
+
this.inferenceUrl = 'https://dhruva-api.bhashini.gov.in/services/inference/pipeline';
|
|
49
|
+
this.inferenceHeaders = {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'userID': this.userId,
|
|
52
|
+
'ulcaApiKey': this.apiKey
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Failed to initialize Bhashini:', error);
|
|
59
|
+
// Use fallback endpoint
|
|
60
|
+
this.inferenceUrl = 'https://dhruva-api.bhashini.gov.in/services/inference/pipeline';
|
|
61
|
+
this.inferenceHeaders = {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
'userID': this.userId,
|
|
64
|
+
'ulcaApiKey': this.apiKey
|
|
65
|
+
};
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async convertToWav(audioBlob) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
73
|
+
const reader = new FileReader();
|
|
74
|
+
|
|
75
|
+
reader.onload = async (e) => {
|
|
76
|
+
try {
|
|
77
|
+
const arrayBuffer = e.target.result;
|
|
78
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
79
|
+
|
|
80
|
+
// Convert to mono 16kHz
|
|
81
|
+
const offlineContext = new OfflineAudioContext(1, audioBuffer.duration * 16000, 16000);
|
|
82
|
+
const source = offlineContext.createBufferSource();
|
|
83
|
+
source.buffer = audioBuffer;
|
|
84
|
+
source.connect(offlineContext.destination);
|
|
85
|
+
source.start();
|
|
86
|
+
|
|
87
|
+
const renderedBuffer = await offlineContext.startRendering();
|
|
88
|
+
|
|
89
|
+
// Convert to WAV
|
|
90
|
+
const wav = this.audioBufferToWav(renderedBuffer);
|
|
91
|
+
const wavBlob = new Blob([wav], { type: 'audio/wav' });
|
|
92
|
+
|
|
93
|
+
// Convert to base64
|
|
94
|
+
const wavReader = new FileReader();
|
|
95
|
+
wavReader.onloadend = () => {
|
|
96
|
+
const base64 = wavReader.result.split(',')[1];
|
|
97
|
+
resolve(base64);
|
|
98
|
+
};
|
|
99
|
+
wavReader.readAsDataURL(wavBlob);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
reject(error);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
reader.onerror = reject;
|
|
106
|
+
reader.readAsArrayBuffer(audioBlob);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
audioBufferToWav(buffer) {
|
|
111
|
+
const length = buffer.length * buffer.numberOfChannels * 2 + 44;
|
|
112
|
+
const arrayBuffer = new ArrayBuffer(length);
|
|
113
|
+
const view = new DataView(arrayBuffer);
|
|
114
|
+
const channels = [];
|
|
115
|
+
let offset = 0;
|
|
116
|
+
let pos = 0;
|
|
117
|
+
|
|
118
|
+
// Write WAV header
|
|
119
|
+
const setUint16 = (data) => {
|
|
120
|
+
view.setUint16(pos, data, true);
|
|
121
|
+
pos += 2;
|
|
122
|
+
};
|
|
123
|
+
const setUint32 = (data) => {
|
|
124
|
+
view.setUint32(pos, data, true);
|
|
125
|
+
pos += 4;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// "RIFF" chunk descriptor
|
|
129
|
+
setUint32(0x46464952); // "RIFF"
|
|
130
|
+
setUint32(length - 8); // file length - 8
|
|
131
|
+
setUint32(0x45564157); // "WAVE"
|
|
132
|
+
|
|
133
|
+
// "fmt " sub-chunk
|
|
134
|
+
setUint32(0x20746d66); // "fmt "
|
|
135
|
+
setUint32(16); // SubChunk1Size = 16
|
|
136
|
+
setUint16(1); // AudioFormat = 1 (PCM)
|
|
137
|
+
setUint16(buffer.numberOfChannels);
|
|
138
|
+
setUint32(buffer.sampleRate);
|
|
139
|
+
setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // byte rate
|
|
140
|
+
setUint16(buffer.numberOfChannels * 2); // block align
|
|
141
|
+
setUint16(16); // bits per sample
|
|
142
|
+
|
|
143
|
+
// "data" sub-chunk
|
|
144
|
+
setUint32(0x61746164); // "data"
|
|
145
|
+
setUint32(length - pos - 4); // SubChunk2Size
|
|
146
|
+
|
|
147
|
+
// Write interleaved data
|
|
148
|
+
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
|
149
|
+
channels.push(buffer.getChannelData(i));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
while (pos < length) {
|
|
153
|
+
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
|
154
|
+
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
|
|
155
|
+
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
|
|
156
|
+
view.setInt16(pos, sample, true);
|
|
157
|
+
pos += 2;
|
|
158
|
+
}
|
|
159
|
+
offset++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return arrayBuffer;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async speechToText(
|
|
166
|
+
base64Audio,
|
|
167
|
+
sourceLanguage = 'auto',
|
|
168
|
+
targetLanguage = 'hi'
|
|
169
|
+
) {
|
|
170
|
+
if (!this.inferenceUrl) {
|
|
171
|
+
await this.initialize();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const pipelineTasks = [
|
|
175
|
+
{
|
|
176
|
+
taskType: 'asr',
|
|
177
|
+
config: {
|
|
178
|
+
language: { sourceLanguage },
|
|
179
|
+
serviceId:
|
|
180
|
+
this.asrServiceId[sourceLanguage] ||
|
|
181
|
+
this.asrServiceId['hi'],
|
|
182
|
+
audioFormat: 'wav',
|
|
183
|
+
samplingRate: 16000
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// Only translate if languages differ
|
|
189
|
+
if (sourceLanguage !== targetLanguage) {
|
|
190
|
+
pipelineTasks.push({
|
|
191
|
+
taskType: 'translation',
|
|
192
|
+
config: {
|
|
193
|
+
sourceLanguage:
|
|
194
|
+
sourceLanguage === 'auto' ? undefined : sourceLanguage,
|
|
195
|
+
targetLanguage
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const response = await fetch(this.inferenceUrl, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: this.inferenceHeaders,
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
pipelineTasks,
|
|
205
|
+
inputData: {
|
|
206
|
+
audio: [{ audioContent: base64Audio }]
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
|
|
217
|
+
// If translated, return translation
|
|
218
|
+
if (pipelineTasks.length > 1) {
|
|
219
|
+
return data.pipelineResponse[1].output[0].target;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Otherwise return raw transcription
|
|
223
|
+
return data.pipelineResponse[0].output[0].source;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async textToSpeech(text, language = 'en', gender = 'female') {
|
|
228
|
+
if (!this.inferenceUrl) {
|
|
229
|
+
await this.initialize();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ✅ Clean the text first
|
|
233
|
+
const cleanedText = text.replace(/!/g, ".");
|
|
234
|
+
|
|
235
|
+
const response = await fetch(this.inferenceUrl, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: this.inferenceHeaders,
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
pipelineTasks: [
|
|
240
|
+
{
|
|
241
|
+
taskType: "tts",
|
|
242
|
+
config: {
|
|
243
|
+
language: { sourceLanguage: language },
|
|
244
|
+
serviceId: this.ttsServiceId[language] || this.ttsServiceId["en"],
|
|
245
|
+
samplingRate: 8000,
|
|
246
|
+
gender: gender,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
inputData: {
|
|
251
|
+
input: [{ source: cleanedText }],
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`TTS API error: ${response.status} ${response.statusText}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const data = await response.json();
|
|
261
|
+
return data.pipelineResponse[0].audio[0].audioContent;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async translateText(text, sourceLanguage, targetLanguage) {
|
|
266
|
+
if (!this.inferenceUrl) {
|
|
267
|
+
await this.initialize();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const response = await fetch(this.inferenceUrl, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: this.inferenceHeaders,
|
|
273
|
+
body: JSON.stringify({
|
|
274
|
+
pipelineTasks: [{
|
|
275
|
+
taskType: 'translation',
|
|
276
|
+
config: {
|
|
277
|
+
language: {
|
|
278
|
+
sourceLanguage: sourceLanguage,
|
|
279
|
+
targetLanguage: targetLanguage
|
|
280
|
+
},
|
|
281
|
+
serviceId: this.translationServiceId
|
|
282
|
+
}
|
|
283
|
+
}],
|
|
284
|
+
inputData: {
|
|
285
|
+
input: [{ source: text }]
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw new Error(`Translation API error: ${response.status} ${response.statusText}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const data = await response.json();
|
|
295
|
+
return data.pipelineResponse[0].output[0].target;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async speechToEnglish(base64Audio, sourceLanguage = 'hi') {
|
|
299
|
+
// Step 1: Transcribe audio to source language text
|
|
300
|
+
const transcribedText = await this.speechToText(base64Audio, sourceLanguage);
|
|
301
|
+
|
|
302
|
+
// Step 2: If already English, return as is
|
|
303
|
+
if (sourceLanguage === 'en') {
|
|
304
|
+
return { transcription: transcribedText, translation: transcribedText };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Step 3: Translate to English
|
|
308
|
+
const translatedText = await this.translateText(transcribedText, sourceLanguage, 'en');
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
transcription: transcribedText,
|
|
312
|
+
translation: translatedText
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async speechToHindi(base64Audio, sourceLanguage = 'en') {
|
|
317
|
+
// Step 1: Transcribe audio to source language text
|
|
318
|
+
const transcribedText = await this.speechToText(base64Audio, sourceLanguage);
|
|
319
|
+
|
|
320
|
+
// Step 2: If already Hindi, return as is
|
|
321
|
+
if (sourceLanguage === 'hi') {
|
|
322
|
+
return { transcription: transcribedText, translation: transcribedText };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Step 3: Translate to Hindi
|
|
326
|
+
const translatedText = await this.translateText(transcribedText, sourceLanguage, 'hi');
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
transcription: transcribedText,
|
|
330
|
+
translation: translatedText
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
async transcribeSpeechToEnglish(base64Audio, sourceLanguage = "hi") {
|
|
336
|
+
// Step 1: Transcribe audio to source language text
|
|
337
|
+
const transcribedText = await this.speechToText(
|
|
338
|
+
base64Audio,
|
|
339
|
+
sourceLanguage,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// Step 2: If already English, return as is
|
|
343
|
+
if (sourceLanguage === "en") {
|
|
344
|
+
return {
|
|
345
|
+
transcription: transcribedText,
|
|
346
|
+
translation: transcribedText,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Step 3: Translate to English
|
|
351
|
+
const translatedText = await this.translateText(
|
|
352
|
+
transcribedText,
|
|
353
|
+
sourceLanguage,
|
|
354
|
+
"en",
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
transcription: transcribedText,
|
|
359
|
+
translation: translatedText,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
}
|