reneco-advanced-input-module 0.0.21 → 0.0.23
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/cjs/loader.cjs.js +1 -1
- package/dist/cjs/voice-input-module.cjs.entry.js +481 -154
- package/dist/cjs/voice-input-module.cjs.entry.js.map +1 -1
- package/dist/cjs/voice-input-module.cjs.js +1 -1
- package/dist/cjs/voice-input-module.entry.cjs.js.map +1 -1
- package/dist/collection/components/voice-input-module/voice-input-module.js +295 -96
- package/dist/collection/components/voice-input-module/voice-input-module.js.map +1 -1
- package/dist/collection/services/audio-recorder.service.js +61 -44
- package/dist/collection/services/audio-recorder.service.js.map +1 -1
- package/dist/collection/services/llm.service.js +137 -8
- package/dist/collection/services/llm.service.js.map +1 -1
- package/dist/collection/services/speech-to-text.service.js +39 -5
- package/dist/collection/services/speech-to-text.service.js.map +1 -1
- package/dist/collection/types/service-providers.types.js +9 -1
- package/dist/collection/types/service-providers.types.js.map +1 -1
- package/dist/components/voice-input-module.js +489 -155
- package/dist/components/voice-input-module.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/esm/voice-input-module.entry.js +481 -154
- package/dist/esm/voice-input-module.entry.js.map +1 -1
- package/dist/esm/voice-input-module.js +1 -1
- package/dist/types/components/voice-input-module/voice-input-module.d.ts +6 -0
- package/dist/types/components.d.ts +18 -0
- package/dist/types/services/audio-recorder.service.d.ts +5 -0
- package/dist/types/services/llm.service.d.ts +22 -0
- package/dist/types/services/speech-to-text.service.d.ts +8 -0
- package/dist/types/types/service-providers.types.d.ts +6 -2
- package/dist/voice-input-module/p-920964b2.entry.js +3 -0
- package/dist/voice-input-module/p-920964b2.entry.js.map +1 -0
- package/dist/voice-input-module/voice-input-module.entry.esm.js.map +1 -1
- package/dist/voice-input-module/voice-input-module.esm.js +1 -1
- package/package.json +1 -1
- package/readme.md +81 -0
- package/www/build/p-920964b2.entry.js +3 -0
- package/www/build/p-920964b2.entry.js.map +1 -0
- package/www/build/p-cbf8974a.js +2 -0
- package/www/build/voice-input-module.entry.esm.js.map +1 -1
- package/www/build/voice-input-module.esm.js +1 -1
- package/www/index.html +12 -1
- package/dist/voice-input-module/p-4e449895.entry.js +0 -3
- package/dist/voice-input-module/p-4e449895.entry.js.map +0 -1
- package/www/build/p-3a11e8d2.js +0 -2
- package/www/build/p-4e449895.entry.js +0 -3
- package/www/build/p-4e449895.entry.js.map +0 -1
|
@@ -2,69 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
var index = require('./index-BTSzTkSZ.js');
|
|
4
4
|
|
|
5
|
+
const TRANSCRIPTION_MODELS = {
|
|
6
|
+
openai: ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe', 'whisper-1'],
|
|
7
|
+
mistral: ['voxtral-mini-latest', 'voxtral-mini-transcribe-realtime-latest'],
|
|
8
|
+
};
|
|
9
|
+
const COMPLETION_MODELS = {
|
|
10
|
+
openai: ['gpt-5', 'gpt-5-mini', 'gpt-5.5', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'gpt-5.2', 'gpt-5.3', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-pro', 'gpt-5.4-nano'],
|
|
11
|
+
anthropic: ['claude-opus-4', 'claude-sonnet-4', 'claude-haiku-4', 'claude-3.7-sonnet', 'claude-3.5-sonnet', 'claude-opus-4.7', 'claude-opus-4.6', 'claude-opus-4.5', 'claude-sonnet-4.6', 'claude-sonnet-4.5'],
|
|
12
|
+
mistral: ['mistral-large-latest', 'mistral-medium-latest', 'mistral-small-latest', 'ministral', 'mistral-nemo'],
|
|
13
|
+
};
|
|
14
|
+
|
|
5
15
|
class AudioRecorderService {
|
|
6
16
|
constructor() {
|
|
7
17
|
this.mediaRecorder = null;
|
|
8
18
|
this.audioChunks = [];
|
|
9
19
|
this.stream = null;
|
|
20
|
+
this.audioContext = null;
|
|
21
|
+
this.scriptProcessor = null;
|
|
22
|
+
this.pcmChunks = [];
|
|
23
|
+
this.sampleRate = 16000;
|
|
10
24
|
}
|
|
11
25
|
async startRecording() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
15
|
-
console.error('Failed to start recording:', 'Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
|
|
16
|
-
return; // Exit gracefully instead of throwing
|
|
17
|
-
}
|
|
18
|
-
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
19
|
-
audio: {
|
|
20
|
-
echoCancellation: true,
|
|
21
|
-
noiseSuppression: true,
|
|
22
|
-
autoGainControl: true
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
this.audioChunks = [];
|
|
26
|
-
this.mediaRecorder = new MediaRecorder(this.stream, {
|
|
27
|
-
mimeType: 'audio/webm;codecs=opus'
|
|
28
|
-
});
|
|
29
|
-
this.mediaRecorder.ondataavailable = (event) => {
|
|
30
|
-
if (event.data && event.data.size > 0) {
|
|
31
|
-
this.audioChunks.push(event.data);
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
this.mediaRecorder.start(100); // Collect data every 100ms
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
console.error('Failed to start recording:', error);
|
|
26
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
27
|
+
throw new Error('Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
|
|
38
28
|
}
|
|
29
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
30
|
+
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
|
|
31
|
+
});
|
|
32
|
+
this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
|
|
33
|
+
const source = this.audioContext.createMediaStreamSource(this.stream);
|
|
34
|
+
this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
|
|
35
|
+
this.pcmChunks = [];
|
|
36
|
+
this.scriptProcessor.onaudioprocess = (e) => {
|
|
37
|
+
const input = e.inputBuffer.getChannelData(0);
|
|
38
|
+
this.pcmChunks.push(new Float32Array(input));
|
|
39
|
+
};
|
|
40
|
+
source.connect(this.scriptProcessor);
|
|
41
|
+
this.scriptProcessor.connect(this.audioContext.destination);
|
|
39
42
|
}
|
|
40
43
|
async stopRecording() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if (!this.audioContext || !this.scriptProcessor) {
|
|
45
|
+
throw new Error('No active recording found');
|
|
46
|
+
}
|
|
47
|
+
this.scriptProcessor.disconnect();
|
|
48
|
+
await this.audioContext.close();
|
|
49
|
+
const wavBlob = this.encodeWav(this.pcmChunks, this.sampleRate);
|
|
50
|
+
this.cleanup();
|
|
51
|
+
return wavBlob;
|
|
52
|
+
}
|
|
53
|
+
encodeWav(chunks, sampleRate) {
|
|
54
|
+
const totalSamples = chunks.reduce((acc, c) => acc + c.length, 0);
|
|
55
|
+
const buffer = new ArrayBuffer(44 + totalSamples * 2);
|
|
56
|
+
const view = new DataView(buffer);
|
|
57
|
+
const writeStr = (offset, str) => {
|
|
58
|
+
for (let i = 0; i < str.length; i++)
|
|
59
|
+
view.setUint8(offset + i, str.charCodeAt(i));
|
|
60
|
+
};
|
|
61
|
+
writeStr(0, 'RIFF');
|
|
62
|
+
view.setUint32(4, 36 + totalSamples * 2, true);
|
|
63
|
+
writeStr(8, 'WAVE');
|
|
64
|
+
writeStr(12, 'fmt ');
|
|
65
|
+
view.setUint32(16, 16, true); // PCM chunk size
|
|
66
|
+
view.setUint16(20, 1, true); // PCM format
|
|
67
|
+
view.setUint16(22, 1, true); // mono
|
|
68
|
+
view.setUint32(24, sampleRate, true);
|
|
69
|
+
view.setUint32(28, sampleRate * 2, true); // byte rate
|
|
70
|
+
view.setUint16(32, 2, true); // block align
|
|
71
|
+
view.setUint16(34, 16, true); // bits per sample
|
|
72
|
+
writeStr(36, 'data');
|
|
73
|
+
view.setUint32(40, totalSamples * 2, true);
|
|
74
|
+
let offset = 44;
|
|
75
|
+
for (const chunk of chunks) {
|
|
76
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
77
|
+
const s = Math.max(-1, Math.min(1, chunk[i]));
|
|
78
|
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
79
|
+
offset += 2;
|
|
45
80
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.cleanup();
|
|
49
|
-
resolve(audioBlob);
|
|
50
|
-
};
|
|
51
|
-
this.mediaRecorder.onerror = (event) => {
|
|
52
|
-
reject(new Error(`Recording error: ${event}`));
|
|
53
|
-
};
|
|
54
|
-
this.mediaRecorder.stop();
|
|
55
|
-
});
|
|
81
|
+
}
|
|
82
|
+
return new Blob([buffer], { type: 'audio/wav' });
|
|
56
83
|
}
|
|
57
84
|
isRecording() {
|
|
58
|
-
|
|
59
|
-
return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'recording';
|
|
85
|
+
return this.scriptProcessor !== null;
|
|
60
86
|
}
|
|
61
87
|
cleanup() {
|
|
62
88
|
if (this.stream) {
|
|
63
89
|
this.stream.getTracks().forEach(track => track.stop());
|
|
64
90
|
this.stream = null;
|
|
65
91
|
}
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
92
|
+
this.audioContext = null;
|
|
93
|
+
this.scriptProcessor = null;
|
|
94
|
+
this.pcmChunks = [];
|
|
68
95
|
}
|
|
69
96
|
}
|
|
70
97
|
|
|
@@ -126,15 +153,49 @@ class WhisperSpeechToTextService {
|
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
}
|
|
156
|
+
class MistralSpeechToTextService {
|
|
157
|
+
constructor(config) {
|
|
158
|
+
this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
|
|
159
|
+
this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
|
|
160
|
+
this.model = (config === null || config === void 0 ? void 0 : config.model) || 'voxtral-mini-latest';
|
|
161
|
+
this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
|
|
162
|
+
if (!this.useProxy && !this.apiKey) {
|
|
163
|
+
throw new Error('Mistral API key is required for speech-to-text service');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async transcribe(audioContent, lang = 'en') {
|
|
167
|
+
var _a;
|
|
168
|
+
try {
|
|
169
|
+
const formData = new FormData();
|
|
170
|
+
formData.append('file', audioContent);
|
|
171
|
+
formData.append('model', this.model);
|
|
172
|
+
formData.append('language', lang);
|
|
173
|
+
const endpoint = this.useProxy ? `${this.proxyUrl}/transcribe-mistral` : 'https://api.mistral.ai/v1/audio/transcriptions';
|
|
174
|
+
const headers = {};
|
|
175
|
+
if (!this.useProxy) {
|
|
176
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
177
|
+
}
|
|
178
|
+
const response = await fetch(endpoint, { method: 'POST', headers, body: formData });
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
181
|
+
throw new Error(`Transcription failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
|
|
182
|
+
}
|
|
183
|
+
const result = await response.json();
|
|
184
|
+
return result.text || '';
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
throw new Error(`Mistral speech-to-text failed: ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
129
191
|
class SpeechToTextServiceFactory {
|
|
130
192
|
static create(config) {
|
|
131
193
|
var _a;
|
|
132
|
-
const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || '
|
|
194
|
+
const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
|
|
133
195
|
switch (provider) {
|
|
134
|
-
case '
|
|
135
|
-
|
|
136
|
-
default:
|
|
137
|
-
throw new Error(`Unsupported speech-to-text provider: ${provider}`);
|
|
196
|
+
case 'openai': return new WhisperSpeechToTextService(config.speechToText);
|
|
197
|
+
case 'mistral': return new MistralSpeechToTextService(config.speechToText);
|
|
198
|
+
default: throw new Error(`Unsupported speech-to-text provider: ${provider}`);
|
|
138
199
|
}
|
|
139
200
|
}
|
|
140
201
|
}
|
|
@@ -326,7 +387,8 @@ class OpenAILLMService {
|
|
|
326
387
|
`${field.readonly ? ', readonly' : ''}` +
|
|
327
388
|
`${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
|
|
328
389
|
`${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
|
|
329
|
-
`${field.max && field.max !== "" ? ', max=' + field.max : ''}
|
|
390
|
+
`${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
|
|
391
|
+
`${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
|
|
330
392
|
if (field.options && field.options.length > 0) {
|
|
331
393
|
if (field.options.length < 50) {
|
|
332
394
|
description += `) - options: ${field.options.join(', ')}`;
|
|
@@ -348,7 +410,8 @@ class OpenAILLMService {
|
|
|
348
410
|
`${field.readonly ? ', readonly' : ''}` +
|
|
349
411
|
`${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
|
|
350
412
|
`${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
|
|
351
|
-
`${field.max && field.max !== "" ? ', max=' + field.max : ''}
|
|
413
|
+
`${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
|
|
414
|
+
`${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
|
|
352
415
|
if (field.options && field.options.length > 0) {
|
|
353
416
|
if (field.options.length < 50) {
|
|
354
417
|
description += `) - options: ${field.options.join(', ')}`;
|
|
@@ -492,12 +555,14 @@ class OpenAILLMService {
|
|
|
492
555
|
7. Only include fields where relevant information is found
|
|
493
556
|
8. The current GMT datetime is ${new Date().toGMTString()}
|
|
494
557
|
9. Respect the constraints written between parenthesis for readonly status (readonly fields MUST NOT be filled with values), min and max values, whatever the transcription says
|
|
558
|
+
9b. CRITICAL - DATE/DATETIME OUT OF RANGE: For date and datetime fields with min and/or max constraints, if the value from the transcription falls outside the allowed range, leave the field EMPTY. NEVER clamp, adjust, or substitute a boundary date - either the date is valid and within range, or the field is left empty
|
|
495
559
|
10. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
|
|
496
560
|
11. CRITICAL - DUPLICATE FIELDS: When you see multiple fields with the same name but different IDs (e.g., "ID:field1 | Event date" and "ID:field2 | Event date"), these are DIFFERENT fields in DIFFERENT sections. The user may explicitly say which section they are filling (e.g., "for the first/second sub-form"). Listen VERY CAREFULLY to these contextual clues. Use the readonly headers to understand which section each field belongs to. If the user says "for the clinical sign, the event date is X", you must fill the Event date field that comes AFTER the "MC Clinical sign" header, NOT the first Event date you see
|
|
497
561
|
12. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
|
|
498
|
-
13. CRITICAL:
|
|
562
|
+
13. CRITICAL - UNKNOWN/NO/N-A VALUES: The interpretation of words like "Unknown", "Inconnu", "No", "Non", "N/A", "Non applicable" depends ENTIRELY on whether the field has those words as explicit options. Rule: if the user says "field X is Unknown" (or any similar phrasing) AND "Unknown" (or a close variant) exists in the options list for that field, then SELECT that option - it is a valid value. If the user says such words for a field that does NOT have them as options (string, number, date, etc.), then it means the user has no value to provide - leave the field empty, do NOT fill it with the literal text
|
|
499
563
|
14. CRITICAL: Each field has a unique ID shown as "ID:xxx" at the start of its description. You MUST include this exact ID in your response for each field you fill. The ID is the ONLY way to distinguish between fields with the same name
|
|
500
564
|
15. CRITICAL - DEFAULT VALUES: Some fields have default values shown as "default=xxx". If the user does NOT mention a specific value for that field in the transcription, you MUST return the default value. Default values should be preserved unless explicitly overridden by the user
|
|
565
|
+
16. CRITICAL - PATTERN VALIDATION: Some fields have a regex pattern shown as "pattern=xxx". The value you extract MUST match this pattern exactly. If the transcription is too ambiguous to produce a value that matches the pattern, leave the field EMPTY
|
|
501
566
|
|
|
502
567
|
Respond with JSON in this exact format: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
|
|
503
568
|
let userPrompt = `
|
|
@@ -566,10 +631,12 @@ class OpenAILLMService {
|
|
|
566
631
|
8. Return the same schema structure with 'default' values filled
|
|
567
632
|
9. The current GMT datetime is ${new Date().toGMTString()}
|
|
568
633
|
10. Respect the constraints written between parenthesis for readonly status (readonly fields MUST NOT be filled with values), min and max values, whatever the transcription says
|
|
634
|
+
10b. CRITICAL - DATE/DATETIME OUT OF RANGE: For date and datetime fields with min and/or max constraints, if the value from the transcription falls outside the allowed range, leave the field EMPTY. NEVER clamp, adjust, or substitute a boundary date - either the date is valid and within range, or the field is left empty
|
|
569
635
|
11. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
|
|
570
636
|
12. CRITICAL - DUPLICATE FIELDS: When you see multiple fields with the same name but different IDs (e.g., "ID:field1 | Event date" and "ID:field2 | Event date"), these are DIFFERENT fields in DIFFERENT sections. The user will explicitly say which section they are filling (e.g., "for the first/second sub-form", "in the anamnesis section", "for the clinical sign"). Listen VERY CAREFULLY to these contextual clues. Use the readonly headers to understand which section each field belongs to. If the user says "for the clinical sign, the event date is X", you must fill the Event date field that comes AFTER the "MC Clinical sign" header, NOT the first Event date you see
|
|
571
637
|
13. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
|
|
572
|
-
14. CRITICAL:
|
|
638
|
+
14. CRITICAL - UNKNOWN/NO/N-A VALUES: The interpretation of words like "Unknown", "Inconnu", "No", "Non", "N/A", "Non applicable" depends ENTIRELY on whether the field has those words as explicit options. Rule: if the user says "field X is Unknown" (or any similar phrasing) AND "Unknown" (or a close variant) exists in the options list for that field, then SELECT that option - it is a valid value. If the user says such words for a field that does NOT have them as options (string, number, date, etc.), then it means the user has no value to provide - leave the field empty, do NOT fill it with the literal text
|
|
639
|
+
15. CRITICAL - PATTERN VALIDATION: Some fields have a regex pattern shown as "pattern=xxx". The value you extract MUST match this pattern exactly. If the transcription is too ambiguous to produce a value that matches the pattern, leave the field EMPTY
|
|
573
640
|
|
|
574
641
|
Respond with JSON in this exact format: {"schema": {...}}`;
|
|
575
642
|
let userPrompt = `
|
|
@@ -633,15 +700,138 @@ class OpenAILLMService {
|
|
|
633
700
|
}
|
|
634
701
|
}
|
|
635
702
|
}
|
|
703
|
+
class AnthropicLLMService {
|
|
704
|
+
constructor(config) {
|
|
705
|
+
this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
|
|
706
|
+
this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
|
|
707
|
+
this.model = (config === null || config === void 0 ? void 0 : config.model) || 'claude-sonnet-4.5';
|
|
708
|
+
this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
|
|
709
|
+
if (!this.useProxy && !this.apiKey) {
|
|
710
|
+
throw new Error('Anthropic API key is required');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async fillFormFromTranscription(transcription, schema) {
|
|
714
|
+
return this.fillForm(transcription, schema);
|
|
715
|
+
}
|
|
716
|
+
async fillFormFromJson(json, schema) {
|
|
717
|
+
return this.fillForm(json, schema);
|
|
718
|
+
}
|
|
719
|
+
async fillForm(data, schema) {
|
|
720
|
+
var _a;
|
|
721
|
+
const endpoint = this.useProxy ? `${this.proxyUrl}/complete-anthropic` : 'https://api.anthropic.com/v1/messages';
|
|
722
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
723
|
+
if (!this.useProxy) {
|
|
724
|
+
headers['x-api-key'] = this.apiKey;
|
|
725
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
726
|
+
}
|
|
727
|
+
const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
|
|
728
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
729
|
+
const userPrompt = `Data: "${data}"
|
|
730
|
+
|
|
731
|
+
Form fields:
|
|
732
|
+
${JSON.stringify(finalSchema, null, 2)}
|
|
733
|
+
|
|
734
|
+
Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
|
|
735
|
+
const body = this.useProxy
|
|
736
|
+
? { model: this.model, messages: [{ role: 'user', content: userPrompt }], system: systemPrompt }
|
|
737
|
+
: { model: this.model, max_tokens: 4096, system: systemPrompt, messages: [{ role: 'user', content: userPrompt }] };
|
|
738
|
+
const response = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
|
|
739
|
+
if (!response.ok) {
|
|
740
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
741
|
+
throw new Error(`Anthropic API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
|
|
742
|
+
}
|
|
743
|
+
const result = await response.json();
|
|
744
|
+
const content = this.useProxy ? result.choices[0].message.content : result.content[0].text;
|
|
745
|
+
return JSON.parse(content);
|
|
746
|
+
}
|
|
747
|
+
buildSystemPrompt() {
|
|
748
|
+
return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
|
|
749
|
+
Rules:
|
|
750
|
+
1. Only extract values that can be confidently determined
|
|
751
|
+
2. Respect field types (string, number, datetime, boolean, select)
|
|
752
|
+
3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
|
|
753
|
+
4. For date fields, use DD/MM/YYYY format
|
|
754
|
+
5. For select fields, use exact option values from the provided choices
|
|
755
|
+
6. Leave fields empty if no relevant information is found
|
|
756
|
+
7. Fields marked readonly MUST NOT receive values
|
|
757
|
+
8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
|
|
758
|
+
9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
|
|
759
|
+
10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
|
|
760
|
+
Respond ONLY with valid JSON.`;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
class MistralLLMService {
|
|
764
|
+
constructor(config) {
|
|
765
|
+
this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
|
|
766
|
+
this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
|
|
767
|
+
this.model = (config === null || config === void 0 ? void 0 : config.model) || 'mistral-medium-latest';
|
|
768
|
+
this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
|
|
769
|
+
if (!this.useProxy && !this.apiKey) {
|
|
770
|
+
throw new Error('Mistral API key is required');
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async fillFormFromTranscription(transcription, schema) {
|
|
774
|
+
return this.fillForm(transcription, schema);
|
|
775
|
+
}
|
|
776
|
+
async fillFormFromJson(json, schema) {
|
|
777
|
+
return this.fillForm(json, schema);
|
|
778
|
+
}
|
|
779
|
+
async fillForm(data, schema) {
|
|
780
|
+
var _a;
|
|
781
|
+
const endpoint = this.useProxy ? `${this.proxyUrl}/complete-mistral` : 'https://api.mistral.ai/v1/chat/completions';
|
|
782
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
783
|
+
if (!this.useProxy) {
|
|
784
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
785
|
+
}
|
|
786
|
+
const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
|
|
787
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
788
|
+
const userPrompt = `Data: "${data}"
|
|
789
|
+
|
|
790
|
+
Form fields:
|
|
791
|
+
${JSON.stringify(finalSchema, null, 2)}
|
|
792
|
+
|
|
793
|
+
Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
|
|
794
|
+
const response = await fetch(endpoint, {
|
|
795
|
+
method: 'POST',
|
|
796
|
+
headers,
|
|
797
|
+
body: JSON.stringify({
|
|
798
|
+
model: this.model,
|
|
799
|
+
messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }],
|
|
800
|
+
response_format: { type: 'json_object' },
|
|
801
|
+
}),
|
|
802
|
+
});
|
|
803
|
+
if (!response.ok) {
|
|
804
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
805
|
+
throw new Error(`Mistral API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
|
|
806
|
+
}
|
|
807
|
+
const result = await response.json();
|
|
808
|
+
return JSON.parse(result.choices[0].message.content);
|
|
809
|
+
}
|
|
810
|
+
buildSystemPrompt() {
|
|
811
|
+
return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
|
|
812
|
+
Rules:
|
|
813
|
+
1. Only extract values that can be confidently determined
|
|
814
|
+
2. Respect field types (string, number, datetime, boolean, select)
|
|
815
|
+
3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
|
|
816
|
+
4. For date fields, use DD/MM/YYYY format
|
|
817
|
+
5. For select fields, use exact option values from the provided choices
|
|
818
|
+
6. Leave fields empty if no relevant information is found
|
|
819
|
+
7. Fields marked readonly MUST NOT receive values
|
|
820
|
+
8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
|
|
821
|
+
9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
|
|
822
|
+
10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
|
|
823
|
+
Respond ONLY with valid JSON.`;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
636
826
|
class LLMServiceFactory {
|
|
637
827
|
static create(config) {
|
|
638
828
|
var _a;
|
|
639
829
|
const provider = ((_a = config.llm) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
|
|
640
830
|
switch (provider) {
|
|
641
|
-
case 'openai':
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
831
|
+
case 'openai': return new OpenAILLMService(config.llm);
|
|
832
|
+
case 'anthropic': return new AnthropicLLMService(config.llm);
|
|
833
|
+
case 'mistral': return new MistralLLMService(config.llm);
|
|
834
|
+
default: throw new Error(`Unsupported LLM provider: ${provider}`);
|
|
645
835
|
}
|
|
646
836
|
}
|
|
647
837
|
}
|
|
@@ -4161,6 +4351,8 @@ const VoiceFormRecorder = class {
|
|
|
4161
4351
|
this.apiProxyUrl = 'http://localhost:8492';
|
|
4162
4352
|
this.transcriptionModel = 'gpt-4o-transcribe';
|
|
4163
4353
|
this.completionModel = 'gpt-5-mini';
|
|
4354
|
+
this.transcriptionProvider = 'openai';
|
|
4355
|
+
this.completionProvider = 'openai';
|
|
4164
4356
|
this.context = undefined;
|
|
4165
4357
|
this.classificationRootUrl = 'http://localhost';
|
|
4166
4358
|
this.language = 'en';
|
|
@@ -4172,6 +4364,7 @@ const VoiceFormRecorder = class {
|
|
|
4172
4364
|
this.isRecording = false;
|
|
4173
4365
|
this.isProcessing = false;
|
|
4174
4366
|
this.hasError = false;
|
|
4367
|
+
this.hasTransientError = false;
|
|
4175
4368
|
this.transcription = '';
|
|
4176
4369
|
this.filledData = null;
|
|
4177
4370
|
this.debugInfo = {};
|
|
@@ -4184,7 +4377,21 @@ const VoiceFormRecorder = class {
|
|
|
4184
4377
|
if (!input.files || input.files.length === 0)
|
|
4185
4378
|
return;
|
|
4186
4379
|
const file = input.files[0];
|
|
4187
|
-
this.
|
|
4380
|
+
const isEn = this.language === 'en';
|
|
4381
|
+
// Validate file type
|
|
4382
|
+
const validAudioTypes = /^audio\//;
|
|
4383
|
+
const validAudioExtensions = /\.(mp3|wav|webm|m4a|ogg|flac|aac|opus)$/i;
|
|
4384
|
+
if (!validAudioTypes.test(file.type) && !validAudioExtensions.test(file.name)) {
|
|
4385
|
+
this.hasTransientError = true;
|
|
4386
|
+
this.statusMessage = isEn
|
|
4387
|
+
? `Invalid file type: "${file.name}". Please select an audio file.`
|
|
4388
|
+
: `Type de fichier invalide : "${file.name}". Veuillez sélectionner un fichier audio.`;
|
|
4389
|
+
input.value = '';
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
// Reset the input so the same file can be re-selected after an error
|
|
4393
|
+
input.value = '';
|
|
4394
|
+
await this.processAudioContent(file);
|
|
4188
4395
|
};
|
|
4189
4396
|
this.audioRecorder = new AudioRecorderService();
|
|
4190
4397
|
}
|
|
@@ -4208,17 +4415,33 @@ const VoiceFormRecorder = class {
|
|
|
4208
4415
|
this.updateDebugInfo(errorMessage, { error: errorMessage });
|
|
4209
4416
|
}
|
|
4210
4417
|
else {
|
|
4418
|
+
// Validate transcription provider/model
|
|
4419
|
+
const allowedTranscriptionModels = TRANSCRIPTION_MODELS[this.transcriptionProvider];
|
|
4420
|
+
if (!allowedTranscriptionModels) {
|
|
4421
|
+
throw new Error(`Unsupported transcription provider: '${this.transcriptionProvider}'. Allowed: openai, mistral`);
|
|
4422
|
+
}
|
|
4423
|
+
if (!allowedTranscriptionModels.includes(this.transcriptionModel)) {
|
|
4424
|
+
throw new Error(`Model '${this.transcriptionModel}' is not allowed for transcription provider '${this.transcriptionProvider}'. Allowed: ${allowedTranscriptionModels.join(', ')}`);
|
|
4425
|
+
}
|
|
4426
|
+
// Validate completion provider/model
|
|
4427
|
+
const allowedCompletionModels = COMPLETION_MODELS[this.completionProvider];
|
|
4428
|
+
if (!allowedCompletionModels) {
|
|
4429
|
+
throw new Error(`Unsupported completion provider: '${this.completionProvider}'. Allowed: openai, anthropic, mistral`);
|
|
4430
|
+
}
|
|
4431
|
+
if (!allowedCompletionModels.includes(this.completionModel)) {
|
|
4432
|
+
throw new Error(`Model '${this.completionModel}' is not allowed for completion provider '${this.completionProvider}'. Allowed: ${allowedCompletionModels.join(', ')}`);
|
|
4433
|
+
}
|
|
4211
4434
|
// Parse form schema
|
|
4212
4435
|
this.parsedSchema = JSON.parse(this.formJson || '{}');
|
|
4213
4436
|
// Parse service configuration
|
|
4214
4437
|
this.parsedConfig = JSON.parse(this.serviceConfig || '{}');
|
|
4215
4438
|
// Add API key to config if provided via prop
|
|
4216
4439
|
if (this.apiKey) {
|
|
4217
|
-
this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { apiKey: this.apiKey, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { apiKey: this.apiKey, model: this.completionModel }) });
|
|
4440
|
+
this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { provider: this.transcriptionProvider, apiKey: this.apiKey, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { provider: this.completionProvider, apiKey: this.apiKey, model: this.completionModel }) });
|
|
4218
4441
|
}
|
|
4219
4442
|
else {
|
|
4220
4443
|
// Use proxy API if no API key provided
|
|
4221
|
-
this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { useProxy: true, proxyUrl: this.apiProxyUrl, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { useProxy: true, proxyUrl: this.apiProxyUrl, model: this.completionModel }) });
|
|
4444
|
+
this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { provider: this.transcriptionProvider, useProxy: true, proxyUrl: this.apiProxyUrl, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { provider: this.completionProvider, useProxy: true, proxyUrl: this.apiProxyUrl, model: this.completionModel }) });
|
|
4222
4445
|
}
|
|
4223
4446
|
// Initialize services
|
|
4224
4447
|
this.speechToTextService = SpeechToTextServiceFactory.create(this.parsedConfig);
|
|
@@ -4228,6 +4451,7 @@ const VoiceFormRecorder = class {
|
|
|
4228
4451
|
config: this.parsedConfig
|
|
4229
4452
|
});
|
|
4230
4453
|
this.hasError = false;
|
|
4454
|
+
this.hasTransientError = false;
|
|
4231
4455
|
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.idle) || (this.language == 'en' ? 'Select an input method' : 'Sélectionner une méthode de saisie');
|
|
4232
4456
|
if (this.parsedInputTypes.length === 0) {
|
|
4233
4457
|
this.inputTypes = 'voice';
|
|
@@ -4248,6 +4472,91 @@ const VoiceFormRecorder = class {
|
|
|
4248
4472
|
} });
|
|
4249
4473
|
}
|
|
4250
4474
|
}
|
|
4475
|
+
getMicErrorMessage(error) {
|
|
4476
|
+
var _a, _b;
|
|
4477
|
+
const isEn = this.language === 'en';
|
|
4478
|
+
const errorName = error.name || '';
|
|
4479
|
+
if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
|
|
4480
|
+
return isEn
|
|
4481
|
+
? 'Microphone access denied. Allow microphone access in your browser settings and try again.'
|
|
4482
|
+
: "Accès au microphone refusé. Autorisez l'accès au microphone dans les paramètres du navigateur et réessayez.";
|
|
4483
|
+
}
|
|
4484
|
+
if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
|
|
4485
|
+
return isEn
|
|
4486
|
+
? 'No microphone found. Please connect a microphone and try again.'
|
|
4487
|
+
: 'Aucun microphone trouvé. Connectez un microphone et réessayez.';
|
|
4488
|
+
}
|
|
4489
|
+
if (errorName === 'NotReadableError' || errorName === 'TrackStartError') {
|
|
4490
|
+
return isEn
|
|
4491
|
+
? 'Microphone is busy or unavailable. Close other apps using the microphone and try again.'
|
|
4492
|
+
: 'Le microphone est occupé ou indisponible. Fermez les autres applications utilisant le microphone et réessayez.';
|
|
4493
|
+
}
|
|
4494
|
+
if (errorName === 'AbortError') {
|
|
4495
|
+
return isEn
|
|
4496
|
+
? 'Microphone access was interrupted. Please try again.'
|
|
4497
|
+
: "L'accès au microphone a été interrompu. Veuillez réessayer.";
|
|
4498
|
+
}
|
|
4499
|
+
if (errorName === 'OverconstrainedError') {
|
|
4500
|
+
return isEn
|
|
4501
|
+
? 'Microphone does not support the required audio settings.'
|
|
4502
|
+
: 'Le microphone ne supporte pas les paramètres audio requis.';
|
|
4503
|
+
}
|
|
4504
|
+
if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('HTTPS')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('not supported'))) {
|
|
4505
|
+
return isEn
|
|
4506
|
+
? 'Microphone access requires HTTPS or localhost.'
|
|
4507
|
+
: "L'accès au microphone nécessite HTTPS ou localhost.";
|
|
4508
|
+
}
|
|
4509
|
+
return isEn ? `Microphone error: ${error.message}` : `Erreur microphone : ${error.message}`;
|
|
4510
|
+
}
|
|
4511
|
+
getReadableErrorMessage(error, phase) {
|
|
4512
|
+
const isEn = this.language === 'en';
|
|
4513
|
+
const msg = error.message || '';
|
|
4514
|
+
if (/401|unauthorized|invalid.{0,20}(api )?key|authentication failed/i.test(msg)) {
|
|
4515
|
+
return isEn
|
|
4516
|
+
? 'Authentication failed: invalid API key. Check your configuration.'
|
|
4517
|
+
: "Échec d'authentification : clé API invalide. Vérifiez votre configuration.";
|
|
4518
|
+
}
|
|
4519
|
+
if (/403|forbidden/i.test(msg)) {
|
|
4520
|
+
return isEn
|
|
4521
|
+
? 'Access denied: check your API permissions.'
|
|
4522
|
+
: 'Accès refusé : vérifiez vos permissions API.';
|
|
4523
|
+
}
|
|
4524
|
+
if (/429|rate.{0,10}limit|too many requests/i.test(msg)) {
|
|
4525
|
+
return isEn
|
|
4526
|
+
? 'Rate limit exceeded. Please wait a moment and try again.'
|
|
4527
|
+
: 'Limite de requêtes dépassée. Veuillez attendre quelques secondes et réessayer.';
|
|
4528
|
+
}
|
|
4529
|
+
if (/404|not found/i.test(msg)) {
|
|
4530
|
+
return isEn
|
|
4531
|
+
? 'API endpoint not found. Check your proxy URL configuration.'
|
|
4532
|
+
: "Endpoint API introuvable. Vérifiez l'URL de votre proxy.";
|
|
4533
|
+
}
|
|
4534
|
+
if (/5\d\d|server error|internal server|bad gateway|service unavailable/i.test(msg)) {
|
|
4535
|
+
return isEn
|
|
4536
|
+
? 'Server error. Please try again in a moment.'
|
|
4537
|
+
: 'Erreur serveur. Veuillez réessayer dans un instant.';
|
|
4538
|
+
}
|
|
4539
|
+
if (/network|failed to fetch|err_connection|connection refused/i.test(msg)) {
|
|
4540
|
+
return isEn
|
|
4541
|
+
? 'Network error: check your connection and proxy server.'
|
|
4542
|
+
: 'Erreur réseau : vérifiez votre connexion et le serveur proxy.';
|
|
4543
|
+
}
|
|
4544
|
+
if (/json|parse error|unexpected token|invalid response/i.test(msg)) {
|
|
4545
|
+
return isEn
|
|
4546
|
+
? 'Unexpected response from AI service. Please try again.'
|
|
4547
|
+
: 'Réponse inattendue du service IA. Veuillez réessayer.';
|
|
4548
|
+
}
|
|
4549
|
+
if (phase === 'transcription') {
|
|
4550
|
+
return isEn ? `Transcription failed: ${msg}` : `Échec de la transcription : ${msg}`;
|
|
4551
|
+
}
|
|
4552
|
+
if (phase === 'llm') {
|
|
4553
|
+
return isEn ? `Form filling failed: ${msg}` : `Échec du remplissage du formulaire : ${msg}`;
|
|
4554
|
+
}
|
|
4555
|
+
if (phase === 'ocr') {
|
|
4556
|
+
return isEn ? `OCR processing failed: ${msg}` : `Échec du traitement OCR : ${msg}`;
|
|
4557
|
+
}
|
|
4558
|
+
return isEn ? `Processing error: ${msg}` : `Erreur de traitement : ${msg}`;
|
|
4559
|
+
}
|
|
4251
4560
|
async handleRecordClick() {
|
|
4252
4561
|
if (this.isProcessing)
|
|
4253
4562
|
return;
|
|
@@ -4261,7 +4570,7 @@ const VoiceFormRecorder = class {
|
|
|
4261
4570
|
async startRecording() {
|
|
4262
4571
|
var _a;
|
|
4263
4572
|
try {
|
|
4264
|
-
this.
|
|
4573
|
+
this.hasTransientError = false;
|
|
4265
4574
|
this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
|
|
4266
4575
|
this.updateDebugInfo('Start Recording Attempt', {});
|
|
4267
4576
|
await this.audioRecorder.startRecording();
|
|
@@ -4274,8 +4583,8 @@ const VoiceFormRecorder = class {
|
|
|
4274
4583
|
});
|
|
4275
4584
|
}
|
|
4276
4585
|
catch (error) {
|
|
4277
|
-
this.
|
|
4278
|
-
this.statusMessage =
|
|
4586
|
+
this.hasTransientError = true;
|
|
4587
|
+
this.statusMessage = this.getMicErrorMessage(error);
|
|
4279
4588
|
this.updateDebugInfo('Recording Error', { error: error.message });
|
|
4280
4589
|
this.formFilled.emit({
|
|
4281
4590
|
success: false,
|
|
@@ -4285,40 +4594,36 @@ const VoiceFormRecorder = class {
|
|
|
4285
4594
|
}
|
|
4286
4595
|
async processJsonForm(ocrData) {
|
|
4287
4596
|
var _a, _b, _c;
|
|
4597
|
+
this.isProcessing = true;
|
|
4598
|
+
this.hasTransientError = false;
|
|
4599
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing document...' : 'Traitement du document ...');
|
|
4288
4600
|
try {
|
|
4289
|
-
this.isProcessing = true;
|
|
4290
|
-
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
|
|
4291
|
-
// Extract content from OCR format
|
|
4292
4601
|
const extractedData = (ocrData === null || ocrData === void 0 ? void 0 : ocrData.content) || ocrData;
|
|
4293
4602
|
const jsonForm = JSON.stringify(extractedData);
|
|
4294
|
-
|
|
4603
|
+
if (!extractedData) {
|
|
4604
|
+
throw Object.assign(new Error('OCR returned no data'), { phase: 'ocr' });
|
|
4605
|
+
}
|
|
4295
4606
|
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
4296
4607
|
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
4297
|
-
|
|
4298
|
-
|
|
4608
|
+
let filledSchema;
|
|
4609
|
+
try {
|
|
4610
|
+
filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
|
|
4611
|
+
}
|
|
4612
|
+
catch (llmError) {
|
|
4613
|
+
throw Object.assign(new Error(llmError.message), { phase: 'llm' });
|
|
4614
|
+
}
|
|
4299
4615
|
this.filledData = this.extractFilledData(filledSchema);
|
|
4300
|
-
this.updateDebugInfo('Form Filled', {
|
|
4301
|
-
filledSchema,
|
|
4302
|
-
extractedData: this.filledData
|
|
4303
|
-
});
|
|
4616
|
+
this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
|
|
4304
4617
|
this.parsedSchema = this.filledData;
|
|
4305
|
-
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire
|
|
4306
|
-
this.
|
|
4307
|
-
// Emit success event
|
|
4308
|
-
this.formFilled.emit({
|
|
4309
|
-
success: true,
|
|
4310
|
-
data: this.filledData,
|
|
4311
|
-
jsonForm: jsonForm
|
|
4312
|
-
});
|
|
4618
|
+
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
|
|
4619
|
+
this.formFilled.emit({ success: true, data: this.filledData, jsonForm });
|
|
4313
4620
|
}
|
|
4314
4621
|
catch (error) {
|
|
4315
|
-
|
|
4316
|
-
this.
|
|
4317
|
-
this.
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
jsonForm: JSON.stringify(ocrData)
|
|
4321
|
-
});
|
|
4622
|
+
console.error('OCR processing error:', error);
|
|
4623
|
+
this.hasTransientError = true;
|
|
4624
|
+
this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'llm');
|
|
4625
|
+
this.updateDebugInfo('OCR Processing Error', { phase: error.phase, error: error.message });
|
|
4626
|
+
this.formFilled.emit({ success: false, error: error.message, jsonForm: JSON.stringify(ocrData) });
|
|
4322
4627
|
}
|
|
4323
4628
|
finally {
|
|
4324
4629
|
this.isProcessing = false;
|
|
@@ -4326,83 +4631,100 @@ const VoiceFormRecorder = class {
|
|
|
4326
4631
|
}
|
|
4327
4632
|
async processAudioContent(audioFile) {
|
|
4328
4633
|
var _a, _b, _c;
|
|
4329
|
-
this.
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
});
|
|
4634
|
+
this.isProcessing = true;
|
|
4635
|
+
this.hasTransientError = false;
|
|
4636
|
+
this.updateDebugInfo('Audio Captured', { size: audioFile.size, type: audioFile.type });
|
|
4333
4637
|
try {
|
|
4334
|
-
//
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4638
|
+
// Validate audio file before sending to API
|
|
4639
|
+
const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB (API limit)
|
|
4640
|
+
if (audioFile.size <= 44) { // WAV header alone is 44 bytes; effectively empty
|
|
4641
|
+
this.hasTransientError = true;
|
|
4642
|
+
this.statusMessage = this.language == 'en'
|
|
4643
|
+
? 'Recording is empty or too short. Please try again.'
|
|
4644
|
+
: "L'enregistrement est vide ou trop court. Veuillez réessayer.";
|
|
4645
|
+
this.formFilled.emit({ success: false, error: 'Empty or too short audio' });
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
4648
|
+
if (audioFile.size > MAX_AUDIO_SIZE) {
|
|
4649
|
+
this.hasTransientError = true;
|
|
4650
|
+
this.statusMessage = this.language == 'en'
|
|
4651
|
+
? `File too large (${Math.round(audioFile.size / 1024 / 1024)}MB). Maximum is 25MB.`
|
|
4652
|
+
: `Fichier trop volumineux (${Math.round(audioFile.size / 1024 / 1024)}Mo). Maximum : 25Mo.`;
|
|
4653
|
+
this.formFilled.emit({ success: false, error: 'Audio file too large' });
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
// Step 1: Transcription
|
|
4657
|
+
let transcription;
|
|
4658
|
+
try {
|
|
4659
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
|
|
4660
|
+
transcription = await this.speechToTextService.transcribe(audioFile, this.language);
|
|
4661
|
+
this.transcription = transcription;
|
|
4662
|
+
this.updateDebugInfo('Transcription Complete', { transcription });
|
|
4663
|
+
}
|
|
4664
|
+
catch (transcriptionError) {
|
|
4665
|
+
throw Object.assign(new Error(transcriptionError.message), { phase: 'transcription' });
|
|
4666
|
+
}
|
|
4339
4667
|
if (!transcription.trim()) {
|
|
4340
|
-
|
|
4668
|
+
this.hasTransientError = true;
|
|
4669
|
+
this.statusMessage = this.language == 'en'
|
|
4670
|
+
? 'No speech detected. Please speak clearly and try again.'
|
|
4671
|
+
: 'Aucune parole détectée. Parlez clairement et réessayez.';
|
|
4672
|
+
this.formFilled.emit({ success: false, error: 'No speech detected', transcription: '' });
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
// Step 2: LLM form filling
|
|
4676
|
+
let filledSchema;
|
|
4677
|
+
try {
|
|
4678
|
+
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
4679
|
+
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
4680
|
+
filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
|
|
4681
|
+
}
|
|
4682
|
+
catch (llmError) {
|
|
4683
|
+
throw Object.assign(new Error(llmError.message), { phase: 'llm' });
|
|
4341
4684
|
}
|
|
4342
|
-
// Fill form using LLM
|
|
4343
|
-
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
4344
|
-
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
4345
|
-
const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
|
|
4346
|
-
// Extract filled data
|
|
4347
4685
|
this.filledData = this.extractFilledData(filledSchema);
|
|
4348
|
-
this.updateDebugInfo('Form Filled', {
|
|
4349
|
-
filledSchema,
|
|
4350
|
-
extractedData: this.filledData
|
|
4351
|
-
});
|
|
4686
|
+
this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
|
|
4352
4687
|
this.parsedSchema = this.filledData;
|
|
4353
|
-
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire
|
|
4354
|
-
this.
|
|
4355
|
-
// Emit success event
|
|
4356
|
-
this.formFilled.emit({
|
|
4357
|
-
success: true,
|
|
4358
|
-
data: this.filledData,
|
|
4359
|
-
transcription: transcription
|
|
4360
|
-
});
|
|
4688
|
+
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
|
|
4689
|
+
this.formFilled.emit({ success: true, data: this.filledData, transcription });
|
|
4361
4690
|
}
|
|
4362
4691
|
catch (error) {
|
|
4363
|
-
console.error('
|
|
4364
|
-
this.
|
|
4365
|
-
this.statusMessage =
|
|
4366
|
-
this.updateDebugInfo('
|
|
4367
|
-
this.formFilled.emit({
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
});
|
|
4692
|
+
console.error('Audio processing error:', error);
|
|
4693
|
+
this.hasTransientError = true;
|
|
4694
|
+
this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'audio-upload');
|
|
4695
|
+
this.updateDebugInfo('Audio Processing Error', { phase: error.phase, error: error.message });
|
|
4696
|
+
this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
|
|
4697
|
+
}
|
|
4698
|
+
finally {
|
|
4699
|
+
this.isProcessing = false;
|
|
4372
4700
|
}
|
|
4373
4701
|
}
|
|
4374
4702
|
async stopRecordingAndProcess() {
|
|
4375
4703
|
var _a;
|
|
4704
|
+
this.isRecording = false;
|
|
4705
|
+
this.isProcessing = true;
|
|
4706
|
+
this.hasTransientError = false;
|
|
4707
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
|
|
4708
|
+
this.updateDebugInfo('Stop Recording', {});
|
|
4709
|
+
this.recordingStateChanged.emit({ isRecording: false, state: 'processing' });
|
|
4376
4710
|
try {
|
|
4377
|
-
this.isRecording = false;
|
|
4378
|
-
this.isProcessing = true;
|
|
4379
|
-
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
|
|
4380
|
-
this.updateDebugInfo('Stop Recording', {});
|
|
4381
|
-
this.recordingStateChanged.emit({
|
|
4382
|
-
isRecording: false,
|
|
4383
|
-
state: 'processing'
|
|
4384
|
-
});
|
|
4385
|
-
// Stop recording and get audio blob
|
|
4386
4711
|
const audioBlob = await this.audioRecorder.stopRecording();
|
|
4387
|
-
const audioContent = new File([audioBlob], 'audio.
|
|
4388
|
-
|
|
4712
|
+
const audioContent = new File([audioBlob], 'audio.wav', { type: 'audio/wav' });
|
|
4713
|
+
// processAudioContent manages isProcessing itself; await ensures we don't return early
|
|
4714
|
+
await this.processAudioContent(audioContent);
|
|
4389
4715
|
}
|
|
4390
4716
|
catch (error) {
|
|
4391
|
-
|
|
4392
|
-
this.
|
|
4393
|
-
this.
|
|
4394
|
-
this.
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
});
|
|
4717
|
+
// Only reached if audioRecorder.stopRecording() itself throws
|
|
4718
|
+
this.hasTransientError = true;
|
|
4719
|
+
this.isProcessing = false;
|
|
4720
|
+
this.statusMessage = (this.language == 'en'
|
|
4721
|
+
? `Failed to stop recording: ${error.message}`
|
|
4722
|
+
: `Impossible d'arrêter l'enregistrement : ${error.message}`);
|
|
4723
|
+
this.updateDebugInfo('Stop Recording Error', { error: error.message });
|
|
4724
|
+
this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
|
|
4399
4725
|
}
|
|
4400
4726
|
finally {
|
|
4401
|
-
this.
|
|
4402
|
-
this.recordingStateChanged.emit({
|
|
4403
|
-
isRecording: false,
|
|
4404
|
-
state: 'idle'
|
|
4405
|
-
});
|
|
4727
|
+
this.recordingStateChanged.emit({ isRecording: false, state: 'idle' });
|
|
4406
4728
|
}
|
|
4407
4729
|
}
|
|
4408
4730
|
extractFilledData(filledData) {
|
|
@@ -4710,12 +5032,14 @@ const VoiceFormRecorder = class {
|
|
|
4710
5032
|
return (index.h("button", { class: buttonClass, style: buttonStyle, onClick: () => this.handleRecordClick(), disabled: isDisabled, "aria-label": this.isRecording ? 'Stop recording' : 'Start recording' }, this.isProcessing ? (index.h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, index.h("circle", { cx: "12", cy: "12", r: "3" }, index.h("animate", { attributeName: "r", values: "3;6;3", dur: "1s", repeatCount: "indefinite" }), index.h("animate", { attributeName: "opacity", values: "1;0.3;1", dur: "1s", repeatCount: "indefinite" })))) : this.isRecording ? (index.h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, index.h("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }))) : (index.h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, index.h("circle", { cx: "12", cy: "12", r: "8" })))));
|
|
4711
5033
|
}
|
|
4712
5034
|
renderStatusMessage() {
|
|
5035
|
+
const hasAnyError = this.hasError || this.hasTransientError;
|
|
4713
5036
|
const statusClass = [
|
|
4714
5037
|
'status-text',
|
|
4715
|
-
|
|
4716
|
-
this.filledData && !
|
|
5038
|
+
hasAnyError && 'error',
|
|
5039
|
+
this.filledData && !hasAnyError && 'success'
|
|
4717
5040
|
].filter(Boolean).join(' ');
|
|
4718
|
-
|
|
5041
|
+
const statusStyle = this.getStatusStyle();
|
|
5042
|
+
return index.h("div", { class: statusClass, style: statusStyle }, this.statusMessage);
|
|
4719
5043
|
}
|
|
4720
5044
|
renderFormPreview() {
|
|
4721
5045
|
if (!this.parsedSchema)
|
|
@@ -4888,13 +5212,16 @@ const VoiceFormRecorder = class {
|
|
|
4888
5212
|
}
|
|
4889
5213
|
render() {
|
|
4890
5214
|
const containerStyle = this.getContainerStyle();
|
|
4891
|
-
|
|
4892
|
-
return (index.h("div", { key: '8503450dbd4de2cb91172bd2bdbdf16a0c4cdce0' }, index.h("div", { key: '8b3f553574a64754312f68f2abe0d64f57c56d3e', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, index.h("div", { key: '3d08a37d611bc91dbe11c065c633f0d36c8b362e', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? index.h("div", { class: "status-text", style: statusStyle }, this.statusMessage) : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
|
|
5215
|
+
return (index.h("div", { key: 'd2eb6b74840a57809920b3dbba3fd7dfbeef8531' }, index.h("div", { key: 'e66e1d4fc23176f9024ca20413535440a8be3ce8', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, index.h("div", { key: '57dd7b882771d1f0bd56a97bfd0a06ffee09428e', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? this.renderStatusMessage() : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
|
|
4893
5216
|
}
|
|
4894
5217
|
static get watchers() { return {
|
|
4895
5218
|
"formJson": ["initializeServices"],
|
|
4896
5219
|
"serviceConfig": ["initializeServices"],
|
|
4897
|
-
"theme": ["initializeServices"]
|
|
5220
|
+
"theme": ["initializeServices"],
|
|
5221
|
+
"transcriptionProvider": ["initializeServices"],
|
|
5222
|
+
"completionProvider": ["initializeServices"],
|
|
5223
|
+
"transcriptionModel": ["initializeServices"],
|
|
5224
|
+
"completionModel": ["initializeServices"]
|
|
4898
5225
|
}; }
|
|
4899
5226
|
};
|
|
4900
5227
|
VoiceFormRecorder.style = voiceInputModuleCss;
|