sera-ai 1.0.28 → 1.0.30
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/index.js +219 -455
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -455
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -151,12 +151,12 @@ var useFFmpegConverter = () => {
|
|
|
151
151
|
const [statusMessage, setStatusMessage] = React3.useState("");
|
|
152
152
|
const loadFFmpeg = React3.useCallback(async () => {
|
|
153
153
|
if (ffmpegInstance) {
|
|
154
|
-
console.log("[
|
|
154
|
+
console.log("[SERA] FFmpeg already loaded, skipping");
|
|
155
155
|
setFfmpegLoaded(true);
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
if (ffmpegLoadingPromise) {
|
|
159
|
-
console.log("[
|
|
159
|
+
console.log("[SERA] FFmpeg already loading, waiting for existing promise");
|
|
160
160
|
const result = await ffmpegLoadingPromise;
|
|
161
161
|
if (result) {
|
|
162
162
|
setFfmpegLoaded(true);
|
|
@@ -166,7 +166,7 @@ var useFFmpegConverter = () => {
|
|
|
166
166
|
ffmpegLoadingPromise = (async () => {
|
|
167
167
|
try {
|
|
168
168
|
const cdnCorePath = "https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js";
|
|
169
|
-
console.log(
|
|
169
|
+
console.log(`[SERA] FFmpeg loading from CDN | corePath=${cdnCorePath}`);
|
|
170
170
|
setStatusMessage("Loading FFmpeg...");
|
|
171
171
|
const ffmpeg$1 = ffmpeg.createFFmpeg({
|
|
172
172
|
log: false,
|
|
@@ -180,10 +180,10 @@ var useFFmpegConverter = () => {
|
|
|
180
180
|
setFfmpegLoaded(true);
|
|
181
181
|
setIsLoaded(true);
|
|
182
182
|
setStatusMessage("");
|
|
183
|
-
console.log("[
|
|
183
|
+
console.log("[SERA] FFmpeg WASM loaded successfully from CDN");
|
|
184
184
|
return true;
|
|
185
185
|
} catch (err) {
|
|
186
|
-
console.error("
|
|
186
|
+
console.error("[SERA] FFmpeg loading failed:", err);
|
|
187
187
|
setError("Failed to load FFmpeg");
|
|
188
188
|
setStatusMessage("");
|
|
189
189
|
ffmpegLoadingPromise = null;
|
|
@@ -253,13 +253,13 @@ var useFFmpegConverter = () => {
|
|
|
253
253
|
if (!ffmpegInstance || !ffmpegLoaded) {
|
|
254
254
|
const loaded = await loadFFmpeg();
|
|
255
255
|
if (!loaded) {
|
|
256
|
-
console.error("
|
|
256
|
+
console.error("[SERA] FFmpeg loading failed for FLAC conversion");
|
|
257
257
|
return wavFile;
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
const ffmpeg$1 = ffmpegInstance;
|
|
261
261
|
if (!ffmpeg$1) {
|
|
262
|
-
console.error("FFmpeg not available");
|
|
262
|
+
console.error("[SERA] FFmpeg not available for FLAC conversion");
|
|
263
263
|
return wavFile;
|
|
264
264
|
}
|
|
265
265
|
try {
|
|
@@ -293,7 +293,7 @@ var useFFmpegConverter = () => {
|
|
|
293
293
|
const flacFile = new File([flacBlob], flacFileName, { type: "audio/flac" });
|
|
294
294
|
setProgress(100);
|
|
295
295
|
setStatusMessage("FLAC conversion complete");
|
|
296
|
-
console.log(`[
|
|
296
|
+
console.log(`[SERA] FLAC conversion complete | inputSize=${wavFile.size}, outputSize=${flacFile.size}, reduction=${Math.round((1 - flacFile.size / wavFile.size) * 100)}%`);
|
|
297
297
|
setTimeout(() => {
|
|
298
298
|
setIsConverting(false);
|
|
299
299
|
setProgress(0);
|
|
@@ -301,7 +301,7 @@ var useFFmpegConverter = () => {
|
|
|
301
301
|
}, 500);
|
|
302
302
|
return flacFile;
|
|
303
303
|
} catch (err) {
|
|
304
|
-
console.error("FLAC conversion failed:", err);
|
|
304
|
+
console.error("[SERA] FLAC conversion failed:", err);
|
|
305
305
|
setError("FLAC conversion failed");
|
|
306
306
|
setIsConverting(false);
|
|
307
307
|
setProgress(0);
|
|
@@ -318,7 +318,7 @@ var useFFmpegConverter = () => {
|
|
|
318
318
|
}
|
|
319
319
|
const maxFileSize = 50 * 1024 * 1024;
|
|
320
320
|
if (file.size > maxFileSize) {
|
|
321
|
-
console.warn(`[
|
|
321
|
+
console.warn(`[SERA] Silence removal skipped, file too large | size=${file.size}`);
|
|
322
322
|
return file;
|
|
323
323
|
}
|
|
324
324
|
try {
|
|
@@ -326,12 +326,12 @@ var useFFmpegConverter = () => {
|
|
|
326
326
|
setError(null);
|
|
327
327
|
setProgress(0);
|
|
328
328
|
setStatusMessage("Removing silence...");
|
|
329
|
-
console.log(`[
|
|
329
|
+
console.log(`[SERA] Silence removal started | size=${file.size}, name=${file.name}`);
|
|
330
330
|
if (!ffmpegInstance) {
|
|
331
331
|
setStatusMessage("Loading FFmpeg for silence removal...");
|
|
332
332
|
const loaded = await loadFFmpeg();
|
|
333
333
|
if (!loaded || !ffmpegInstance) {
|
|
334
|
-
console.error("[
|
|
334
|
+
console.error("[SERA] FFmpeg loading failed for silence removal");
|
|
335
335
|
setIsConverting(false);
|
|
336
336
|
setProgress(0);
|
|
337
337
|
setStatusMessage("");
|
|
@@ -345,7 +345,7 @@ var useFFmpegConverter = () => {
|
|
|
345
345
|
const wavData = await ffmpeg.fetchFile(file);
|
|
346
346
|
ffmpegInstance.FS("writeFile", inputFileName, wavData);
|
|
347
347
|
const originalSize = file.size;
|
|
348
|
-
console.log(`[
|
|
348
|
+
console.log(`[SERA] Silence removal input written | size=${originalSize}`);
|
|
349
349
|
setProgress(30);
|
|
350
350
|
setStatusMessage("Analyzing and removing silence...");
|
|
351
351
|
await ffmpegInstance.run(
|
|
@@ -364,11 +364,11 @@ var useFFmpegConverter = () => {
|
|
|
364
364
|
ffmpegInstance.FS("unlink", inputFileName);
|
|
365
365
|
ffmpegInstance.FS("unlink", outputFileName);
|
|
366
366
|
} catch (cleanupErr) {
|
|
367
|
-
console.warn("[
|
|
367
|
+
console.warn("[SERA] Silence removal cleanup warning:", cleanupErr);
|
|
368
368
|
}
|
|
369
369
|
const processedSize = outputData.length;
|
|
370
370
|
const reductionPercent = Math.round((1 - processedSize / originalSize) * 100);
|
|
371
|
-
console.log(`[
|
|
371
|
+
console.log(`[SERA] Silence removal complete | inputSize=${originalSize}, outputSize=${processedSize}, reduction=${reductionPercent}%`);
|
|
372
372
|
const processedFile = new File(
|
|
373
373
|
[new Uint8Array(outputData)],
|
|
374
374
|
file.name,
|
|
@@ -383,7 +383,7 @@ var useFFmpegConverter = () => {
|
|
|
383
383
|
}, 500);
|
|
384
384
|
return processedFile;
|
|
385
385
|
} catch (err) {
|
|
386
|
-
console.error("[
|
|
386
|
+
console.error("[SERA] Silence removal failed:", err);
|
|
387
387
|
setError("Silence removal failed");
|
|
388
388
|
setIsConverting(false);
|
|
389
389
|
setProgress(0);
|
|
@@ -422,7 +422,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
422
422
|
const DB_VERSION = 1;
|
|
423
423
|
const createAudioEncodingWorker = () => {
|
|
424
424
|
const workerCode = `
|
|
425
|
-
console.log("Audio encoding worker loaded");
|
|
425
|
+
console.log("[SERA] Audio encoding worker loaded");
|
|
426
426
|
|
|
427
427
|
self.onmessage = function (e) {
|
|
428
428
|
const { command, data, id } = e.data;
|
|
@@ -441,11 +441,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
441
441
|
const float32Array =
|
|
442
442
|
audioData instanceof Float32Array ? audioData : new Float32Array(audioData);
|
|
443
443
|
|
|
444
|
-
console.log("Worker encoding audio
|
|
445
|
-
length: float32Array.length,
|
|
446
|
-
firstSamples: Array.from(float32Array.slice(0, 10)),
|
|
447
|
-
hasValidData: Array.from(float32Array).some((sample) => sample !== 0),
|
|
448
|
-
});
|
|
444
|
+
console.log("[SERA] Worker encoding audio | samples=" + float32Array.length);
|
|
449
445
|
|
|
450
446
|
const buffer = new ArrayBuffer(float32Array.length * 4);
|
|
451
447
|
const view = new Float32Array(buffer);
|
|
@@ -499,11 +495,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
499
495
|
|
|
500
496
|
const float32Result = new Float32Array(bytes.buffer);
|
|
501
497
|
|
|
502
|
-
console.log("Worker decoded audio
|
|
503
|
-
length: float32Result.length,
|
|
504
|
-
firstSamples: Array.from(float32Result.slice(0, 10)),
|
|
505
|
-
hasValidData: Array.from(float32Result).some((sample) => sample !== 0),
|
|
506
|
-
});
|
|
498
|
+
console.log("[SERA] Worker decoded audio | samples=" + float32Result.length);
|
|
507
499
|
|
|
508
500
|
self.postMessage({
|
|
509
501
|
type: "complete",
|
|
@@ -528,7 +520,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
528
520
|
});
|
|
529
521
|
}
|
|
530
522
|
} catch (error) {
|
|
531
|
-
console.error("
|
|
523
|
+
console.error("[SERA] Audio encoding worker error:", error);
|
|
532
524
|
self.postMessage({
|
|
533
525
|
type: "error",
|
|
534
526
|
id,
|
|
@@ -539,7 +531,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
539
531
|
};
|
|
540
532
|
|
|
541
533
|
self.onerror = function (error) {
|
|
542
|
-
console.error("
|
|
534
|
+
console.error("[SERA] Audio encoding worker script error:", error);
|
|
543
535
|
self.postMessage({
|
|
544
536
|
type: "error",
|
|
545
537
|
error: error.message || "Unknown worker error",
|
|
@@ -559,7 +551,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
559
551
|
const { type, id, result, error, progress, message } = e.data;
|
|
560
552
|
const operation = pendingOperations.current.get(id);
|
|
561
553
|
if (!operation) {
|
|
562
|
-
console.warn(`No pending operation found
|
|
554
|
+
console.warn(`[SERA] No pending worker operation found | id=${id}`);
|
|
563
555
|
return;
|
|
564
556
|
}
|
|
565
557
|
switch (type) {
|
|
@@ -576,16 +568,16 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
576
568
|
}
|
|
577
569
|
};
|
|
578
570
|
workerRef.current.onerror = (error) => {
|
|
579
|
-
console.error("Audio encoding worker error:", error);
|
|
571
|
+
console.error("[SERA] Audio encoding worker error:", error);
|
|
580
572
|
for (const [id, operation] of pendingOperations.current) {
|
|
581
573
|
operation.reject(new Error("Worker crashed"));
|
|
582
574
|
}
|
|
583
575
|
pendingOperations.current.clear();
|
|
584
576
|
workerRef.current = null;
|
|
585
577
|
};
|
|
586
|
-
console.log("
|
|
578
|
+
console.log("[SERA] Audio encoding worker initialized");
|
|
587
579
|
} catch (error) {
|
|
588
|
-
console.error("
|
|
580
|
+
console.error("[SERA] Audio encoding worker initialization failed:", error);
|
|
589
581
|
workerRef.current = null;
|
|
590
582
|
}
|
|
591
583
|
}
|
|
@@ -595,7 +587,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
595
587
|
async (audioData) => {
|
|
596
588
|
const worker = initWorker();
|
|
597
589
|
if (!worker) {
|
|
598
|
-
console.warn("Worker not available, falling back to main thread encoding");
|
|
590
|
+
console.warn("[SERA] Worker not available, falling back to main thread encoding");
|
|
599
591
|
return float32ArrayToBase64(audioData);
|
|
600
592
|
}
|
|
601
593
|
const operationId = `encode_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -635,7 +627,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
635
627
|
async (base64Data) => {
|
|
636
628
|
const worker = initWorker();
|
|
637
629
|
if (!worker) {
|
|
638
|
-
console.warn("Worker not available, falling back to main thread decoding");
|
|
630
|
+
console.warn("[SERA] Worker not available, falling back to main thread decoding");
|
|
639
631
|
return base64ToFloat32Array(base64Data);
|
|
640
632
|
}
|
|
641
633
|
const operationId = `decode_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -677,7 +669,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
677
669
|
return new Promise((resolve, reject) => {
|
|
678
670
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
679
671
|
request.onerror = () => {
|
|
680
|
-
console.error("Failed to open IndexedDB:", request.error);
|
|
672
|
+
console.error("[SERA] Failed to open IndexedDB:", request.error);
|
|
681
673
|
reject(request.error);
|
|
682
674
|
};
|
|
683
675
|
request.onsuccess = () => {
|
|
@@ -735,16 +727,16 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
735
727
|
await new Promise((resolve, reject) => {
|
|
736
728
|
const request = store.add(session);
|
|
737
729
|
request.onsuccess = () => {
|
|
738
|
-
console.log(`
|
|
730
|
+
console.log(`[SERA] Audio session created | sessionId=${sessionId}`);
|
|
739
731
|
resolve();
|
|
740
732
|
};
|
|
741
733
|
request.onerror = () => {
|
|
742
|
-
console.error("Failed to create session:", request.error);
|
|
734
|
+
console.error("[SERA] Failed to create audio session:", request.error);
|
|
743
735
|
reject(request.error);
|
|
744
736
|
};
|
|
745
737
|
});
|
|
746
738
|
} catch (error) {
|
|
747
|
-
console.error("Error creating session:", error);
|
|
739
|
+
console.error("[SERA] Error creating audio session:", error);
|
|
748
740
|
throw error;
|
|
749
741
|
}
|
|
750
742
|
},
|
|
@@ -785,7 +777,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
785
777
|
getRequest.onerror = () => reject(getRequest.error);
|
|
786
778
|
});
|
|
787
779
|
} catch (error) {
|
|
788
|
-
console.error(`Failed to append chunk
|
|
780
|
+
console.error(`[SERA] Failed to append audio chunk | sequence=${sequence}:`, error);
|
|
789
781
|
}
|
|
790
782
|
},
|
|
791
783
|
[initDB, encodeInWorker]
|
|
@@ -804,26 +796,24 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
804
796
|
if (!session) return false;
|
|
805
797
|
const audioChunks = [];
|
|
806
798
|
const chunkIndices = Object.keys(session.audioChunks).map(Number).sort((a, b) => a - b);
|
|
807
|
-
console.log(
|
|
808
|
-
`Found ${chunkIndices.length} chunks with indices: ${chunkIndices.slice(0, 10)}...`
|
|
809
|
-
);
|
|
799
|
+
console.log(`[SERA] Retrying session | sessionId=${sessionId}, chunks=${chunkIndices.length}`);
|
|
810
800
|
for (const index of chunkIndices) {
|
|
811
801
|
try {
|
|
812
802
|
const base64Data = session.audioChunks[index];
|
|
813
803
|
const float32Array = await decodeInWorker(base64Data);
|
|
814
804
|
audioChunks.push(float32Array);
|
|
815
805
|
} catch (error) {
|
|
816
|
-
console.error(`Failed to decode chunk
|
|
806
|
+
console.error(`[SERA] Failed to decode audio chunk | index=${index}:`, error);
|
|
817
807
|
}
|
|
818
808
|
}
|
|
819
809
|
if (audioChunks.length === 0) {
|
|
820
|
-
console.error("No valid chunks found");
|
|
810
|
+
console.error("[SERA] No valid audio chunks found for retry");
|
|
821
811
|
return false;
|
|
822
812
|
}
|
|
823
813
|
await reprocessSession(audioChunks, session.metadata);
|
|
824
814
|
return true;
|
|
825
815
|
} catch (error) {
|
|
826
|
-
console.error("
|
|
816
|
+
console.error("[SERA] Session retry failed:", error);
|
|
827
817
|
return false;
|
|
828
818
|
}
|
|
829
819
|
},
|
|
@@ -838,16 +828,16 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
838
828
|
await new Promise((resolve, reject) => {
|
|
839
829
|
const request = store.delete(sessionId);
|
|
840
830
|
request.onsuccess = () => {
|
|
841
|
-
console.log(`Deleted completed session
|
|
831
|
+
console.log(`[SERA] Deleted completed session | sessionId=${sessionId}`);
|
|
842
832
|
resolve();
|
|
843
833
|
};
|
|
844
834
|
request.onerror = () => {
|
|
845
|
-
console.error("Failed to delete session:", request.error);
|
|
835
|
+
console.error("[SERA] Failed to delete session:", request.error);
|
|
846
836
|
reject(request.error);
|
|
847
837
|
};
|
|
848
838
|
});
|
|
849
839
|
} catch (error) {
|
|
850
|
-
console.error("Error marking session complete:", error);
|
|
840
|
+
console.error("[SERA] Error marking session complete:", error);
|
|
851
841
|
throw error;
|
|
852
842
|
}
|
|
853
843
|
},
|
|
@@ -871,7 +861,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
871
861
|
session.lastError = error;
|
|
872
862
|
const updateRequest = store.put(session);
|
|
873
863
|
updateRequest.onsuccess = () => {
|
|
874
|
-
console.log(`
|
|
864
|
+
console.log(`[SERA] Session marked as failed | sessionId=${sessionId}`);
|
|
875
865
|
resolve();
|
|
876
866
|
};
|
|
877
867
|
updateRequest.onerror = () => reject(updateRequest.error);
|
|
@@ -879,7 +869,7 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
879
869
|
getRequest.onerror = () => reject(getRequest.error);
|
|
880
870
|
});
|
|
881
871
|
} catch (error2) {
|
|
882
|
-
console.error("Error marking session failed:", error2);
|
|
872
|
+
console.error("[SERA] Error marking session failed:", error2);
|
|
883
873
|
throw error2;
|
|
884
874
|
}
|
|
885
875
|
},
|
|
@@ -894,16 +884,16 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
894
884
|
await new Promise((resolve, reject) => {
|
|
895
885
|
const request = store.delete(sessionId);
|
|
896
886
|
request.onsuccess = () => {
|
|
897
|
-
console.log(`Deleted session
|
|
887
|
+
console.log(`[SERA] Deleted session | sessionId=${sessionId}`);
|
|
898
888
|
resolve();
|
|
899
889
|
};
|
|
900
890
|
request.onerror = () => {
|
|
901
|
-
console.error("Failed to delete session:", request.error);
|
|
891
|
+
console.error("[SERA] Failed to delete session:", request.error);
|
|
902
892
|
reject(request.error);
|
|
903
893
|
};
|
|
904
894
|
});
|
|
905
895
|
} catch (error) {
|
|
906
|
-
console.error("Error deleting session:", error);
|
|
896
|
+
console.error("[SERA] Error deleting session:", error);
|
|
907
897
|
throw error;
|
|
908
898
|
}
|
|
909
899
|
},
|
|
@@ -921,12 +911,12 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
921
911
|
resolve(request.result[0]);
|
|
922
912
|
};
|
|
923
913
|
request.onerror = () => {
|
|
924
|
-
console.error("Failed to get failed sessions:", request.error);
|
|
914
|
+
console.error("[SERA] Failed to get failed sessions:", request.error);
|
|
925
915
|
reject(request.error);
|
|
926
916
|
};
|
|
927
917
|
});
|
|
928
918
|
} catch (error) {
|
|
929
|
-
console.error("Error getting failed sessions:", error);
|
|
919
|
+
console.error("[SERA] Error getting failed sessions:", error);
|
|
930
920
|
return null;
|
|
931
921
|
}
|
|
932
922
|
}, [initDB]);
|
|
@@ -942,22 +932,22 @@ var useAudioRecovery = (reprocessSession) => {
|
|
|
942
932
|
await new Promise((resolve, reject) => {
|
|
943
933
|
const request = store.clear();
|
|
944
934
|
request.onsuccess = () => {
|
|
945
|
-
console.log("Cleared all audio sessions");
|
|
935
|
+
console.log("[SERA] Cleared all audio sessions");
|
|
946
936
|
resolve();
|
|
947
937
|
};
|
|
948
938
|
request.onerror = () => {
|
|
949
|
-
console.error("Failed to clear sessions:", request.error);
|
|
939
|
+
console.error("[SERA] Failed to clear sessions:", request.error);
|
|
950
940
|
reject(request.error);
|
|
951
941
|
};
|
|
952
942
|
});
|
|
953
943
|
} catch (error) {
|
|
954
|
-
console.error("Error clearing sessions:", error);
|
|
944
|
+
console.error("[SERA] Error clearing sessions:", error);
|
|
955
945
|
throw error;
|
|
956
946
|
}
|
|
957
947
|
}, [initDB]);
|
|
958
948
|
const cleanup = React3.useCallback(() => {
|
|
959
949
|
if (workerRef.current) {
|
|
960
|
-
console.log("
|
|
950
|
+
console.log("[SERA] Cleaning up audio encoding worker");
|
|
961
951
|
workerRef.current.terminate();
|
|
962
952
|
workerRef.current = null;
|
|
963
953
|
}
|
|
@@ -1089,7 +1079,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1089
1079
|
let sessionId = "";
|
|
1090
1080
|
let metadata = {};
|
|
1091
1081
|
let speciality = "";
|
|
1092
|
-
console.log(
|
|
1082
|
+
console.log(`[SERA] Parsing HL7 transcription | segments=${segments.length}`);
|
|
1093
1083
|
for (const segment of segments) {
|
|
1094
1084
|
switch (segment.type) {
|
|
1095
1085
|
case "MSH":
|
|
@@ -1131,7 +1121,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1131
1121
|
const parsedClassification = JSON.parse(classificationJson);
|
|
1132
1122
|
Object.assign(classifiedInfo, parsedClassification);
|
|
1133
1123
|
} catch (e) {
|
|
1134
|
-
console.error("Failed to parse classification JSON:", e);
|
|
1124
|
+
console.error("[SERA] Failed to parse HL7 classification JSON:", e);
|
|
1135
1125
|
}
|
|
1136
1126
|
}
|
|
1137
1127
|
}
|
|
@@ -1182,7 +1172,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1182
1172
|
}
|
|
1183
1173
|
};
|
|
1184
1174
|
} catch (error) {
|
|
1185
|
-
console.error("HL7 transcription conversion error:", error);
|
|
1175
|
+
console.error("[SERA] HL7 transcription conversion error:", error);
|
|
1186
1176
|
throw new Error(
|
|
1187
1177
|
`Failed to convert HL7 transcription data: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1188
1178
|
);
|
|
@@ -1195,7 +1185,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1195
1185
|
try {
|
|
1196
1186
|
const segments = parseHL7(hl7Data);
|
|
1197
1187
|
const specialities = [];
|
|
1198
|
-
console.log(
|
|
1188
|
+
console.log(`[SERA] Parsing HL7 specialities | segments=${segments.length}`);
|
|
1199
1189
|
let currentSpeciality = null;
|
|
1200
1190
|
for (const segment of segments) {
|
|
1201
1191
|
switch (segment.type) {
|
|
@@ -1237,7 +1227,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1237
1227
|
specialities.push(specialityData);
|
|
1238
1228
|
}
|
|
1239
1229
|
} catch (e) {
|
|
1240
|
-
console.error("Failed to parse legacy specialities JSON data:", e);
|
|
1230
|
+
console.error("[SERA] Failed to parse legacy specialities JSON data:", e);
|
|
1241
1231
|
}
|
|
1242
1232
|
}
|
|
1243
1233
|
}
|
|
@@ -1250,10 +1240,10 @@ var useHL7FHIRConverter = () => {
|
|
|
1250
1240
|
}
|
|
1251
1241
|
specialities.push(currentSpeciality);
|
|
1252
1242
|
}
|
|
1253
|
-
console.log(
|
|
1243
|
+
console.log(`[SERA] Parsed specialities | count=${specialities.length}`);
|
|
1254
1244
|
return { specialities };
|
|
1255
1245
|
} catch (error) {
|
|
1256
|
-
console.error("HL7 specialities conversion error:", error);
|
|
1246
|
+
console.error("[SERA] HL7 specialities conversion error:", error);
|
|
1257
1247
|
throw new Error(
|
|
1258
1248
|
`Failed to convert HL7 specialities data: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1259
1249
|
);
|
|
@@ -1300,8 +1290,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1300
1290
|
classification = conclusionData.classifiedInfo;
|
|
1301
1291
|
}
|
|
1302
1292
|
} catch (parseError) {
|
|
1303
|
-
console.error("
|
|
1304
|
-
console.log("Raw conclusion:", resource.conclusion);
|
|
1293
|
+
console.error("[SERA] Failed to parse DiagnosticReport conclusion:", parseError);
|
|
1305
1294
|
if (!classification["Clinical Summary"]) {
|
|
1306
1295
|
classification["Clinical Summary"] = [];
|
|
1307
1296
|
}
|
|
@@ -1320,16 +1309,13 @@ var useHL7FHIRConverter = () => {
|
|
|
1320
1309
|
break;
|
|
1321
1310
|
case "Composition":
|
|
1322
1311
|
if (resource.section && Array.isArray(resource.section) && resource.section.length > 0) {
|
|
1323
|
-
console.log(
|
|
1312
|
+
console.log(`[SERA] Processing FHIR Composition | sections=${resource.section.length}`);
|
|
1324
1313
|
for (const section of resource.section) {
|
|
1325
1314
|
if (section.title === "Transcription" && section.text?.div) {
|
|
1326
1315
|
const textContent = section.text.div.replace(/<[^>]*>/g, "");
|
|
1327
1316
|
transcription += (transcription ? "\n" : "") + textContent;
|
|
1328
|
-
console.log(
|
|
1317
|
+
console.log(`[SERA] Extracted transcription text | chars=${textContent.length}`);
|
|
1329
1318
|
} else if (section.title === "Medical Classification") {
|
|
1330
|
-
console.log(
|
|
1331
|
-
"\u2139\uFE0F Skipping Medical Classification section (handled by DiagnosticReport)"
|
|
1332
|
-
);
|
|
1333
1319
|
}
|
|
1334
1320
|
}
|
|
1335
1321
|
} else if (resource.text?.div) {
|
|
@@ -1338,10 +1324,8 @@ var useHL7FHIRConverter = () => {
|
|
|
1338
1324
|
}
|
|
1339
1325
|
if (resource.identifier) {
|
|
1340
1326
|
const identifiers = Array.isArray(resource.identifier) ? resource.identifier : [resource.identifier];
|
|
1341
|
-
console.log(`\u{1F4CB} Found ${identifiers.length} identifier(s)`);
|
|
1342
1327
|
for (let j = 0; j < identifiers.length; j++) {
|
|
1343
1328
|
const identifier = identifiers[j];
|
|
1344
|
-
console.log(` [${j}] system: ${identifier.system}, value: ${identifier.value}`);
|
|
1345
1329
|
if (identifier.system === "http://nuxera.ai/session-identifier" && identifier.value) {
|
|
1346
1330
|
sessionId = identifier.value;
|
|
1347
1331
|
break;
|
|
@@ -1350,10 +1334,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1350
1334
|
}
|
|
1351
1335
|
if (!sessionId && resource.id) {
|
|
1352
1336
|
sessionId = resource.id;
|
|
1353
|
-
console.log(
|
|
1354
|
-
"\u26A0\uFE0F [Priority 2] Using Composition.id as fallback session ID:",
|
|
1355
|
-
sessionId
|
|
1356
|
-
);
|
|
1337
|
+
console.log(`[SERA] Using Composition.id as fallback sessionId | sessionId=${sessionId}`);
|
|
1357
1338
|
}
|
|
1358
1339
|
break;
|
|
1359
1340
|
case "Task":
|
|
@@ -1361,10 +1342,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1361
1342
|
for (const identifier of resource.identifier) {
|
|
1362
1343
|
if (identifier.system === "http://nuxera.ai/session-id" && identifier.value) {
|
|
1363
1344
|
sessionId = identifier.value;
|
|
1364
|
-
console.log(
|
|
1365
|
-
"\u2705 [Priority 3] Extracted session ID from Task.identifier:",
|
|
1366
|
-
sessionId
|
|
1367
|
-
);
|
|
1345
|
+
console.log(`[SERA] Extracted sessionId from Task.identifier | sessionId=${sessionId}`);
|
|
1368
1346
|
break;
|
|
1369
1347
|
}
|
|
1370
1348
|
}
|
|
@@ -1374,15 +1352,15 @@ var useHL7FHIRConverter = () => {
|
|
|
1374
1352
|
}
|
|
1375
1353
|
if (!sessionId && fhirData.resourceType === "Bundle" && fhirData.identifier?.value) {
|
|
1376
1354
|
sessionId = fhirData.identifier.value;
|
|
1377
|
-
console.log(
|
|
1355
|
+
console.log(`[SERA] Using Bundle.identifier as fallback sessionId | sessionId=${sessionId}`);
|
|
1378
1356
|
}
|
|
1379
1357
|
if (!sessionId && fhirData.resourceType === "Bundle" && fhirData.id) {
|
|
1380
1358
|
sessionId = fhirData.id;
|
|
1381
|
-
console.log(
|
|
1359
|
+
console.log(`[SERA] Using Bundle.id as fallback sessionId | sessionId=${sessionId}`);
|
|
1382
1360
|
}
|
|
1383
1361
|
if (!sessionId) {
|
|
1384
1362
|
sessionId = `session_${Date.now()}`;
|
|
1385
|
-
console.warn(
|
|
1363
|
+
console.warn(`[SERA] No sessionId found in FHIR response, generated | sessionId=${sessionId}`);
|
|
1386
1364
|
}
|
|
1387
1365
|
return {
|
|
1388
1366
|
transcription: transcription || "No transcription found in FHIR data",
|
|
@@ -1395,7 +1373,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1395
1373
|
}
|
|
1396
1374
|
};
|
|
1397
1375
|
} catch (error) {
|
|
1398
|
-
console.error("
|
|
1376
|
+
console.error("[SERA] FHIR transcription conversion error:", error);
|
|
1399
1377
|
throw new Error(
|
|
1400
1378
|
`Failed to convert FHIR transcription data: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1401
1379
|
);
|
|
@@ -1441,7 +1419,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1441
1419
|
} catch (error) {
|
|
1442
1420
|
const errorMessage = error instanceof Error ? error.message : "Unknown conversion error";
|
|
1443
1421
|
setConversionError(errorMessage);
|
|
1444
|
-
console.error("Transcription conversion failed:", error);
|
|
1422
|
+
console.error("[SERA] Transcription conversion failed:", error);
|
|
1445
1423
|
return {
|
|
1446
1424
|
transcription: "Conversion failed - see raw response",
|
|
1447
1425
|
classification: { "Conversion Error": [errorMessage] },
|
|
@@ -1537,7 +1515,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1537
1515
|
)}|||||F|||${timestamp}`
|
|
1538
1516
|
);
|
|
1539
1517
|
const hl7Message = hl7Lines.join("\r\n") + "\r\n";
|
|
1540
|
-
console.log(
|
|
1518
|
+
console.log(`[SERA] HL7 transcription request constructed | size=${hl7Message.length}`);
|
|
1541
1519
|
const formData = new FormData();
|
|
1542
1520
|
const hl7Blob = new Blob([hl7Message], { type: "text/plain" });
|
|
1543
1521
|
formData.append("hl7_request", hl7Blob, "transcription_request.hl7");
|
|
@@ -1792,10 +1770,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1792
1770
|
});
|
|
1793
1771
|
}
|
|
1794
1772
|
}
|
|
1795
|
-
console.log(
|
|
1796
|
-
"Constructed FHIR Transcription Request Bundle:",
|
|
1797
|
-
JSON.stringify(fhirBundle, null, 2)
|
|
1798
|
-
);
|
|
1773
|
+
console.log(`[SERA] FHIR transcription request constructed | entries=${fhirBundle.entry.length}`);
|
|
1799
1774
|
const formData = new FormData();
|
|
1800
1775
|
const fhirBlob = new Blob([JSON.stringify(fhirBundle, null, 2)], {
|
|
1801
1776
|
type: "application/fhir+json"
|
|
@@ -1807,10 +1782,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1807
1782
|
}
|
|
1808
1783
|
formData.append("sequence", requestData.sequence.toString());
|
|
1809
1784
|
formData.append("isFinalChunk", requestData.isFinalChunk.toString());
|
|
1810
|
-
console.log(
|
|
1811
|
-
"Created FHIR FormData with session ID:",
|
|
1812
|
-
requestData.sessionId || "none (first call)"
|
|
1813
|
-
);
|
|
1785
|
+
console.log(`[SERA] FHIR FormData created | sessionId=${requestData.sessionId || "none"}`);
|
|
1814
1786
|
return formData;
|
|
1815
1787
|
},
|
|
1816
1788
|
[formatFHIRDate, mapFHIRGender]
|
|
@@ -1882,16 +1854,7 @@ var useHL7FHIRConverter = () => {
|
|
|
1882
1854
|
`OBX|${obxSequence++}|TX|REQUEST_TYPE^Request Type||DICTATION|||||F|||${timestamp}`
|
|
1883
1855
|
);
|
|
1884
1856
|
const hl7Message = hl7Lines.join("\r\n") + "\r\n";
|
|
1885
|
-
console.log(
|
|
1886
|
-
console.log("Request Data:", requestData);
|
|
1887
|
-
console.log("Audio File:", {
|
|
1888
|
-
name: audioFile.name,
|
|
1889
|
-
size: audioFile.size,
|
|
1890
|
-
type: audioFile.type
|
|
1891
|
-
});
|
|
1892
|
-
console.log("HL7 Message:");
|
|
1893
|
-
console.log(hl7Message);
|
|
1894
|
-
console.log("=== End Debug ===");
|
|
1857
|
+
console.log(`[SERA] HL7 dictation request constructed | size=${hl7Message.length}, audioFile=${audioFile.name}, audioSize=${audioFile.size}`);
|
|
1895
1858
|
const formData = new FormData();
|
|
1896
1859
|
formData.append("audio", audioFile);
|
|
1897
1860
|
const hl7Blob = new Blob([hl7Message], { type: "text/plain; charset=utf-8" });
|
|
@@ -2046,15 +2009,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2046
2009
|
}
|
|
2047
2010
|
]
|
|
2048
2011
|
};
|
|
2049
|
-
console.log(
|
|
2050
|
-
console.log("Request Data:", requestData);
|
|
2051
|
-
console.log("Audio File:", {
|
|
2052
|
-
name: audioFile.name,
|
|
2053
|
-
size: audioFile.size,
|
|
2054
|
-
type: audioFile.type
|
|
2055
|
-
});
|
|
2056
|
-
console.log("FHIR Bundle:", JSON.stringify(fhirBundle, null, 2));
|
|
2057
|
-
console.log("=== End Debug ===");
|
|
2012
|
+
console.log(`[SERA] FHIR dictation request constructed | entries=${fhirBundle.entry.length}, audioFile=${audioFile.name}, audioSize=${audioFile.size}`);
|
|
2058
2013
|
const formData = new FormData();
|
|
2059
2014
|
formData.append("audio", audioFile);
|
|
2060
2015
|
const fhirBlob = new Blob([JSON.stringify(fhirBundle, null, 2)], {
|
|
@@ -2070,7 +2025,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2070
2025
|
try {
|
|
2071
2026
|
const segments = parseHL7(hl7Data);
|
|
2072
2027
|
let dictationResponse = {};
|
|
2073
|
-
console.log(
|
|
2028
|
+
console.log(`[SERA] Parsing HL7 dictation | segments=${segments.length}`);
|
|
2074
2029
|
for (const segment of segments) {
|
|
2075
2030
|
switch (segment.type) {
|
|
2076
2031
|
case "MSH":
|
|
@@ -2165,7 +2120,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2165
2120
|
);
|
|
2166
2121
|
}
|
|
2167
2122
|
} catch (e) {
|
|
2168
|
-
console.warn("Could not calculate processing time:", e);
|
|
2123
|
+
console.warn("[SERA] Could not calculate processing time:", e);
|
|
2169
2124
|
}
|
|
2170
2125
|
}
|
|
2171
2126
|
const finalResponse = {
|
|
@@ -2175,10 +2130,10 @@ var useHL7FHIRConverter = () => {
|
|
|
2175
2130
|
language: dictationResponse.language,
|
|
2176
2131
|
processingTimes: dictationResponse.processingTimes
|
|
2177
2132
|
};
|
|
2178
|
-
console.log(
|
|
2133
|
+
console.log(`[SERA] HL7 dictation converted | sessionId=${finalResponse.sessionId}, dictationLength=${finalResponse.dictation.length}`);
|
|
2179
2134
|
return finalResponse;
|
|
2180
2135
|
} catch (error) {
|
|
2181
|
-
console.error("HL7 dictation conversion error:", error);
|
|
2136
|
+
console.error("[SERA] HL7 dictation conversion error:", error);
|
|
2182
2137
|
throw new Error(
|
|
2183
2138
|
`Failed to convert HL7 dictation data: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2184
2139
|
);
|
|
@@ -2191,20 +2146,16 @@ var useHL7FHIRConverter = () => {
|
|
|
2191
2146
|
let dictationResponse = {};
|
|
2192
2147
|
let parsedData = fhirData;
|
|
2193
2148
|
if (typeof fhirData === "string") {
|
|
2194
|
-
console.log("\u{1F4DD} Response is a string, parsing JSON...");
|
|
2195
2149
|
try {
|
|
2196
2150
|
parsedData = JSON.parse(fhirData);
|
|
2197
|
-
console.log("\u2705 JSON parsed successfully");
|
|
2198
2151
|
} catch (e) {
|
|
2199
|
-
console.error("
|
|
2152
|
+
console.error("[SERA] Failed to parse FHIR dictation JSON string:", e);
|
|
2200
2153
|
throw new Error("Invalid JSON response from server");
|
|
2201
2154
|
}
|
|
2202
2155
|
}
|
|
2203
|
-
console.log("Parsing FHIR dictation data:", parsedData);
|
|
2204
2156
|
let resources = [];
|
|
2205
2157
|
if (parsedData.resourceType === "Bundle" && parsedData.entry) {
|
|
2206
2158
|
resources = parsedData.entry.map((entry) => entry.resource);
|
|
2207
|
-
console.log(`\u{1F4E6} Found ${resources.length} resources in Bundle`);
|
|
2208
2159
|
} else if (parsedData.resourceType) {
|
|
2209
2160
|
resources = [parsedData];
|
|
2210
2161
|
} else if (parsedData.resource) {
|
|
@@ -2212,26 +2163,17 @@ var useHL7FHIRConverter = () => {
|
|
|
2212
2163
|
}
|
|
2213
2164
|
for (let i = 0; i < resources.length; i++) {
|
|
2214
2165
|
const resource = resources[i];
|
|
2215
|
-
console.log(`
|
|
2216
|
-
--- Processing resource [${i}]: ${resource?.resourceType || "unknown"} ---`);
|
|
2217
2166
|
if (!resource || !resource.resourceType) continue;
|
|
2218
2167
|
switch (resource.resourceType) {
|
|
2219
2168
|
case "MessageHeader":
|
|
2220
2169
|
dictationResponse.sessionId = resource.id;
|
|
2221
|
-
console.log("\u2705 Session ID from MessageHeader:", resource.id);
|
|
2222
2170
|
break;
|
|
2223
2171
|
case "Media":
|
|
2224
|
-
console.log("\u{1F3AF} Found Media resource for dictation");
|
|
2225
2172
|
if (resource.extension && Array.isArray(resource.extension)) {
|
|
2226
|
-
console.log(`\u{1F4CB} Processing ${resource.extension.length} extensions`);
|
|
2227
2173
|
for (const ext of resource.extension) {
|
|
2228
|
-
console.log(
|
|
2229
|
-
` Extension URL: ${ext.url}, valueString: ${ext.valueString || ext.valueInteger}`
|
|
2230
|
-
);
|
|
2231
2174
|
switch (ext.url) {
|
|
2232
2175
|
case "http://nuxera.ai/extensions/transcription":
|
|
2233
2176
|
dictationResponse.dictation = ext.valueString || "";
|
|
2234
|
-
console.log("\u2705\u2705 Extracted dictation text:", dictationResponse.dictation);
|
|
2235
2177
|
break;
|
|
2236
2178
|
case "http://nuxera.ai/extensions/audio-size":
|
|
2237
2179
|
if (!dictationResponse.processingTimes) {
|
|
@@ -2241,17 +2183,12 @@ var useHL7FHIRConverter = () => {
|
|
|
2241
2183
|
if (sizeMatch) {
|
|
2242
2184
|
dictationResponse.processingTimes.audioSizeMB = parseFloat(sizeMatch[1]);
|
|
2243
2185
|
}
|
|
2244
|
-
console.log(
|
|
2245
|
-
"\u2705 Extracted audio size:",
|
|
2246
|
-
dictationResponse.processingTimes.audioSizeMB
|
|
2247
|
-
);
|
|
2248
2186
|
break;
|
|
2249
2187
|
case "http://nuxera.ai/extensions/processing-timestamp":
|
|
2250
2188
|
if (!dictationResponse.processingTimes) {
|
|
2251
2189
|
dictationResponse.processingTimes = {};
|
|
2252
2190
|
}
|
|
2253
2191
|
dictationResponse.processingTimes.requestTimestamp = ext.valueInteger;
|
|
2254
|
-
console.log("\u2705 Extracted processing timestamp:", ext.valueInteger);
|
|
2255
2192
|
break;
|
|
2256
2193
|
case "http://nuxera.ai/extensions/confidence":
|
|
2257
2194
|
dictationResponse.confidence = ext.valueDecimal || ext.valueInteger;
|
|
@@ -2276,11 +2213,9 @@ var useHL7FHIRConverter = () => {
|
|
|
2276
2213
|
}
|
|
2277
2214
|
if (!dictationResponse.sessionId && resource.identifier && resource.identifier[0]) {
|
|
2278
2215
|
dictationResponse.sessionId = resource.identifier[0].value;
|
|
2279
|
-
console.log("\u2705 Session ID from Media.identifier:", dictationResponse.sessionId);
|
|
2280
2216
|
}
|
|
2281
2217
|
if (!dictationResponse.sessionId && resource.id) {
|
|
2282
2218
|
dictationResponse.sessionId = resource.id;
|
|
2283
|
-
console.log("\u2705 Session ID from Media.id:", dictationResponse.sessionId);
|
|
2284
2219
|
}
|
|
2285
2220
|
if (resource.createdDateTime && dictationResponse.processingTimes) {
|
|
2286
2221
|
dictationResponse.processingTimes.processedAt = resource.createdDateTime;
|
|
@@ -2369,14 +2304,9 @@ var useHL7FHIRConverter = () => {
|
|
|
2369
2304
|
dictationResponse.processingTimes.totalProcessingMs = Math.round(
|
|
2370
2305
|
processedAtMs - requestMs
|
|
2371
2306
|
);
|
|
2372
|
-
console.log(
|
|
2373
|
-
"\u2705 Calculated total processing time:",
|
|
2374
|
-
dictationResponse.processingTimes.totalProcessingMs,
|
|
2375
|
-
"ms"
|
|
2376
|
-
);
|
|
2377
2307
|
}
|
|
2378
2308
|
} catch (e) {
|
|
2379
|
-
console.warn("Could not calculate processing time:", e);
|
|
2309
|
+
console.warn("[SERA] Could not calculate FHIR dictation processing time:", e);
|
|
2380
2310
|
}
|
|
2381
2311
|
}
|
|
2382
2312
|
const finalResponse = {
|
|
@@ -2386,10 +2316,10 @@ var useHL7FHIRConverter = () => {
|
|
|
2386
2316
|
language: dictationResponse.language,
|
|
2387
2317
|
processingTimes: dictationResponse.processingTimes
|
|
2388
2318
|
};
|
|
2389
|
-
console.log(
|
|
2319
|
+
console.log(`[SERA] FHIR dictation converted | sessionId=${finalResponse.sessionId}, dictationLength=${finalResponse.dictation.length}`);
|
|
2390
2320
|
return finalResponse;
|
|
2391
2321
|
} catch (error) {
|
|
2392
|
-
console.error("
|
|
2322
|
+
console.error("[SERA] FHIR dictation conversion error:", error);
|
|
2393
2323
|
throw new Error(
|
|
2394
2324
|
`Failed to convert FHIR dictation data: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
2395
2325
|
);
|
|
@@ -2423,7 +2353,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2423
2353
|
} catch (error) {
|
|
2424
2354
|
const errorMessage = error instanceof Error ? error.message : "Unknown conversion error";
|
|
2425
2355
|
setConversionError(errorMessage);
|
|
2426
|
-
console.error("Dictation conversion failed:", error);
|
|
2356
|
+
console.error("[SERA] Dictation conversion failed:", error);
|
|
2427
2357
|
return {
|
|
2428
2358
|
dictation: "",
|
|
2429
2359
|
sessionId: null,
|
|
@@ -2498,12 +2428,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2498
2428
|
)}|||||F|||${timestamp}`
|
|
2499
2429
|
);
|
|
2500
2430
|
const hl7Message = hl7Lines.join("\r\n") + "\r\n";
|
|
2501
|
-
console.log(
|
|
2502
|
-
console.log("Operation:", operation);
|
|
2503
|
-
console.log("API Key Data:", apiKeyData);
|
|
2504
|
-
console.log("HL7 Message:");
|
|
2505
|
-
console.log(hl7Message);
|
|
2506
|
-
console.log("=== End Debug ===");
|
|
2431
|
+
console.log(`[SERA] HL7 API key request constructed | operation=${operation}, size=${hl7Message.length}`);
|
|
2507
2432
|
return hl7Message;
|
|
2508
2433
|
},
|
|
2509
2434
|
[escapeHL7]
|
|
@@ -2663,11 +2588,7 @@ var useHL7FHIRConverter = () => {
|
|
|
2663
2588
|
});
|
|
2664
2589
|
}
|
|
2665
2590
|
}
|
|
2666
|
-
console.log(
|
|
2667
|
-
console.log("Operation:", operation);
|
|
2668
|
-
console.log("API Key Data:", apiKeyData);
|
|
2669
|
-
console.log("FHIR Bundle:", JSON.stringify(fhirBundle, null, 2));
|
|
2670
|
-
console.log("=== End Debug ===");
|
|
2591
|
+
console.log(`[SERA] FHIR API key request constructed | operation=${operation}, entries=${fhirBundle.entry.length}`);
|
|
2671
2592
|
return JSON.stringify(fhirBundle);
|
|
2672
2593
|
},
|
|
2673
2594
|
[]
|
|
@@ -2844,9 +2765,6 @@ var createAudioProcessorWorker = () => {
|
|
|
2844
2765
|
});
|
|
2845
2766
|
this._audioLevelCheckInterval = 0;
|
|
2846
2767
|
|
|
2847
|
-
// Debug: Log audio capture status every few seconds
|
|
2848
|
-
if (this._audioLevelCheckInterval % 1000 === 0) {
|
|
2849
|
-
}
|
|
2850
2768
|
}
|
|
2851
2769
|
|
|
2852
2770
|
if (audioLevel > this._audioThreshold) {
|
|
@@ -2902,7 +2820,6 @@ var createAudioProcessorWorker = () => {
|
|
|
2902
2820
|
},
|
|
2903
2821
|
[flat.buffer]
|
|
2904
2822
|
);
|
|
2905
|
-
console.log('[UPLOAD] Sending chunk: ' + (this._bufferSize / this._sampleRate).toFixed(1) + 's');
|
|
2906
2823
|
// Clear buffer after upload
|
|
2907
2824
|
this._buffer = [];
|
|
2908
2825
|
this._bufferSize = 0;
|
|
@@ -2987,31 +2904,17 @@ var useAudioRecorder = ({
|
|
|
2987
2904
|
clearFailedSessions
|
|
2988
2905
|
} = useAudioRecovery_default(async (audioChunks, metadata) => {
|
|
2989
2906
|
try {
|
|
2990
|
-
console.log("\u{1F504} Retry callback started with audio chunks:", {
|
|
2991
|
-
chunksCount: audioChunks.length,
|
|
2992
|
-
totalSamples: audioChunks.reduce((sum, chunk) => sum + chunk.length, 0),
|
|
2993
|
-
chunkDetails: audioChunks.map((chunk, idx) => ({
|
|
2994
|
-
index: idx,
|
|
2995
|
-
length: chunk.length,
|
|
2996
|
-
hasData: chunk.length > 0
|
|
2997
|
-
}))
|
|
2998
|
-
});
|
|
2999
2907
|
if (audioChunks.length === 0) {
|
|
3000
2908
|
throw new Error("No audio chunks provided for retry");
|
|
3001
2909
|
}
|
|
3002
2910
|
const combinedAudio = combineAudioChunks(audioChunks);
|
|
3003
|
-
console.log(
|
|
3004
|
-
combinedLength: combinedAudio.length,
|
|
3005
|
-
hasAudio: combinedAudio.length > 0
|
|
3006
|
-
});
|
|
2911
|
+
console.log(`[SERA] Recovery retry | chunks=${audioChunks.length}, samples=${combinedAudio.length}`);
|
|
3007
2912
|
if (combinedAudio.length === 0) {
|
|
3008
2913
|
throw new Error("Combined audio is empty");
|
|
3009
2914
|
}
|
|
3010
2915
|
if (!uploadChunkToServerRef.current) {
|
|
3011
2916
|
throw new Error("Upload function not yet initialized");
|
|
3012
2917
|
}
|
|
3013
|
-
const newSessionId = `session_retry_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3014
|
-
console.log("\u{1F194} Generated new retry session ID:", newSessionId);
|
|
3015
2918
|
await uploadChunkToServerRef.current(
|
|
3016
2919
|
combinedAudio,
|
|
3017
2920
|
true,
|
|
@@ -3020,9 +2923,9 @@ var useAudioRecorder = ({
|
|
|
3020
2923
|
false,
|
|
3021
2924
|
metadata.sampleRate
|
|
3022
2925
|
);
|
|
3023
|
-
console.log("[
|
|
2926
|
+
console.log("[SERA] Recovery retry completed successfully");
|
|
3024
2927
|
} catch (error2) {
|
|
3025
|
-
console.error("[
|
|
2928
|
+
console.error("[SERA] Recovery retry failed:", error2);
|
|
3026
2929
|
throw error2;
|
|
3027
2930
|
}
|
|
3028
2931
|
});
|
|
@@ -3049,7 +2952,6 @@ var useAudioRecorder = ({
|
|
|
3049
2952
|
const ffmpegLoadedRef = React3__namespace.default.useRef(ffmpegLoaded);
|
|
3050
2953
|
React3__namespace.default.useEffect(() => {
|
|
3051
2954
|
ffmpegLoadedRef.current = ffmpegLoaded;
|
|
3052
|
-
console.log(`\u{1F504} ffmpegLoadedRef updated to: ${ffmpegLoaded}`);
|
|
3053
2955
|
}, [ffmpegLoaded]);
|
|
3054
2956
|
React3__namespace.default.useEffect(() => {
|
|
3055
2957
|
selectedModelRef.current = selectedModel;
|
|
@@ -3064,11 +2966,6 @@ var useAudioRecorder = ({
|
|
|
3064
2966
|
selectedFormatRef.current = selectedFormat;
|
|
3065
2967
|
}, [selectedFormat]);
|
|
3066
2968
|
React3__namespace.default.useEffect(() => {
|
|
3067
|
-
console.log("Triggering onTranscriptionUpdate with:", {
|
|
3068
|
-
alreadyDoneTranscription,
|
|
3069
|
-
sessionId: sessionIdRef.current,
|
|
3070
|
-
localSessionId: localSessionIdRef.current
|
|
3071
|
-
});
|
|
3072
2969
|
if (alreadyDoneTranscription.length > 0) {
|
|
3073
2970
|
onTranscriptionUpdate(
|
|
3074
2971
|
alreadyDoneTranscription,
|
|
@@ -3076,30 +2973,21 @@ var useAudioRecorder = ({
|
|
|
3076
2973
|
);
|
|
3077
2974
|
}
|
|
3078
2975
|
}, [alreadyDoneTranscription]);
|
|
3079
|
-
React3__namespace.default.useEffect(() => {
|
|
3080
|
-
console.log("Speciality changed in useAudioRecorder:", speciality);
|
|
3081
|
-
}, [speciality]);
|
|
3082
2976
|
React3__namespace.default.useEffect(() => {
|
|
3083
2977
|
let isMounted = true;
|
|
3084
2978
|
if (!ffmpegLoadedRef.current) {
|
|
3085
2979
|
(async () => {
|
|
3086
|
-
console.log("Initializing FFmpeg WASM\u2026");
|
|
3087
2980
|
try {
|
|
3088
2981
|
const ok = await loadFFmpeg();
|
|
3089
|
-
if (isMounted) {
|
|
3090
|
-
console.log("FFmpeg
|
|
3091
|
-
if (ok) {
|
|
3092
|
-
console.log("FFmpeg WASM initialized successfully for audio recorder");
|
|
3093
|
-
}
|
|
2982
|
+
if (isMounted && ok) {
|
|
2983
|
+
console.log("[SERA] FFmpeg WASM initialized");
|
|
3094
2984
|
}
|
|
3095
2985
|
} catch (error2) {
|
|
3096
2986
|
if (isMounted) {
|
|
3097
|
-
console.error("FFmpeg WASM initialization failed:", error2);
|
|
2987
|
+
console.error("[SERA] FFmpeg WASM initialization failed:", error2);
|
|
3098
2988
|
}
|
|
3099
2989
|
}
|
|
3100
2990
|
})();
|
|
3101
|
-
} else {
|
|
3102
|
-
console.log("FFmpeg WASM already initialized, skipping");
|
|
3103
2991
|
}
|
|
3104
2992
|
return () => {
|
|
3105
2993
|
isMounted = false;
|
|
@@ -3123,12 +3011,11 @@ var useAudioRecorder = ({
|
|
|
3123
3011
|
const track = audioTracks[0];
|
|
3124
3012
|
const settings = track.getSettings();
|
|
3125
3013
|
setCurrentDeviceId(settings.deviceId || null);
|
|
3126
|
-
console.log();
|
|
3127
3014
|
}
|
|
3128
3015
|
testStream.getTracks().forEach((track) => track.stop());
|
|
3129
3016
|
return true;
|
|
3130
3017
|
} catch (error2) {
|
|
3131
|
-
console.error("Microphone validation failed:", error2);
|
|
3018
|
+
console.error("[SERA] Microphone validation failed:", error2);
|
|
3132
3019
|
if (error2 instanceof Error) {
|
|
3133
3020
|
let errorMessage = "";
|
|
3134
3021
|
if (error2.name === "NotFoundError" || error2.name === "DevicesNotFoundError") {
|
|
@@ -3151,9 +3038,6 @@ var useAudioRecorder = ({
|
|
|
3151
3038
|
}
|
|
3152
3039
|
}, [currentDeviceId]);
|
|
3153
3040
|
React3__namespace.default.useEffect(() => {
|
|
3154
|
-
if (speciality) {
|
|
3155
|
-
console.log("Speciality set, microphone validation will happen on recording start");
|
|
3156
|
-
}
|
|
3157
3041
|
}, [speciality]);
|
|
3158
3042
|
const clearAllSessions = React3__namespace.default.useCallback(async () => {
|
|
3159
3043
|
await clearFailedSessions();
|
|
@@ -3162,30 +3046,16 @@ var useAudioRecorder = ({
|
|
|
3162
3046
|
const uploadChunkToServer = React3__namespace.default.useCallback(
|
|
3163
3047
|
async (audioData, isFinalChunk, sequence, retry = false, isPausedChunk = false, sampleRateOverride) => {
|
|
3164
3048
|
const currentFfmpegLoaded = ffmpegLoadedRef.current;
|
|
3165
|
-
console.log(
|
|
3166
|
-
ffmpegLoaded: currentFfmpegLoaded,
|
|
3167
|
-
silenceRemovalEnabled: removeSilenceRef.current,
|
|
3168
|
-
hasRemoveSilenceFunction: typeof removeSilence === "function",
|
|
3169
|
-
isFinalChunk,
|
|
3170
|
-
sequence,
|
|
3171
|
-
audioDataLength: audioData?.length,
|
|
3172
|
-
requestFormat: selectedFormatRef.current,
|
|
3173
|
-
sessionHasFailed: sessionHasFailedChunkRef.current
|
|
3174
|
-
});
|
|
3049
|
+
console.log(`[SERA] Step 6: Upload chunk | sequence=${sequence}, isFinal=${isFinalChunk}, samples=${audioData?.length || 0}, format=${selectedFormatRef.current}`);
|
|
3175
3050
|
processorRef.current?.port.postMessage({ command: "resetUploadChunk" });
|
|
3176
3051
|
if (audioData && localSessionIdRef.current && !retry) {
|
|
3177
3052
|
try {
|
|
3178
|
-
console.log(`[DB] Saving chunk ${sequence} to IndexedDB session ${localSessionIdRef.current}`);
|
|
3179
3053
|
await appendAudioToSession(localSessionIdRef.current, audioData, sequence);
|
|
3180
|
-
console.log(`[DB] \u2713 Successfully saved audio chunk ${sequence} to IndexedDB (${audioData.length} samples)`);
|
|
3181
3054
|
} catch (error2) {
|
|
3182
|
-
console.error(`[
|
|
3055
|
+
console.error(`[SERA] IndexedDB save failed for chunk ${sequence}:`, error2);
|
|
3183
3056
|
}
|
|
3184
|
-
} else {
|
|
3185
|
-
console.log(`[DB] Skipping IndexedDB save: audioData=${!!audioData}, sessionId=${localSessionIdRef.current}, retry=${retry}`);
|
|
3186
3057
|
}
|
|
3187
3058
|
if (sessionHasFailedChunkRef.current && !retry) {
|
|
3188
|
-
console.log(`[SKIP] Session has failed chunk - skipping server upload for sequence ${sequence}, continuing to record audio`);
|
|
3189
3059
|
if (isFinalChunk) {
|
|
3190
3060
|
setShowRetrySessionPrompt(true);
|
|
3191
3061
|
setIsProcessing(false);
|
|
@@ -3193,151 +3063,96 @@ var useAudioRecorder = ({
|
|
|
3193
3063
|
return;
|
|
3194
3064
|
}
|
|
3195
3065
|
try {
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
if (amplitude > maxAmplitude) maxAmplitude = amplitude;
|
|
3219
|
-
if (amplitude > 1e-3) {
|
|
3220
|
-
nonZeroSamples++;
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
const audioPercentage = nonZeroSamples / audioData.length;
|
|
3224
|
-
console.log(
|
|
3225
|
-
`[AUDIO] Audio stats: maxAmplitude=${maxAmplitude.toFixed(4)}, audioContent=${(audioPercentage * 100).toFixed(2)}%, sequence=${sequence}, isFinal=${isFinalChunk}`
|
|
3226
|
-
);
|
|
3227
|
-
console.log(`[WAV] Converting to WAV with sample rate: ${currentSampleRate}Hz`);
|
|
3228
|
-
const timestamp = Date.now();
|
|
3229
|
-
const fileName = `audio-chunk-${timestamp}.wav`;
|
|
3230
|
-
let wavFile = await convertToWav(audioData, currentSampleRate, fileName);
|
|
3231
|
-
if (!wavFile) {
|
|
3232
|
-
throw new Error("WAV conversion failed through FFmpeg");
|
|
3233
|
-
}
|
|
3234
|
-
console.log(`[INFO] Original WAV file: ${wavFile.size} bytes, ${wavFile.name}`);
|
|
3235
|
-
if (currentFfmpegLoaded && removeSilenceRef.current) {
|
|
3236
|
-
try {
|
|
3237
|
-
console.log("[SILENCE] Attempting to remove silence from audio chunk...");
|
|
3238
|
-
const processedFile = await removeSilence(wavFile);
|
|
3239
|
-
if (processedFile) {
|
|
3240
|
-
console.log(
|
|
3241
|
-
`[SUCCESS] Silence removed successfully: ${processedFile.size} bytes (was ${wavFile.size} bytes)`
|
|
3242
|
-
);
|
|
3243
|
-
if (processedFile.size < 1e3) {
|
|
3244
|
-
console.warn(
|
|
3245
|
-
`[WARN] Processed file very small (${processedFile.size} bytes), using original file`
|
|
3246
|
-
);
|
|
3247
|
-
} else {
|
|
3248
|
-
wavFile = processedFile;
|
|
3249
|
-
}
|
|
3250
|
-
} else {
|
|
3251
|
-
console.warn("Silence removal returned null, using original file");
|
|
3252
|
-
}
|
|
3253
|
-
} catch (silenceError) {
|
|
3254
|
-
console.warn("Silence removal failed, using original file:", silenceError);
|
|
3255
|
-
}
|
|
3256
|
-
} else {
|
|
3257
|
-
console.log("[SILENCE] Silence removal disabled or not loaded");
|
|
3258
|
-
}
|
|
3259
|
-
let audioFile = wavFile;
|
|
3260
|
-
try {
|
|
3261
|
-
console.log("[FLAC] Converting WAV to FLAC...");
|
|
3262
|
-
const flacFile = await convertToFlac(wavFile);
|
|
3263
|
-
if (flacFile && flacFile.type === "audio/flac") {
|
|
3264
|
-
audioFile = flacFile;
|
|
3265
|
-
console.log(
|
|
3266
|
-
`[FLAC] Conversion successful: ${wavFile.size} bytes WAV \u2192 ${flacFile.size} bytes FLAC`
|
|
3267
|
-
);
|
|
3268
|
-
} else {
|
|
3269
|
-
console.warn("[FLAC] Conversion failed, using WAV file");
|
|
3270
|
-
}
|
|
3271
|
-
} catch (flacError) {
|
|
3272
|
-
console.warn("[FLAC] Conversion error, using WAV file:", flacError);
|
|
3066
|
+
if (!audioData || audioData.length === 0) {
|
|
3067
|
+
if (retry) {
|
|
3068
|
+
throw new pRetry.AbortError("No audio data provided for retry");
|
|
3069
|
+
}
|
|
3070
|
+
throw new Error("No audio data provided");
|
|
3071
|
+
}
|
|
3072
|
+
const currentSampleRate = sampleRateOverride ?? recordingSampleRateRef.current ?? audioContextRef.current?.sampleRate;
|
|
3073
|
+
if (!isValidSampleRate(currentSampleRate)) {
|
|
3074
|
+
throw new InvalidSampleRateError(currentSampleRate);
|
|
3075
|
+
}
|
|
3076
|
+
console.log(`[SERA] Step 7: Audio preparation | ${audioData.length} samples, ${(audioData.length / currentSampleRate).toFixed(1)}s, ${currentSampleRate}Hz`);
|
|
3077
|
+
const timestamp = Date.now();
|
|
3078
|
+
const fileName = `audio-chunk-${timestamp}.wav`;
|
|
3079
|
+
let wavFile = await convertToWav(audioData, currentSampleRate, fileName);
|
|
3080
|
+
if (!wavFile) {
|
|
3081
|
+
throw new Error("WAV conversion failed through FFmpeg");
|
|
3082
|
+
}
|
|
3083
|
+
if (currentFfmpegLoaded && removeSilenceRef.current) {
|
|
3084
|
+
try {
|
|
3085
|
+
const processedFile = await removeSilence(wavFile);
|
|
3086
|
+
if (processedFile && processedFile.size >= 1e3) {
|
|
3087
|
+
wavFile = processedFile;
|
|
3273
3088
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3089
|
+
} catch (silenceError) {
|
|
3090
|
+
console.warn("[SERA] Silence removal failed, using original file:", silenceError);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
let audioFile = wavFile;
|
|
3094
|
+
try {
|
|
3095
|
+
const flacFile = await convertToFlac(wavFile);
|
|
3096
|
+
if (flacFile && flacFile.type === "audio/flac") {
|
|
3097
|
+
audioFile = flacFile;
|
|
3098
|
+
}
|
|
3099
|
+
} catch (flacError) {
|
|
3100
|
+
console.warn("[SERA] FLAC conversion failed, using WAV file:", flacError);
|
|
3101
|
+
}
|
|
3102
|
+
console.log(`[SERA] Step 8: Audio file ready | ${audioFile.name}, ${audioFile.size} bytes`);
|
|
3103
|
+
const patientDetailsPayload = patientDetails;
|
|
3104
|
+
const requestData = {
|
|
3105
|
+
sessionId: retry ? void 0 : sessionIdRef.current || void 0,
|
|
3106
|
+
model: selectedModelRef.current,
|
|
3107
|
+
doctorName,
|
|
3108
|
+
patientDetails: patientDetailsPayload,
|
|
3109
|
+
removeSilence: removeSilenceRef.current,
|
|
3110
|
+
skipDiarization: skipDiarizationRef.current,
|
|
3111
|
+
isFinalChunk,
|
|
3112
|
+
isPaused: isPausedChunk,
|
|
3113
|
+
sequence,
|
|
3114
|
+
speciality,
|
|
3115
|
+
retry
|
|
3116
|
+
};
|
|
3117
|
+
let formData;
|
|
3118
|
+
switch (selectedFormatRef.current) {
|
|
3119
|
+
case "hl7":
|
|
3120
|
+
formData = createHL7TranscriptionRequest(audioFile, requestData);
|
|
3121
|
+
break;
|
|
3122
|
+
case "fhir":
|
|
3123
|
+
formData = createFHIRTranscriptionRequest(audioFile, requestData);
|
|
3124
|
+
break;
|
|
3125
|
+
case "json":
|
|
3126
|
+
default:
|
|
3127
|
+
formData = new FormData();
|
|
3128
|
+
if (retry) {
|
|
3129
|
+
formData.append("retry", "true");
|
|
3130
|
+
} else if (sessionIdRef.current) {
|
|
3131
|
+
formData.append("sessionId", sessionIdRef.current);
|
|
3281
3132
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
patientDetails
|
|
3288
|
-
removeSilence: removeSilenceRef.current,
|
|
3289
|
-
skipDiarization: skipDiarizationRef.current,
|
|
3290
|
-
isFinalChunk,
|
|
3291
|
-
isPaused: isPausedChunk,
|
|
3292
|
-
sequence,
|
|
3293
|
-
speciality,
|
|
3294
|
-
retry
|
|
3295
|
-
};
|
|
3296
|
-
let formData;
|
|
3297
|
-
let contentType;
|
|
3298
|
-
switch (selectedFormatRef.current) {
|
|
3299
|
-
case "hl7":
|
|
3300
|
-
formData = createHL7TranscriptionRequest(audioFile, requestData);
|
|
3301
|
-
contentType = "multipart/form-data; hl7-request=true";
|
|
3302
|
-
console.log("Created HL7-formatted request");
|
|
3303
|
-
console.log("HL7 FormData entries:", Array.from(formData.entries()));
|
|
3304
|
-
break;
|
|
3305
|
-
case "fhir":
|
|
3306
|
-
formData = createFHIRTranscriptionRequest(audioFile, requestData);
|
|
3307
|
-
contentType = "multipart/form-data; fhir-request=true";
|
|
3308
|
-
console.log("Created FHIR-formatted request");
|
|
3309
|
-
console.log("FHIR FormData entries:", Array.from(formData.entries()));
|
|
3310
|
-
break;
|
|
3311
|
-
case "json":
|
|
3312
|
-
default:
|
|
3313
|
-
formData = new FormData();
|
|
3314
|
-
if (retry) {
|
|
3315
|
-
formData.append("retry", "true");
|
|
3316
|
-
} else if (sessionIdRef.current) {
|
|
3317
|
-
formData.append("sessionId", sessionIdRef.current);
|
|
3318
|
-
}
|
|
3319
|
-
formData.append("audio", audioFile);
|
|
3320
|
-
formData.append("model", selectedModelRef.current);
|
|
3321
|
-
formData.append("doctorName", doctorName);
|
|
3322
|
-
if (patientHistory) formData.append("patientHistory", patientHistory);
|
|
3323
|
-
if (patientDetailsPayload) {
|
|
3324
|
-
formData.append("patientDetails", JSON.stringify(patientDetailsPayload));
|
|
3325
|
-
}
|
|
3326
|
-
formData.append("removeSilence", removeSilenceRef.current.toString());
|
|
3327
|
-
formData.append("skipDiarization", skipDiarizationRef.current.toString());
|
|
3328
|
-
formData.append("isFinalChunk", isFinalChunk.toString());
|
|
3329
|
-
formData.append("isPaused", isPausedChunk.toString());
|
|
3330
|
-
formData.append("sequence", sequence.toString());
|
|
3331
|
-
formData.append("speciality", speciality);
|
|
3332
|
-
console.log("Created JSON-formatted request");
|
|
3333
|
-
break;
|
|
3133
|
+
formData.append("audio", audioFile);
|
|
3134
|
+
formData.append("model", selectedModelRef.current);
|
|
3135
|
+
formData.append("doctorName", doctorName);
|
|
3136
|
+
if (patientHistory) formData.append("patientHistory", patientHistory);
|
|
3137
|
+
if (patientDetailsPayload) {
|
|
3138
|
+
formData.append("patientDetails", JSON.stringify(patientDetailsPayload));
|
|
3334
3139
|
}
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3140
|
+
formData.append("removeSilence", removeSilenceRef.current.toString());
|
|
3141
|
+
formData.append("skipDiarization", skipDiarizationRef.current.toString());
|
|
3142
|
+
formData.append("isFinalChunk", isFinalChunk.toString());
|
|
3143
|
+
formData.append("isPaused", isPausedChunk.toString());
|
|
3144
|
+
formData.append("sequence", sequence.toString());
|
|
3145
|
+
formData.append("speciality", speciality);
|
|
3146
|
+
break;
|
|
3147
|
+
}
|
|
3148
|
+
const headers = {
|
|
3149
|
+
"x-api-key": effectiveApiKey || "",
|
|
3150
|
+
"x-response-format": selectedFormatRef.current,
|
|
3151
|
+
"x-request-format": selectedFormatRef.current
|
|
3152
|
+
};
|
|
3153
|
+
const data = await pRetry__default.default(
|
|
3154
|
+
async (attemptNumber) => {
|
|
3155
|
+
console.log(`[SERA] Step 9: Sending to server | sequence=${sequence}, attempt=${attemptNumber}`);
|
|
3341
3156
|
const response = await fetch(`${apiBaseUrl}/api/transcribe`, {
|
|
3342
3157
|
method: "POST",
|
|
3343
3158
|
headers,
|
|
@@ -3345,11 +3160,7 @@ var useAudioRecorder = ({
|
|
|
3345
3160
|
});
|
|
3346
3161
|
if (!response.ok) {
|
|
3347
3162
|
const errorText = await response.text();
|
|
3348
|
-
console.error(
|
|
3349
|
-
status: response.status,
|
|
3350
|
-
statusText: response.statusText,
|
|
3351
|
-
body: errorText
|
|
3352
|
-
});
|
|
3163
|
+
console.error(`[SERA] Server error | status=${response.status}, body=${errorText}`);
|
|
3353
3164
|
let errorMessage = `HTTP ${response.status}`;
|
|
3354
3165
|
try {
|
|
3355
3166
|
const errorData = JSON.parse(errorText);
|
|
@@ -3370,30 +3181,20 @@ var useAudioRecorder = ({
|
|
|
3370
3181
|
let responseData;
|
|
3371
3182
|
if (selectedFormatRef.current === "json") {
|
|
3372
3183
|
responseData = await response.json();
|
|
3373
|
-
console.log("Parsed JSON response:", responseData);
|
|
3374
3184
|
} else if (selectedFormatRef.current === "hl7") {
|
|
3375
3185
|
responseData = await response.text();
|
|
3376
|
-
console.log("Received HL7 response:", responseData);
|
|
3377
3186
|
} else if (selectedFormatRef.current === "fhir") {
|
|
3378
3187
|
responseData = await response.json();
|
|
3379
|
-
console.log("Received FHIR response:", responseData);
|
|
3380
3188
|
} else {
|
|
3381
3189
|
const responseText = await response.text();
|
|
3382
3190
|
try {
|
|
3383
3191
|
responseData = JSON.parse(responseText);
|
|
3384
|
-
console.log("Fallback: Parsed as JSON:", responseData);
|
|
3385
3192
|
} catch {
|
|
3386
3193
|
responseData = responseText;
|
|
3387
|
-
console.log("Fallback: Using as text:", responseData);
|
|
3388
3194
|
}
|
|
3389
3195
|
}
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
selectedFormatRef.current
|
|
3393
|
-
);
|
|
3394
|
-
console.log("Original response:", responseData);
|
|
3395
|
-
console.log("Converted response:", convertedData);
|
|
3396
|
-
return convertedData;
|
|
3196
|
+
console.log(`[SERA] Step 10: Server response received | sequence=${sequence}`);
|
|
3197
|
+
return responseData;
|
|
3397
3198
|
},
|
|
3398
3199
|
{
|
|
3399
3200
|
retries: 3,
|
|
@@ -3402,13 +3203,7 @@ var useAudioRecorder = ({
|
|
|
3402
3203
|
maxTimeout: 1e4,
|
|
3403
3204
|
randomize: true,
|
|
3404
3205
|
onFailedAttempt: (error2) => {
|
|
3405
|
-
console.warn(
|
|
3406
|
-
`[WARN] Transcribe attempt ${error2.attemptNumber} failed for sequence ${sequence}:`,
|
|
3407
|
-
{
|
|
3408
|
-
error: error2,
|
|
3409
|
-
retriesLeft: error2.retriesLeft
|
|
3410
|
-
}
|
|
3411
|
-
);
|
|
3206
|
+
console.warn(`[SERA] Attempt ${error2.attemptNumber} failed for sequence ${sequence} | retriesLeft=${error2.retriesLeft}`);
|
|
3412
3207
|
if (error2.retriesLeft > 0) {
|
|
3413
3208
|
setError(
|
|
3414
3209
|
`Network issue detected. Retrying... (${error2.retriesLeft} attempts remaining)`
|
|
@@ -3417,27 +3212,23 @@ var useAudioRecorder = ({
|
|
|
3417
3212
|
}
|
|
3418
3213
|
}
|
|
3419
3214
|
);
|
|
3215
|
+
const convertedData = convertTranscriptionResponse(data, selectedFormatRef.current);
|
|
3420
3216
|
if (conversionError) {
|
|
3421
3217
|
clearError();
|
|
3422
3218
|
}
|
|
3423
3219
|
if (error && error.includes("Retrying")) {
|
|
3424
3220
|
setError(null);
|
|
3425
3221
|
}
|
|
3426
|
-
if (!
|
|
3222
|
+
if (!convertedData) {
|
|
3427
3223
|
throw new Error("No data received from transcription server");
|
|
3428
3224
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
data.sessionId
|
|
3434
|
-
);
|
|
3435
|
-
sessionIdRef.current = data.sessionId;
|
|
3436
|
-
} else if (!retry && data.sessionId && !sessionIdRef.current) {
|
|
3437
|
-
console.log("[SUCCESS] Received initial server session ID:", data.sessionId);
|
|
3438
|
-
sessionIdRef.current = data.sessionId;
|
|
3225
|
+
if (retry && convertedData.sessionId) {
|
|
3226
|
+
sessionIdRef.current = convertedData.sessionId;
|
|
3227
|
+
} else if (!retry && convertedData.sessionId && !sessionIdRef.current) {
|
|
3228
|
+
sessionIdRef.current = convertedData.sessionId;
|
|
3439
3229
|
}
|
|
3440
|
-
|
|
3230
|
+
console.log(`[SERA] Step 11: Transcription received | sequence=${sequence}, sessionId=${sessionIdRef.current}`);
|
|
3231
|
+
receivedTranscriptionsRef.current.set(sequence, convertedData.transcription);
|
|
3441
3232
|
while (receivedTranscriptionsRef.current.has(nextExpectedSequenceRef.current)) {
|
|
3442
3233
|
const t = receivedTranscriptionsRef.current.get(nextExpectedSequenceRef.current);
|
|
3443
3234
|
setAlreadyDoneTranscription(t);
|
|
@@ -3445,17 +3236,21 @@ var useAudioRecorder = ({
|
|
|
3445
3236
|
nextExpectedSequenceRef.current++;
|
|
3446
3237
|
}
|
|
3447
3238
|
if (isFinalChunk) {
|
|
3239
|
+
console.log(`[SERA] Step 12: Final chunk complete - transcription done | sessionId=${sessionIdRef.current}`);
|
|
3448
3240
|
setTranscriptionDone(true);
|
|
3449
|
-
onTranscriptionComplete(
|
|
3241
|
+
onTranscriptionComplete(
|
|
3242
|
+
convertedData.transcription,
|
|
3243
|
+
convertedData.classifiedInfo,
|
|
3244
|
+
sessionIdRef.current
|
|
3245
|
+
);
|
|
3450
3246
|
if (localSessionIdRef.current) {
|
|
3451
3247
|
await markSessionComplete(localSessionIdRef.current);
|
|
3452
3248
|
setShowRetrySessionPrompt(false);
|
|
3453
3249
|
}
|
|
3454
3250
|
}
|
|
3455
3251
|
} catch (err) {
|
|
3456
|
-
console.error(`[
|
|
3252
|
+
console.error(`[SERA] Upload failed for sequence ${sequence}:`, err);
|
|
3457
3253
|
if (conversionError) {
|
|
3458
|
-
console.error("Conversion error during upload:", conversionError);
|
|
3459
3254
|
setError(`Data conversion failed: ${conversionError}`);
|
|
3460
3255
|
}
|
|
3461
3256
|
const isAbortError = err instanceof Error && err.name === "AbortError";
|
|
@@ -3466,7 +3261,7 @@ var useAudioRecorder = ({
|
|
|
3466
3261
|
localSessionIdRef.current,
|
|
3467
3262
|
err instanceof Error ? err.message : "Unknown error"
|
|
3468
3263
|
);
|
|
3469
|
-
console.
|
|
3264
|
+
console.warn(`[SERA] Chunk ${sequence} failed - continuing to record audio locally`);
|
|
3470
3265
|
if (isFinalChunk) {
|
|
3471
3266
|
setShowRetrySessionPrompt(true);
|
|
3472
3267
|
}
|
|
@@ -3537,18 +3332,17 @@ var useAudioRecorder = ({
|
|
|
3537
3332
|
if (failedSession) {
|
|
3538
3333
|
const success = await retrySession(failedSession.id);
|
|
3539
3334
|
if (success) {
|
|
3540
|
-
console.log(`Successfully retried session ${failedSession.id}`);
|
|
3541
3335
|
await deleteSession(failedSession.id);
|
|
3542
|
-
console.log(`
|
|
3336
|
+
console.log(`[SERA] Session retry succeeded | sessionId=${failedSession.id}`);
|
|
3543
3337
|
setError(null);
|
|
3544
3338
|
retrySucceeded = true;
|
|
3545
3339
|
} else {
|
|
3546
|
-
console.
|
|
3340
|
+
console.warn(`[SERA] Session retry failed | sessionId=${failedSession.id}`);
|
|
3547
3341
|
setError("Retry failed. Please check your connection and try again.");
|
|
3548
3342
|
}
|
|
3549
3343
|
}
|
|
3550
3344
|
} catch (error2) {
|
|
3551
|
-
console.error("Error retrying failed
|
|
3345
|
+
console.error("[SERA] Error retrying failed session:", error2);
|
|
3552
3346
|
setError("Failed to retry sessions. Please try again.");
|
|
3553
3347
|
} finally {
|
|
3554
3348
|
setIsRetryingSession(false);
|
|
@@ -3581,7 +3375,7 @@ var useAudioRecorder = ({
|
|
|
3581
3375
|
const localSessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3582
3376
|
localSessionIdRef.current = localSessionId;
|
|
3583
3377
|
sessionIdRef.current = null;
|
|
3584
|
-
console.log(
|
|
3378
|
+
console.log(`[SERA] Step 1: New recording session created | localSessionId=${localSessionId}`);
|
|
3585
3379
|
}
|
|
3586
3380
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
3587
3381
|
audio: {
|
|
@@ -3597,9 +3391,8 @@ var useAudioRecorder = ({
|
|
|
3597
3391
|
const track = audioTracks[0];
|
|
3598
3392
|
const settings = track.getSettings();
|
|
3599
3393
|
setCurrentDeviceId(settings.deviceId || null);
|
|
3600
|
-
console.log("Recording started with local session:", localSessionIdRef.current);
|
|
3601
3394
|
track.addEventListener("ended", () => {
|
|
3602
|
-
console.
|
|
3395
|
+
console.warn("[SERA] Microphone disconnected during recording");
|
|
3603
3396
|
setError("Microphone disconnected. Session saved - please reconnect and retry.");
|
|
3604
3397
|
stopRecording();
|
|
3605
3398
|
});
|
|
@@ -3626,38 +3419,26 @@ var useAudioRecorder = ({
|
|
|
3626
3419
|
level,
|
|
3627
3420
|
silentDuration,
|
|
3628
3421
|
hasDetectedAudio,
|
|
3629
|
-
isInitialPhase
|
|
3630
|
-
totalRecordingTime,
|
|
3631
|
-
lastAudioTime
|
|
3422
|
+
isInitialPhase
|
|
3632
3423
|
} = event.data;
|
|
3633
3424
|
if (command === "finalChunk" && audioBuffer) {
|
|
3634
3425
|
const sequence = sequenceCounterRef.current++;
|
|
3635
3426
|
const audioArray = new Float32Array(audioBuffer);
|
|
3636
|
-
console.log(`[
|
|
3427
|
+
console.log(`[SERA] Step 4: Final chunk received | sequence=${sequence}, samples=${audioArray.length}`);
|
|
3637
3428
|
enqueueChunk(audioArray, true, sequence);
|
|
3638
3429
|
} else if (command === "chunk" && audioBuffer) {
|
|
3639
3430
|
const sequence = sequenceCounterRef.current++;
|
|
3640
3431
|
const audioArray = new Float32Array(audioBuffer);
|
|
3641
|
-
console.log(`[
|
|
3432
|
+
console.log(`[SERA] Step 4: Chunk received | sequence=${sequence}, samples=${audioArray.length}`);
|
|
3642
3433
|
enqueueChunk(audioArray, false, sequence);
|
|
3643
3434
|
} else if (command === "pauseChunk" && audioBuffer) {
|
|
3644
3435
|
const sequence = sequenceCounterRef.current++;
|
|
3645
|
-
console.log(`[RECEIVE] Pause chunk with audioBuffer, sequence: ${sequence}`);
|
|
3646
3436
|
enqueueChunk(new Float32Array(audioBuffer), false, sequence, true);
|
|
3647
3437
|
} else if (command === "audioLevel") {
|
|
3648
3438
|
setAudioLevel(level);
|
|
3649
3439
|
} else if (command === "prolongedSilence") {
|
|
3650
|
-
console.
|
|
3651
|
-
`Prolonged silence detected: ${Math.round(silentDuration)}s silent, ${Math.round(
|
|
3652
|
-
totalRecordingTime
|
|
3653
|
-
)}s total, last audio ${Math.round(lastAudioTime)}s ago`
|
|
3654
|
-
);
|
|
3440
|
+
console.warn(`[SERA] Prolonged silence: ${Math.round(silentDuration)}s`);
|
|
3655
3441
|
} else if (command === "noAudioDetected") {
|
|
3656
|
-
console.log(
|
|
3657
|
-
`No audio detected: ${Math.round(
|
|
3658
|
-
silentDuration
|
|
3659
|
-
)}s silent, hasDetectedAudio: ${hasDetectedAudio}, isInitialPhase: ${isInitialPhase}`
|
|
3660
|
-
);
|
|
3661
3442
|
setNoAudioDetected(true);
|
|
3662
3443
|
let errorMessage;
|
|
3663
3444
|
if (isInitialPhase && !hasDetectedAudio) {
|
|
@@ -3677,7 +3458,7 @@ var useAudioRecorder = ({
|
|
|
3677
3458
|
source.connect(processor);
|
|
3678
3459
|
audioContextRef.current = audioContext;
|
|
3679
3460
|
recordingSampleRateRef.current = audioContext.sampleRate;
|
|
3680
|
-
console.log(`[
|
|
3461
|
+
console.log(`[SERA] Step 2: Recording started | sampleRate=${audioContext.sampleRate}Hz`);
|
|
3681
3462
|
processorRef.current = processor;
|
|
3682
3463
|
setIsRecording(true);
|
|
3683
3464
|
const intervalId = window.setInterval(() => {
|
|
@@ -3685,7 +3466,7 @@ var useAudioRecorder = ({
|
|
|
3685
3466
|
}, 47e3);
|
|
3686
3467
|
setUploadChunkInterval(intervalId);
|
|
3687
3468
|
} catch (err) {
|
|
3688
|
-
console.error("Recording start failed:", err);
|
|
3469
|
+
console.error("[SERA] Recording start failed:", err);
|
|
3689
3470
|
}
|
|
3690
3471
|
}, [
|
|
3691
3472
|
validateMicrophoneAccess,
|
|
@@ -3697,13 +3478,12 @@ var useAudioRecorder = ({
|
|
|
3697
3478
|
currentDeviceId
|
|
3698
3479
|
]);
|
|
3699
3480
|
const stopRecording = React3__namespace.default.useCallback(async () => {
|
|
3700
|
-
console.log("
|
|
3481
|
+
console.log("[SERA] Step 3: Stop recording requested");
|
|
3701
3482
|
if (uploadChunkInterval) {
|
|
3702
3483
|
clearInterval(uploadChunkInterval);
|
|
3703
3484
|
setUploadChunkInterval(null);
|
|
3704
3485
|
}
|
|
3705
3486
|
if (processorRef.current) {
|
|
3706
|
-
console.log("Stopping recording and sending final chunk --> ", isPaused ? "paused" : "final");
|
|
3707
3487
|
processorRef.current.port.postMessage({ command: "stop" });
|
|
3708
3488
|
setTimeout(async () => {
|
|
3709
3489
|
if (processorRef.current) {
|
|
@@ -3721,18 +3501,13 @@ var useAudioRecorder = ({
|
|
|
3721
3501
|
}, 500);
|
|
3722
3502
|
}
|
|
3723
3503
|
setIsRecording(false);
|
|
3724
|
-
console.log("\u{1F50D} Stop recording - checking for failed session:", {
|
|
3725
|
-
hasSessionFailed: sessionHasFailedChunkRef.current,
|
|
3726
|
-
localSessionId: localSessionIdRef.current
|
|
3727
|
-
});
|
|
3728
3504
|
if (sessionHasFailedChunkRef.current && localSessionIdRef.current) {
|
|
3729
|
-
console.
|
|
3505
|
+
console.warn("[SERA] Recording stopped with failed session - showing retry UI");
|
|
3730
3506
|
setShowRetrySessionPrompt(true);
|
|
3731
3507
|
}
|
|
3732
3508
|
}, [uploadChunkInterval]);
|
|
3733
3509
|
React3__namespace.default.useEffect(() => {
|
|
3734
3510
|
const handleDeviceChange = async () => {
|
|
3735
|
-
console.log("Audio device change detected");
|
|
3736
3511
|
try {
|
|
3737
3512
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
3738
3513
|
const audioInputDevices = devices.filter((device) => device.kind === "audioinput");
|
|
@@ -3751,7 +3526,7 @@ var useAudioRecorder = ({
|
|
|
3751
3526
|
}
|
|
3752
3527
|
}
|
|
3753
3528
|
} catch (error2) {
|
|
3754
|
-
console.error("Device change detection failed:", error2);
|
|
3529
|
+
console.error("[SERA] Device change detection failed:", error2);
|
|
3755
3530
|
setError("Unable to detect audio devices. Please check your microphone permissions.");
|
|
3756
3531
|
}
|
|
3757
3532
|
};
|
|
@@ -3771,7 +3546,7 @@ var useAudioRecorder = ({
|
|
|
3771
3546
|
}, 1e3);
|
|
3772
3547
|
}
|
|
3773
3548
|
} catch (error2) {
|
|
3774
|
-
console.error("Device selection failed:", error2);
|
|
3549
|
+
console.error("[SERA] Device selection failed:", error2);
|
|
3775
3550
|
setError("Failed to switch to selected microphone.");
|
|
3776
3551
|
}
|
|
3777
3552
|
},
|
|
@@ -3807,7 +3582,7 @@ var useAudioRecorder = ({
|
|
|
3807
3582
|
return;
|
|
3808
3583
|
}
|
|
3809
3584
|
const { chunk, isFinal, sequence, isPaused: isPaused2 = false } = chunkQueueRef.current.shift();
|
|
3810
|
-
console.log(`[
|
|
3585
|
+
console.log(`[SERA] Step 5: Queue processing | sequence=${sequence}, isFinal=${isFinal}, queued=${chunkQueueRef.current.length}`);
|
|
3811
3586
|
isProcessingQueueRef.current = true;
|
|
3812
3587
|
uploadChunkToServer(chunk, isFinal, sequence, false, isPaused2).finally(() => {
|
|
3813
3588
|
isProcessingQueueRef.current = false;
|
|
@@ -3816,12 +3591,9 @@ var useAudioRecorder = ({
|
|
|
3816
3591
|
}, [uploadChunkToServer, isLoaded]);
|
|
3817
3592
|
const enqueueChunk = React3__namespace.default.useCallback(
|
|
3818
3593
|
(audioData, isFinalChunk, sequence, isPausedChunk = false) => {
|
|
3819
|
-
console.log(`[QUEUE] Enqueuing ${isFinalChunk ? "FINAL" : isPausedChunk ? "PAUSED" : "regular"} chunk ${sequence}, samples: ${audioData?.length || 0}, queue size: ${chunkQueueRef.current.length}`);
|
|
3820
3594
|
if (isFinalChunk) {
|
|
3821
3595
|
if (!sessionHasFailedChunkRef.current) {
|
|
3822
3596
|
setIsProcessing(true);
|
|
3823
|
-
} else {
|
|
3824
|
-
console.log("\u26A0\uFE0F Session has failed - skipping processing state (IndexedDB save only)");
|
|
3825
3597
|
}
|
|
3826
3598
|
}
|
|
3827
3599
|
chunkQueueRef.current.push({
|
|
@@ -4531,21 +4303,13 @@ var AudioRecorder = ({
|
|
|
4531
4303
|
patientDetails,
|
|
4532
4304
|
selectedFormat,
|
|
4533
4305
|
onTranscriptionUpdate: (text, sessionId) => {
|
|
4534
|
-
console.log(
|
|
4306
|
+
console.log(`[SERA] Transcription update received | sessionId=${sessionId}, textLength=${text.length}`);
|
|
4535
4307
|
if (text.length > 0) {
|
|
4536
|
-
console.log("Transcription update:", text, sessionId);
|
|
4537
4308
|
onTranscriptionUpdate && onTranscriptionUpdate(text, sessionId);
|
|
4538
4309
|
}
|
|
4539
4310
|
},
|
|
4540
4311
|
onTranscriptionComplete: (text, classification, sessionId) => {
|
|
4541
|
-
console.log(
|
|
4542
|
-
"onTranscriptionComplete called with text:",
|
|
4543
|
-
text,
|
|
4544
|
-
"classification:",
|
|
4545
|
-
classification,
|
|
4546
|
-
"sessionId:",
|
|
4547
|
-
sessionId
|
|
4548
|
-
);
|
|
4312
|
+
console.log(`[SERA] Transcription complete | sessionId=${sessionId}, textLength=${text.length}, hasClassification=${!!classification}`);
|
|
4549
4313
|
onTranscriptionComplete && onTranscriptionComplete(text, classification, sessionId);
|
|
4550
4314
|
}
|
|
4551
4315
|
});
|
|
@@ -4604,7 +4368,7 @@ var AudioRecorder = ({
|
|
|
4604
4368
|
] }) });
|
|
4605
4369
|
}
|
|
4606
4370
|
if (isMicrophoneError) {
|
|
4607
|
-
console.log("
|
|
4371
|
+
console.log("[SERA] Showing microphone error UI");
|
|
4608
4372
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start", children: [
|
|
4609
4373
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-5 w-5 text-red-400" }) }),
|
|
4610
4374
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-3 flex-1", children: [
|
|
@@ -4614,7 +4378,7 @@ var AudioRecorder = ({
|
|
|
4614
4378
|
"button",
|
|
4615
4379
|
{
|
|
4616
4380
|
onClick: () => {
|
|
4617
|
-
console.log("
|
|
4381
|
+
console.log("[SERA] Microphone check again requested");
|
|
4618
4382
|
validateMicrophoneAccess();
|
|
4619
4383
|
},
|
|
4620
4384
|
className: "bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition-colors",
|