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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { h } from "@stencil/core";
|
|
2
|
+
import { TRANSCRIPTION_MODELS, COMPLETION_MODELS } from "../../types/service-providers.types";
|
|
2
3
|
import { AudioRecorderService } from "../../services/audio-recorder.service";
|
|
3
4
|
import { SpeechToTextServiceFactory } from "../../services/speech-to-text.service";
|
|
4
5
|
import { LLMServiceFactory } from "../../services/llm.service";
|
|
@@ -23,6 +24,8 @@ export class VoiceFormRecorder {
|
|
|
23
24
|
this.apiProxyUrl = 'http://localhost:8492';
|
|
24
25
|
this.transcriptionModel = 'gpt-4o-transcribe';
|
|
25
26
|
this.completionModel = 'gpt-5-mini';
|
|
27
|
+
this.transcriptionProvider = 'openai';
|
|
28
|
+
this.completionProvider = 'openai';
|
|
26
29
|
this.context = undefined;
|
|
27
30
|
this.classificationRootUrl = 'http://localhost';
|
|
28
31
|
this.language = 'en';
|
|
@@ -34,6 +37,7 @@ export class VoiceFormRecorder {
|
|
|
34
37
|
this.isRecording = false;
|
|
35
38
|
this.isProcessing = false;
|
|
36
39
|
this.hasError = false;
|
|
40
|
+
this.hasTransientError = false;
|
|
37
41
|
this.transcription = '';
|
|
38
42
|
this.filledData = null;
|
|
39
43
|
this.debugInfo = {};
|
|
@@ -46,7 +50,21 @@ export class VoiceFormRecorder {
|
|
|
46
50
|
if (!input.files || input.files.length === 0)
|
|
47
51
|
return;
|
|
48
52
|
const file = input.files[0];
|
|
49
|
-
this.
|
|
53
|
+
const isEn = this.language === 'en';
|
|
54
|
+
// Validate file type
|
|
55
|
+
const validAudioTypes = /^audio\//;
|
|
56
|
+
const validAudioExtensions = /\.(mp3|wav|webm|m4a|ogg|flac|aac|opus)$/i;
|
|
57
|
+
if (!validAudioTypes.test(file.type) && !validAudioExtensions.test(file.name)) {
|
|
58
|
+
this.hasTransientError = true;
|
|
59
|
+
this.statusMessage = isEn
|
|
60
|
+
? `Invalid file type: "${file.name}". Please select an audio file.`
|
|
61
|
+
: `Type de fichier invalide : "${file.name}". Veuillez sélectionner un fichier audio.`;
|
|
62
|
+
input.value = '';
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Reset the input so the same file can be re-selected after an error
|
|
66
|
+
input.value = '';
|
|
67
|
+
await this.processAudioContent(file);
|
|
50
68
|
};
|
|
51
69
|
this.audioRecorder = new AudioRecorderService();
|
|
52
70
|
}
|
|
@@ -70,17 +88,33 @@ export class VoiceFormRecorder {
|
|
|
70
88
|
this.updateDebugInfo(errorMessage, { error: errorMessage });
|
|
71
89
|
}
|
|
72
90
|
else {
|
|
91
|
+
// Validate transcription provider/model
|
|
92
|
+
const allowedTranscriptionModels = TRANSCRIPTION_MODELS[this.transcriptionProvider];
|
|
93
|
+
if (!allowedTranscriptionModels) {
|
|
94
|
+
throw new Error(`Unsupported transcription provider: '${this.transcriptionProvider}'. Allowed: openai, mistral`);
|
|
95
|
+
}
|
|
96
|
+
if (!allowedTranscriptionModels.includes(this.transcriptionModel)) {
|
|
97
|
+
throw new Error(`Model '${this.transcriptionModel}' is not allowed for transcription provider '${this.transcriptionProvider}'. Allowed: ${allowedTranscriptionModels.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
// Validate completion provider/model
|
|
100
|
+
const allowedCompletionModels = COMPLETION_MODELS[this.completionProvider];
|
|
101
|
+
if (!allowedCompletionModels) {
|
|
102
|
+
throw new Error(`Unsupported completion provider: '${this.completionProvider}'. Allowed: openai, anthropic, mistral`);
|
|
103
|
+
}
|
|
104
|
+
if (!allowedCompletionModels.includes(this.completionModel)) {
|
|
105
|
+
throw new Error(`Model '${this.completionModel}' is not allowed for completion provider '${this.completionProvider}'. Allowed: ${allowedCompletionModels.join(', ')}`);
|
|
106
|
+
}
|
|
73
107
|
// Parse form schema
|
|
74
108
|
this.parsedSchema = JSON.parse(this.formJson || '{}');
|
|
75
109
|
// Parse service configuration
|
|
76
110
|
this.parsedConfig = JSON.parse(this.serviceConfig || '{}');
|
|
77
111
|
// Add API key to config if provided via prop
|
|
78
112
|
if (this.apiKey) {
|
|
79
|
-
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 }) });
|
|
113
|
+
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 }) });
|
|
80
114
|
}
|
|
81
115
|
else {
|
|
82
116
|
// Use proxy API if no API key provided
|
|
83
|
-
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 }) });
|
|
117
|
+
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 }) });
|
|
84
118
|
}
|
|
85
119
|
// Initialize services
|
|
86
120
|
this.speechToTextService = SpeechToTextServiceFactory.create(this.parsedConfig);
|
|
@@ -90,6 +124,7 @@ export class VoiceFormRecorder {
|
|
|
90
124
|
config: this.parsedConfig
|
|
91
125
|
});
|
|
92
126
|
this.hasError = false;
|
|
127
|
+
this.hasTransientError = false;
|
|
93
128
|
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');
|
|
94
129
|
if (this.parsedInputTypes.length === 0) {
|
|
95
130
|
this.inputTypes = 'voice';
|
|
@@ -110,6 +145,91 @@ export class VoiceFormRecorder {
|
|
|
110
145
|
} });
|
|
111
146
|
}
|
|
112
147
|
}
|
|
148
|
+
getMicErrorMessage(error) {
|
|
149
|
+
var _a, _b;
|
|
150
|
+
const isEn = this.language === 'en';
|
|
151
|
+
const errorName = error.name || '';
|
|
152
|
+
if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
|
|
153
|
+
return isEn
|
|
154
|
+
? 'Microphone access denied. Allow microphone access in your browser settings and try again.'
|
|
155
|
+
: "Accès au microphone refusé. Autorisez l'accès au microphone dans les paramètres du navigateur et réessayez.";
|
|
156
|
+
}
|
|
157
|
+
if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
|
|
158
|
+
return isEn
|
|
159
|
+
? 'No microphone found. Please connect a microphone and try again.'
|
|
160
|
+
: 'Aucun microphone trouvé. Connectez un microphone et réessayez.';
|
|
161
|
+
}
|
|
162
|
+
if (errorName === 'NotReadableError' || errorName === 'TrackStartError') {
|
|
163
|
+
return isEn
|
|
164
|
+
? 'Microphone is busy or unavailable. Close other apps using the microphone and try again.'
|
|
165
|
+
: 'Le microphone est occupé ou indisponible. Fermez les autres applications utilisant le microphone et réessayez.';
|
|
166
|
+
}
|
|
167
|
+
if (errorName === 'AbortError') {
|
|
168
|
+
return isEn
|
|
169
|
+
? 'Microphone access was interrupted. Please try again.'
|
|
170
|
+
: "L'accès au microphone a été interrompu. Veuillez réessayer.";
|
|
171
|
+
}
|
|
172
|
+
if (errorName === 'OverconstrainedError') {
|
|
173
|
+
return isEn
|
|
174
|
+
? 'Microphone does not support the required audio settings.'
|
|
175
|
+
: 'Le microphone ne supporte pas les paramètres audio requis.';
|
|
176
|
+
}
|
|
177
|
+
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'))) {
|
|
178
|
+
return isEn
|
|
179
|
+
? 'Microphone access requires HTTPS or localhost.'
|
|
180
|
+
: "L'accès au microphone nécessite HTTPS ou localhost.";
|
|
181
|
+
}
|
|
182
|
+
return isEn ? `Microphone error: ${error.message}` : `Erreur microphone : ${error.message}`;
|
|
183
|
+
}
|
|
184
|
+
getReadableErrorMessage(error, phase) {
|
|
185
|
+
const isEn = this.language === 'en';
|
|
186
|
+
const msg = error.message || '';
|
|
187
|
+
if (/401|unauthorized|invalid.{0,20}(api )?key|authentication failed/i.test(msg)) {
|
|
188
|
+
return isEn
|
|
189
|
+
? 'Authentication failed: invalid API key. Check your configuration.'
|
|
190
|
+
: "Échec d'authentification : clé API invalide. Vérifiez votre configuration.";
|
|
191
|
+
}
|
|
192
|
+
if (/403|forbidden/i.test(msg)) {
|
|
193
|
+
return isEn
|
|
194
|
+
? 'Access denied: check your API permissions.'
|
|
195
|
+
: 'Accès refusé : vérifiez vos permissions API.';
|
|
196
|
+
}
|
|
197
|
+
if (/429|rate.{0,10}limit|too many requests/i.test(msg)) {
|
|
198
|
+
return isEn
|
|
199
|
+
? 'Rate limit exceeded. Please wait a moment and try again.'
|
|
200
|
+
: 'Limite de requêtes dépassée. Veuillez attendre quelques secondes et réessayer.';
|
|
201
|
+
}
|
|
202
|
+
if (/404|not found/i.test(msg)) {
|
|
203
|
+
return isEn
|
|
204
|
+
? 'API endpoint not found. Check your proxy URL configuration.'
|
|
205
|
+
: "Endpoint API introuvable. Vérifiez l'URL de votre proxy.";
|
|
206
|
+
}
|
|
207
|
+
if (/5\d\d|server error|internal server|bad gateway|service unavailable/i.test(msg)) {
|
|
208
|
+
return isEn
|
|
209
|
+
? 'Server error. Please try again in a moment.'
|
|
210
|
+
: 'Erreur serveur. Veuillez réessayer dans un instant.';
|
|
211
|
+
}
|
|
212
|
+
if (/network|failed to fetch|err_connection|connection refused/i.test(msg)) {
|
|
213
|
+
return isEn
|
|
214
|
+
? 'Network error: check your connection and proxy server.'
|
|
215
|
+
: 'Erreur réseau : vérifiez votre connexion et le serveur proxy.';
|
|
216
|
+
}
|
|
217
|
+
if (/json|parse error|unexpected token|invalid response/i.test(msg)) {
|
|
218
|
+
return isEn
|
|
219
|
+
? 'Unexpected response from AI service. Please try again.'
|
|
220
|
+
: 'Réponse inattendue du service IA. Veuillez réessayer.';
|
|
221
|
+
}
|
|
222
|
+
if (phase === 'transcription') {
|
|
223
|
+
return isEn ? `Transcription failed: ${msg}` : `Échec de la transcription : ${msg}`;
|
|
224
|
+
}
|
|
225
|
+
if (phase === 'llm') {
|
|
226
|
+
return isEn ? `Form filling failed: ${msg}` : `Échec du remplissage du formulaire : ${msg}`;
|
|
227
|
+
}
|
|
228
|
+
if (phase === 'ocr') {
|
|
229
|
+
return isEn ? `OCR processing failed: ${msg}` : `Échec du traitement OCR : ${msg}`;
|
|
230
|
+
}
|
|
231
|
+
return isEn ? `Processing error: ${msg}` : `Erreur de traitement : ${msg}`;
|
|
232
|
+
}
|
|
113
233
|
async handleRecordClick() {
|
|
114
234
|
if (this.isProcessing)
|
|
115
235
|
return;
|
|
@@ -123,7 +243,7 @@ export class VoiceFormRecorder {
|
|
|
123
243
|
async startRecording() {
|
|
124
244
|
var _a;
|
|
125
245
|
try {
|
|
126
|
-
this.
|
|
246
|
+
this.hasTransientError = false;
|
|
127
247
|
this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
|
|
128
248
|
this.updateDebugInfo('Start Recording Attempt', {});
|
|
129
249
|
await this.audioRecorder.startRecording();
|
|
@@ -136,8 +256,8 @@ export class VoiceFormRecorder {
|
|
|
136
256
|
});
|
|
137
257
|
}
|
|
138
258
|
catch (error) {
|
|
139
|
-
this.
|
|
140
|
-
this.statusMessage =
|
|
259
|
+
this.hasTransientError = true;
|
|
260
|
+
this.statusMessage = this.getMicErrorMessage(error);
|
|
141
261
|
this.updateDebugInfo('Recording Error', { error: error.message });
|
|
142
262
|
this.formFilled.emit({
|
|
143
263
|
success: false,
|
|
@@ -147,40 +267,36 @@ export class VoiceFormRecorder {
|
|
|
147
267
|
}
|
|
148
268
|
async processJsonForm(ocrData) {
|
|
149
269
|
var _a, _b, _c;
|
|
270
|
+
this.isProcessing = true;
|
|
271
|
+
this.hasTransientError = false;
|
|
272
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing document...' : 'Traitement du document ...');
|
|
150
273
|
try {
|
|
151
|
-
this.isProcessing = true;
|
|
152
|
-
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
|
|
153
|
-
// Extract content from OCR format
|
|
154
274
|
const extractedData = (ocrData === null || ocrData === void 0 ? void 0 : ocrData.content) || ocrData;
|
|
155
275
|
const jsonForm = JSON.stringify(extractedData);
|
|
156
|
-
|
|
276
|
+
if (!extractedData) {
|
|
277
|
+
throw Object.assign(new Error('OCR returned no data'), { phase: 'ocr' });
|
|
278
|
+
}
|
|
157
279
|
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
158
280
|
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
159
|
-
|
|
160
|
-
|
|
281
|
+
let filledSchema;
|
|
282
|
+
try {
|
|
283
|
+
filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
|
|
284
|
+
}
|
|
285
|
+
catch (llmError) {
|
|
286
|
+
throw Object.assign(new Error(llmError.message), { phase: 'llm' });
|
|
287
|
+
}
|
|
161
288
|
this.filledData = this.extractFilledData(filledSchema);
|
|
162
|
-
this.updateDebugInfo('Form Filled', {
|
|
163
|
-
filledSchema,
|
|
164
|
-
extractedData: this.filledData
|
|
165
|
-
});
|
|
289
|
+
this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
|
|
166
290
|
this.parsedSchema = this.filledData;
|
|
167
|
-
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire
|
|
168
|
-
this.
|
|
169
|
-
// Emit success event
|
|
170
|
-
this.formFilled.emit({
|
|
171
|
-
success: true,
|
|
172
|
-
data: this.filledData,
|
|
173
|
-
jsonForm: jsonForm
|
|
174
|
-
});
|
|
291
|
+
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
|
|
292
|
+
this.formFilled.emit({ success: true, data: this.filledData, jsonForm });
|
|
175
293
|
}
|
|
176
294
|
catch (error) {
|
|
177
|
-
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
jsonForm: JSON.stringify(ocrData)
|
|
183
|
-
});
|
|
295
|
+
console.error('OCR processing error:', error);
|
|
296
|
+
this.hasTransientError = true;
|
|
297
|
+
this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'llm');
|
|
298
|
+
this.updateDebugInfo('OCR Processing Error', { phase: error.phase, error: error.message });
|
|
299
|
+
this.formFilled.emit({ success: false, error: error.message, jsonForm: JSON.stringify(ocrData) });
|
|
184
300
|
}
|
|
185
301
|
finally {
|
|
186
302
|
this.isProcessing = false;
|
|
@@ -188,83 +304,100 @@ export class VoiceFormRecorder {
|
|
|
188
304
|
}
|
|
189
305
|
async processAudioContent(audioFile) {
|
|
190
306
|
var _a, _b, _c;
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
});
|
|
307
|
+
this.isProcessing = true;
|
|
308
|
+
this.hasTransientError = false;
|
|
309
|
+
this.updateDebugInfo('Audio Captured', { size: audioFile.size, type: audioFile.type });
|
|
195
310
|
try {
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
311
|
+
// Validate audio file before sending to API
|
|
312
|
+
const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB (API limit)
|
|
313
|
+
if (audioFile.size <= 44) { // WAV header alone is 44 bytes; effectively empty
|
|
314
|
+
this.hasTransientError = true;
|
|
315
|
+
this.statusMessage = this.language == 'en'
|
|
316
|
+
? 'Recording is empty or too short. Please try again.'
|
|
317
|
+
: "L'enregistrement est vide ou trop court. Veuillez réessayer.";
|
|
318
|
+
this.formFilled.emit({ success: false, error: 'Empty or too short audio' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (audioFile.size > MAX_AUDIO_SIZE) {
|
|
322
|
+
this.hasTransientError = true;
|
|
323
|
+
this.statusMessage = this.language == 'en'
|
|
324
|
+
? `File too large (${Math.round(audioFile.size / 1024 / 1024)}MB). Maximum is 25MB.`
|
|
325
|
+
: `Fichier trop volumineux (${Math.round(audioFile.size / 1024 / 1024)}Mo). Maximum : 25Mo.`;
|
|
326
|
+
this.formFilled.emit({ success: false, error: 'Audio file too large' });
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Step 1: Transcription
|
|
330
|
+
let transcription;
|
|
331
|
+
try {
|
|
332
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
|
|
333
|
+
transcription = await this.speechToTextService.transcribe(audioFile, this.language);
|
|
334
|
+
this.transcription = transcription;
|
|
335
|
+
this.updateDebugInfo('Transcription Complete', { transcription });
|
|
336
|
+
}
|
|
337
|
+
catch (transcriptionError) {
|
|
338
|
+
throw Object.assign(new Error(transcriptionError.message), { phase: 'transcription' });
|
|
339
|
+
}
|
|
201
340
|
if (!transcription.trim()) {
|
|
202
|
-
|
|
341
|
+
this.hasTransientError = true;
|
|
342
|
+
this.statusMessage = this.language == 'en'
|
|
343
|
+
? 'No speech detected. Please speak clearly and try again.'
|
|
344
|
+
: 'Aucune parole détectée. Parlez clairement et réessayez.';
|
|
345
|
+
this.formFilled.emit({ success: false, error: 'No speech detected', transcription: '' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Step 2: LLM form filling
|
|
349
|
+
let filledSchema;
|
|
350
|
+
try {
|
|
351
|
+
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
352
|
+
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
353
|
+
filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
|
|
354
|
+
}
|
|
355
|
+
catch (llmError) {
|
|
356
|
+
throw Object.assign(new Error(llmError.message), { phase: 'llm' });
|
|
203
357
|
}
|
|
204
|
-
// Fill form using LLM
|
|
205
|
-
this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
|
|
206
|
-
const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
|
|
207
|
-
const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
|
|
208
|
-
// Extract filled data
|
|
209
358
|
this.filledData = this.extractFilledData(filledSchema);
|
|
210
|
-
this.updateDebugInfo('Form Filled', {
|
|
211
|
-
filledSchema,
|
|
212
|
-
extractedData: this.filledData
|
|
213
|
-
});
|
|
359
|
+
this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
|
|
214
360
|
this.parsedSchema = this.filledData;
|
|
215
|
-
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire
|
|
216
|
-
this.
|
|
217
|
-
// Emit success event
|
|
218
|
-
this.formFilled.emit({
|
|
219
|
-
success: true,
|
|
220
|
-
data: this.filledData,
|
|
221
|
-
transcription: transcription
|
|
222
|
-
});
|
|
361
|
+
this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
|
|
362
|
+
this.formFilled.emit({ success: true, data: this.filledData, transcription });
|
|
223
363
|
}
|
|
224
364
|
catch (error) {
|
|
225
|
-
console.error('
|
|
226
|
-
this.
|
|
227
|
-
this.statusMessage =
|
|
228
|
-
this.updateDebugInfo('
|
|
229
|
-
this.formFilled.emit({
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
365
|
+
console.error('Audio processing error:', error);
|
|
366
|
+
this.hasTransientError = true;
|
|
367
|
+
this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'audio-upload');
|
|
368
|
+
this.updateDebugInfo('Audio Processing Error', { phase: error.phase, error: error.message });
|
|
369
|
+
this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
this.isProcessing = false;
|
|
234
373
|
}
|
|
235
374
|
}
|
|
236
375
|
async stopRecordingAndProcess() {
|
|
237
376
|
var _a;
|
|
377
|
+
this.isRecording = false;
|
|
378
|
+
this.isProcessing = true;
|
|
379
|
+
this.hasTransientError = false;
|
|
380
|
+
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
|
|
381
|
+
this.updateDebugInfo('Stop Recording', {});
|
|
382
|
+
this.recordingStateChanged.emit({ isRecording: false, state: 'processing' });
|
|
238
383
|
try {
|
|
239
|
-
this.isRecording = false;
|
|
240
|
-
this.isProcessing = true;
|
|
241
|
-
this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
|
|
242
|
-
this.updateDebugInfo('Stop Recording', {});
|
|
243
|
-
this.recordingStateChanged.emit({
|
|
244
|
-
isRecording: false,
|
|
245
|
-
state: 'processing'
|
|
246
|
-
});
|
|
247
|
-
// Stop recording and get audio blob
|
|
248
384
|
const audioBlob = await this.audioRecorder.stopRecording();
|
|
249
|
-
const audioContent = new File([audioBlob], 'audio.
|
|
250
|
-
|
|
385
|
+
const audioContent = new File([audioBlob], 'audio.wav', { type: 'audio/wav' });
|
|
386
|
+
// processAudioContent manages isProcessing itself; await ensures we don't return early
|
|
387
|
+
await this.processAudioContent(audioContent);
|
|
251
388
|
}
|
|
252
389
|
catch (error) {
|
|
253
|
-
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
390
|
+
// Only reached if audioRecorder.stopRecording() itself throws
|
|
391
|
+
this.hasTransientError = true;
|
|
392
|
+
this.isProcessing = false;
|
|
393
|
+
this.statusMessage = (this.language == 'en'
|
|
394
|
+
? `Failed to stop recording: ${error.message}`
|
|
395
|
+
: `Impossible d'arrêter l'enregistrement : ${error.message}`);
|
|
396
|
+
this.updateDebugInfo('Stop Recording Error', { error: error.message });
|
|
397
|
+
this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
|
|
261
398
|
}
|
|
262
399
|
finally {
|
|
263
|
-
this.
|
|
264
|
-
this.recordingStateChanged.emit({
|
|
265
|
-
isRecording: false,
|
|
266
|
-
state: 'idle'
|
|
267
|
-
});
|
|
400
|
+
this.recordingStateChanged.emit({ isRecording: false, state: 'idle' });
|
|
268
401
|
}
|
|
269
402
|
}
|
|
270
403
|
extractFilledData(filledData) {
|
|
@@ -574,12 +707,14 @@ export class VoiceFormRecorder {
|
|
|
574
707
|
return (h("button", { class: buttonClass, style: buttonStyle, onClick: () => this.handleRecordClick(), disabled: isDisabled, "aria-label": this.isRecording ? 'Stop recording' : 'Start recording' }, this.isProcessing ? (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("circle", { cx: "12", cy: "12", r: "3" }, h("animate", { attributeName: "r", values: "3;6;3", dur: "1s", repeatCount: "indefinite" }), h("animate", { attributeName: "opacity", values: "1;0.3;1", dur: "1s", repeatCount: "indefinite" })))) : this.isRecording ? (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }))) : (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("circle", { cx: "12", cy: "12", r: "8" })))));
|
|
575
708
|
}
|
|
576
709
|
renderStatusMessage() {
|
|
710
|
+
const hasAnyError = this.hasError || this.hasTransientError;
|
|
577
711
|
const statusClass = [
|
|
578
712
|
'status-text',
|
|
579
|
-
|
|
580
|
-
this.filledData && !
|
|
713
|
+
hasAnyError && 'error',
|
|
714
|
+
this.filledData && !hasAnyError && 'success'
|
|
581
715
|
].filter(Boolean).join(' ');
|
|
582
|
-
|
|
716
|
+
const statusStyle = this.getStatusStyle();
|
|
717
|
+
return h("div", { class: statusClass, style: statusStyle }, this.statusMessage);
|
|
583
718
|
}
|
|
584
719
|
renderFormPreview() {
|
|
585
720
|
if (!this.parsedSchema)
|
|
@@ -752,8 +887,7 @@ export class VoiceFormRecorder {
|
|
|
752
887
|
}
|
|
753
888
|
render() {
|
|
754
889
|
const containerStyle = this.getContainerStyle();
|
|
755
|
-
|
|
756
|
-
return (h("div", { key: '8503450dbd4de2cb91172bd2bdbdf16a0c4cdce0' }, h("div", { key: '8b3f553574a64754312f68f2abe0d64f57c56d3e', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h("div", { key: '3d08a37d611bc91dbe11c065c633f0d36c8b362e', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? h("div", { class: "status-text", style: statusStyle }, this.statusMessage) : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
|
|
890
|
+
return (h("div", { key: 'd2eb6b74840a57809920b3dbba3fd7dfbeef8531' }, h("div", { key: 'e66e1d4fc23176f9024ca20413535440a8be3ce8', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, 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() : "")));
|
|
757
891
|
}
|
|
758
892
|
static get is() { return "voice-input-module"; }
|
|
759
893
|
static get encapsulation() { return "shadow"; }
|
|
@@ -888,6 +1022,58 @@ export class VoiceFormRecorder {
|
|
|
888
1022
|
"reflect": false,
|
|
889
1023
|
"defaultValue": "'gpt-5-mini'"
|
|
890
1024
|
},
|
|
1025
|
+
"transcriptionProvider": {
|
|
1026
|
+
"type": "string",
|
|
1027
|
+
"attribute": "transcription-provider",
|
|
1028
|
+
"mutable": false,
|
|
1029
|
+
"complexType": {
|
|
1030
|
+
"original": "TranscriptionProvider",
|
|
1031
|
+
"resolved": "\"mistral\" | \"openai\"",
|
|
1032
|
+
"references": {
|
|
1033
|
+
"TranscriptionProvider": {
|
|
1034
|
+
"location": "import",
|
|
1035
|
+
"path": "../../types/service-providers.types",
|
|
1036
|
+
"id": "src/types/service-providers.types.ts::TranscriptionProvider"
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
"required": false,
|
|
1041
|
+
"optional": false,
|
|
1042
|
+
"docs": {
|
|
1043
|
+
"tags": [],
|
|
1044
|
+
"text": ""
|
|
1045
|
+
},
|
|
1046
|
+
"getter": false,
|
|
1047
|
+
"setter": false,
|
|
1048
|
+
"reflect": false,
|
|
1049
|
+
"defaultValue": "'openai'"
|
|
1050
|
+
},
|
|
1051
|
+
"completionProvider": {
|
|
1052
|
+
"type": "string",
|
|
1053
|
+
"attribute": "completion-provider",
|
|
1054
|
+
"mutable": false,
|
|
1055
|
+
"complexType": {
|
|
1056
|
+
"original": "CompletionProvider",
|
|
1057
|
+
"resolved": "\"anthropic\" | \"mistral\" | \"openai\"",
|
|
1058
|
+
"references": {
|
|
1059
|
+
"CompletionProvider": {
|
|
1060
|
+
"location": "import",
|
|
1061
|
+
"path": "../../types/service-providers.types",
|
|
1062
|
+
"id": "src/types/service-providers.types.ts::CompletionProvider"
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
"required": false,
|
|
1067
|
+
"optional": false,
|
|
1068
|
+
"docs": {
|
|
1069
|
+
"tags": [],
|
|
1070
|
+
"text": ""
|
|
1071
|
+
},
|
|
1072
|
+
"getter": false,
|
|
1073
|
+
"setter": false,
|
|
1074
|
+
"reflect": false,
|
|
1075
|
+
"defaultValue": "'openai'"
|
|
1076
|
+
},
|
|
891
1077
|
"context": {
|
|
892
1078
|
"type": "string",
|
|
893
1079
|
"attribute": "context",
|
|
@@ -1056,6 +1242,7 @@ export class VoiceFormRecorder {
|
|
|
1056
1242
|
"isProcessing": {},
|
|
1057
1243
|
"statusMessage": {},
|
|
1058
1244
|
"hasError": {},
|
|
1245
|
+
"hasTransientError": {},
|
|
1059
1246
|
"transcription": {},
|
|
1060
1247
|
"filledData": {},
|
|
1061
1248
|
"debugInfo": {},
|
|
@@ -1219,6 +1406,18 @@ export class VoiceFormRecorder {
|
|
|
1219
1406
|
}, {
|
|
1220
1407
|
"propName": "theme",
|
|
1221
1408
|
"methodName": "initializeServices"
|
|
1409
|
+
}, {
|
|
1410
|
+
"propName": "transcriptionProvider",
|
|
1411
|
+
"methodName": "initializeServices"
|
|
1412
|
+
}, {
|
|
1413
|
+
"propName": "completionProvider",
|
|
1414
|
+
"methodName": "initializeServices"
|
|
1415
|
+
}, {
|
|
1416
|
+
"propName": "transcriptionModel",
|
|
1417
|
+
"methodName": "initializeServices"
|
|
1418
|
+
}, {
|
|
1419
|
+
"propName": "completionModel",
|
|
1420
|
+
"methodName": "initializeServices"
|
|
1222
1421
|
}];
|
|
1223
1422
|
}
|
|
1224
1423
|
}
|