@eka-care/medassist-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/Synapse.d.ts +131 -0
- package/dist/Synapse.d.ts.map +1 -0
- package/dist/Synapse.js +479 -0
- package/dist/connection/ConnectionFactory.d.ts +17 -0
- package/dist/connection/ConnectionFactory.d.ts.map +1 -0
- package/dist/connection/ConnectionFactory.js +28 -0
- package/dist/connection/Websocket.d.ts +61 -0
- package/dist/connection/Websocket.d.ts.map +1 -0
- package/dist/connection/Websocket.js +184 -0
- package/dist/constants/index.d.ts +17 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +28 -0
- package/dist/constants/types.d.ts +2 -0
- package/dist/constants/types.d.ts.map +1 -0
- package/dist/constants/types.js +2 -0
- package/dist/events/Events.d.ts +47 -0
- package/dist/events/Events.d.ts.map +1 -0
- package/dist/events/Events.js +46 -0
- package/dist/events/Incoming.d.ts +2 -0
- package/dist/events/Incoming.d.ts.map +1 -0
- package/dist/events/Incoming.js +2 -0
- package/dist/events/Outgoing.d.ts +2 -0
- package/dist/events/Outgoing.d.ts.map +1 -0
- package/dist/events/Outgoing.js +2 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +38 -0
- package/dist/events/types.d.ts +86 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/events/types.js +6 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/internal/Api/BaseResource.d.ts +35 -0
- package/dist/internal/Api/BaseResource.d.ts.map +1 -0
- package/dist/internal/Api/BaseResource.js +54 -0
- package/dist/internal/Api/HttpClient.d.ts +25 -0
- package/dist/internal/Api/HttpClient.d.ts.map +1 -0
- package/dist/internal/Api/HttpClient.js +131 -0
- package/dist/internal/Api/types.d.ts +7 -0
- package/dist/internal/Api/types.d.ts.map +1 -0
- package/dist/internal/Api/types.js +2 -0
- package/dist/internal/Error/Error.d.ts +97 -0
- package/dist/internal/Error/Error.d.ts.map +1 -0
- package/dist/internal/Error/Error.js +243 -0
- package/dist/internal/Error/types.d.ts +17 -0
- package/dist/internal/Error/types.d.ts.map +1 -0
- package/dist/internal/Error/types.js +12 -0
- package/dist/internal/connection/BaseConnection.d.ts +100 -0
- package/dist/internal/connection/BaseConnection.d.ts.map +1 -0
- package/dist/internal/connection/BaseConnection.js +138 -0
- package/dist/internal/connection/types.d.ts +43 -0
- package/dist/internal/connection/types.d.ts.map +1 -0
- package/dist/internal/connection/types.js +20 -0
- package/dist/internal/events/EventEmitter.d.ts +7 -0
- package/dist/internal/events/EventEmitter.d.ts.map +1 -0
- package/dist/internal/events/EventEmitter.js +27 -0
- package/dist/internal/store/index.d.ts +10 -0
- package/dist/internal/store/index.d.ts.map +1 -0
- package/dist/internal/store/index.js +9 -0
- package/dist/media/audio/Audio.d.ts +37 -0
- package/dist/media/audio/Audio.d.ts.map +1 -0
- package/dist/media/audio/Audio.js +310 -0
- package/dist/media/audio/types.d.ts +25 -0
- package/dist/media/audio/types.d.ts.map +1 -0
- package/dist/media/audio/types.js +10 -0
- package/dist/media/file/File.d.ts +54 -0
- package/dist/media/file/File.d.ts.map +1 -0
- package/dist/media/file/File.js +159 -0
- package/dist/messages/MessageManager.d.ts +79 -0
- package/dist/messages/MessageManager.d.ts.map +1 -0
- package/dist/messages/MessageManager.js +420 -0
- package/dist/messages/types.d.ts +74 -0
- package/dist/messages/types.d.ts.map +1 -0
- package/dist/messages/types.js +34 -0
- package/dist/resources/config/Config.d.ts +5 -0
- package/dist/resources/config/Config.d.ts.map +1 -0
- package/dist/resources/config/Config.js +5 -0
- package/dist/resources/index.d.ts +34 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +108 -0
- package/dist/resources/session/Session.d.ts +21 -0
- package/dist/resources/session/Session.d.ts.map +1 -0
- package/dist/resources/session/Session.js +39 -0
- package/dist/resources/session/types.d.ts +17 -0
- package/dist/resources/session/types.d.ts.map +1 -0
- package/dist/resources/session/types.js +8 -0
- package/dist/resources/types.d.ts +22 -0
- package/dist/resources/types.d.ts.map +1 -0
- package/dist/resources/types.js +5 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +24 -0
- package/dist/utils/Error.d.ts +45 -0
- package/dist/utils/Error.d.ts.map +1 -0
- package/dist/utils/Error.js +114 -0
- package/package.json +41 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type AudioConfig, type AudioDataCallback, type AudioErrorCallback, type AudioRecordingStatusCallback } from "./types";
|
|
2
|
+
export declare class AudioManager {
|
|
3
|
+
private mediaRecorder;
|
|
4
|
+
private mediaStream;
|
|
5
|
+
private recordingStartTime;
|
|
6
|
+
private autoPauseTimer;
|
|
7
|
+
private config;
|
|
8
|
+
private onAudioData;
|
|
9
|
+
private onAudioError;
|
|
10
|
+
private onRecordingStatusChange;
|
|
11
|
+
private suppressCallbacks;
|
|
12
|
+
constructor(config?: Partial<AudioConfig>);
|
|
13
|
+
private checkCurrentPermissionState;
|
|
14
|
+
start(onAudioData: AudioDataCallback, onRecordingStatusChange?: AudioRecordingStatusCallback, onAudioError?: AudioErrorCallback): Promise<void>;
|
|
15
|
+
private processAudioChunk;
|
|
16
|
+
private toRecordingError;
|
|
17
|
+
private handleError;
|
|
18
|
+
/**
|
|
19
|
+
* Set up auto-pause timer for 15-minute limit
|
|
20
|
+
*/
|
|
21
|
+
private setupAutoPauseTimer;
|
|
22
|
+
private setupMediaRecorderEvents;
|
|
23
|
+
/**
|
|
24
|
+
* Convert blob to base64 string
|
|
25
|
+
*/
|
|
26
|
+
private blobToBase64;
|
|
27
|
+
/**
|
|
28
|
+
* Stop recording manually
|
|
29
|
+
*/
|
|
30
|
+
stop(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Cancel recording (discard data and suppress callbacks)
|
|
33
|
+
*/
|
|
34
|
+
cancel(): void;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=Audio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Audio.d.ts","sourceRoot":"","sources":["../../../src/media/audio/Audio.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAEvB,KAAK,4BAA4B,EAElC,MAAM,SAAS,CAAC;AAGjB,qBAAa,YAAY;IACvB,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,cAAc,CAA8C;IAEpE,OAAO,CAAC,MAAM,CAAc;IAE5B,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,uBAAuB,CAA6C;IAC5E,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;YAU/B,2BAA2B;IAa5B,KAAK,CAChB,WAAW,EAAE,iBAAiB,EAC9B,uBAAuB,CAAC,EAAE,4BAA4B,EACtD,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,IAAI,CAAC;YAkGF,iBAAiB;IAyB/B,OAAO,CAAC,gBAAgB;IA0BxB,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,wBAAwB;IAsChC;;OAEG;IACH,OAAO,CAAC,YAAY;IA8BpB;;OAEG;IACH,IAAI,IAAI,IAAI;IA8BZ;;OAEG;IACH,MAAM,IAAI,IAAI;IAiCd,OAAO,IAAI,IAAI;CAiChB"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AudioManager = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const Error_1 = require("../../internal/Error/Error");
|
|
6
|
+
class AudioManager {
|
|
7
|
+
mediaRecorder = null;
|
|
8
|
+
mediaStream = null;
|
|
9
|
+
recordingStartTime = 0;
|
|
10
|
+
autoPauseTimer = null;
|
|
11
|
+
config;
|
|
12
|
+
onAudioData = null;
|
|
13
|
+
onAudioError = null;
|
|
14
|
+
onRecordingStatusChange = null;
|
|
15
|
+
suppressCallbacks = false; //gurad against late recorder events like ondatavaailable after cancel () or cleanup() is called
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.config = {
|
|
18
|
+
mimeType: "audio/webm;codecs=opus",
|
|
19
|
+
audioBitsPerSecond: 128000,
|
|
20
|
+
maxRecordingDuration: 900000, // 15 minutes
|
|
21
|
+
autoPauseEnabled: true,
|
|
22
|
+
...config,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async checkCurrentPermissionState() {
|
|
26
|
+
const permission = await navigator.permissions.query({
|
|
27
|
+
name: "microphone",
|
|
28
|
+
});
|
|
29
|
+
if (permission.state === "denied") {
|
|
30
|
+
throw new Error_1.RecordingError("Microphone permission denied", {
|
|
31
|
+
context: { permissionState: permission.state },
|
|
32
|
+
hint: "Ensure the browser has microphone access enabled for this site.",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return permission.state;
|
|
36
|
+
}
|
|
37
|
+
async start(onAudioData, onRecordingStatusChange, onAudioError //Clearly handles the error inside the callback without unhandled rejections
|
|
38
|
+
) {
|
|
39
|
+
try {
|
|
40
|
+
// Check if mediaDevices is available
|
|
41
|
+
if (!navigator.mediaDevices) {
|
|
42
|
+
throw new Error_1.RecordingError("Media devices API is not available. This usually means the page is not served over HTTPS or there are browser restrictions.", {
|
|
43
|
+
context: { feature: "mediaDevices" },
|
|
44
|
+
hint: "Serve the app over HTTPS and verify browser/device microphone support.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Check if MediaRecorder is supported
|
|
48
|
+
if (!window.MediaRecorder) {
|
|
49
|
+
throw new Error_1.RecordingError("MediaRecorder is not supported in this browser", {
|
|
50
|
+
context: { feature: "MediaRecorder" },
|
|
51
|
+
hint: "Use a browser that supports the MediaRecorder API.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Check for supported audio formats in order of preference
|
|
55
|
+
const supportedTypes = [
|
|
56
|
+
"audio/mp3",
|
|
57
|
+
"audio/ogg;codecs=opus",
|
|
58
|
+
"audio/mp4",
|
|
59
|
+
"audio/ogg",
|
|
60
|
+
"audio/wav",
|
|
61
|
+
].filter((type) => MediaRecorder.isTypeSupported(type));
|
|
62
|
+
if (supportedTypes.length === 0) {
|
|
63
|
+
throw new Error_1.RecordingError("No supported audio formats found in this browser", {
|
|
64
|
+
context: { requestedTypes: supportedTypes },
|
|
65
|
+
hint: "Verify codec support or adjust the requested MIME types.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Update config with best supported mime type
|
|
69
|
+
this.config.mimeType = supportedTypes[0];
|
|
70
|
+
await this.checkCurrentPermissionState();
|
|
71
|
+
this.onAudioData = onAudioData;
|
|
72
|
+
this.onAudioError = onAudioError ?? null;
|
|
73
|
+
this.onRecordingStatusChange = onRecordingStatusChange ?? null;
|
|
74
|
+
this.suppressCallbacks = false; //reset the guard against late recorder events
|
|
75
|
+
// Get user media
|
|
76
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
77
|
+
audio: {
|
|
78
|
+
echoCancellation: false,
|
|
79
|
+
noiseSuppression: false,
|
|
80
|
+
autoGainControl: false,
|
|
81
|
+
sampleRate: 44100,
|
|
82
|
+
channelCount: 1,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Create MediaRecorder with fallback if the selected format fails
|
|
86
|
+
this.mediaRecorder = new MediaRecorder(this.mediaStream, {
|
|
87
|
+
mimeType: this.config.mimeType,
|
|
88
|
+
audioBitsPerSecond: this.config.audioBitsPerSecond,
|
|
89
|
+
});
|
|
90
|
+
// Verify MediaRecorder is in a valid state
|
|
91
|
+
if (this.mediaRecorder.state !== "inactive") {
|
|
92
|
+
throw new Error_1.RecordingError("MediaRecorder is in an invalid state for start()", {
|
|
93
|
+
context: { recorderState: this.mediaRecorder.state },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Set up event handlers
|
|
97
|
+
this.setupMediaRecorderEvents();
|
|
98
|
+
// Start recording
|
|
99
|
+
this.mediaRecorder.start();
|
|
100
|
+
this.recordingStartTime = Date.now();
|
|
101
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.LISTENING);
|
|
102
|
+
// Set up auto-pause timer if enabled
|
|
103
|
+
if (this.config.autoPauseEnabled) {
|
|
104
|
+
this.setupAutoPauseTimer();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw this.toRecordingError(error, "Failed to start audio recording", {
|
|
109
|
+
stage: "start",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async processAudioChunk(blob) {
|
|
114
|
+
try {
|
|
115
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.PROCESSING);
|
|
116
|
+
const base64Audio = await this.blobToBase64(blob);
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (!this.onAudioData) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const audioData = {
|
|
122
|
+
audio: base64Audio,
|
|
123
|
+
format: this.config.mimeType,
|
|
124
|
+
duration: this.recordingStartTime > 0 ? now - this.recordingStartTime : 0,
|
|
125
|
+
timestamp: this.recordingStartTime > 0 ? this.recordingStartTime : now,
|
|
126
|
+
};
|
|
127
|
+
this.onAudioData(audioData);
|
|
128
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.TRANSCRIBING);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.handleError(error, { stage: "process_audio_chunk" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
toRecordingError(error, fallbackMessage, context) {
|
|
135
|
+
if (error instanceof Error_1.RecordingError) {
|
|
136
|
+
return error;
|
|
137
|
+
}
|
|
138
|
+
if (error instanceof Error_1.SynapseError) {
|
|
139
|
+
return new Error_1.RecordingError(error.message, {
|
|
140
|
+
cause: error,
|
|
141
|
+
context: { ...error.context, ...context },
|
|
142
|
+
hint: error.hint,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const message = error instanceof Error && error.message ? error.message : fallbackMessage;
|
|
146
|
+
return new Error_1.RecordingError(message, {
|
|
147
|
+
cause: error,
|
|
148
|
+
context,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
handleError(error, context) {
|
|
152
|
+
const recordingError = this.toRecordingError(error, "Unexpected recording error", context);
|
|
153
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.ERROR);
|
|
154
|
+
if (this.onAudioError) {
|
|
155
|
+
try {
|
|
156
|
+
this.onAudioError(recordingError);
|
|
157
|
+
}
|
|
158
|
+
catch (callbackError) {
|
|
159
|
+
console.error("Audio error callback failed:", callbackError);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.error("AudioManager error:", recordingError);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Set up auto-pause timer for 15-minute limit
|
|
168
|
+
*/
|
|
169
|
+
setupAutoPauseTimer() {
|
|
170
|
+
if (this.autoPauseTimer) {
|
|
171
|
+
clearTimeout(this.autoPauseTimer);
|
|
172
|
+
}
|
|
173
|
+
this.autoPauseTimer = setTimeout(() => {
|
|
174
|
+
console.log("Auto-pause timer triggered after 15 minutes");
|
|
175
|
+
this.stop();
|
|
176
|
+
}, this.config.maxRecordingDuration);
|
|
177
|
+
}
|
|
178
|
+
setupMediaRecorderEvents() {
|
|
179
|
+
if (!this.mediaRecorder)
|
|
180
|
+
return;
|
|
181
|
+
this.mediaRecorder.ondataavailable = (event) => {
|
|
182
|
+
if (this.suppressCallbacks || event.data.size <= 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.processAudioChunk(event.data).catch((error) => this.handleError(error, {
|
|
186
|
+
stage: "media_recorder_ondataavailable",
|
|
187
|
+
hasData: event.data.size > 0,
|
|
188
|
+
}));
|
|
189
|
+
};
|
|
190
|
+
this.mediaRecorder.onstop = async () => {
|
|
191
|
+
this.recordingStartTime = 0;
|
|
192
|
+
};
|
|
193
|
+
this.mediaRecorder.onerror = (event) => {
|
|
194
|
+
this.handleError(event.error instanceof Error
|
|
195
|
+
? event.error
|
|
196
|
+
: new Error("MediaRecorder encountered an unknown error"), { stage: "media_recorder_onerror" });
|
|
197
|
+
};
|
|
198
|
+
this.mediaRecorder.onstart = () => {
|
|
199
|
+
console.log("MediaRecorder started recording");
|
|
200
|
+
if (this.mediaRecorder) {
|
|
201
|
+
console.log("MediaRecorder state:", this.mediaRecorder.state);
|
|
202
|
+
console.log("MediaRecorder MIME type:", this.mediaRecorder.mimeType);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Convert blob to base64 string
|
|
208
|
+
*/
|
|
209
|
+
blobToBase64(blob) {
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
const reader = new FileReader();
|
|
212
|
+
reader.onload = () => {
|
|
213
|
+
if (typeof reader.result === "string") {
|
|
214
|
+
// Remove data URL prefix (e.g., "data:audio/mp3;base64,")
|
|
215
|
+
const base64 = reader.result.split(",")[1];
|
|
216
|
+
resolve(base64);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
reject(new Error_1.RecordingError("Failed to convert blob to base64", {
|
|
220
|
+
context: { stage: "blob_to_base64" },
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
reader.onerror = () => reject(new Error_1.RecordingError("FileReader error while converting audio chunk to base64", {
|
|
225
|
+
cause: reader.error ?? undefined,
|
|
226
|
+
context: { stage: "blob_to_base64" },
|
|
227
|
+
}));
|
|
228
|
+
reader.readAsDataURL(blob);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stop recording manually
|
|
233
|
+
*/
|
|
234
|
+
stop() {
|
|
235
|
+
if (!this.mediaRecorder)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
// Clear auto-pause timer
|
|
239
|
+
if (this.autoPauseTimer) {
|
|
240
|
+
clearTimeout(this.autoPauseTimer);
|
|
241
|
+
this.autoPauseTimer = null;
|
|
242
|
+
}
|
|
243
|
+
// Stop MediaRecorder
|
|
244
|
+
if (this.mediaRecorder.state === "recording") {
|
|
245
|
+
this.mediaRecorder.stop();
|
|
246
|
+
}
|
|
247
|
+
// Stop media stream
|
|
248
|
+
if (this.mediaStream) {
|
|
249
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
250
|
+
this.mediaStream = null;
|
|
251
|
+
}
|
|
252
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.IDLE);
|
|
253
|
+
console.log("Audio recording stopped manually");
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw this.toRecordingError(error, "Failed to stop audio recording", {
|
|
257
|
+
stage: "stop",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Cancel recording (discard data and suppress callbacks)
|
|
263
|
+
*/
|
|
264
|
+
cancel() {
|
|
265
|
+
if (!this.mediaRecorder)
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
this.suppressCallbacks = true;
|
|
269
|
+
if (this.autoPauseTimer) {
|
|
270
|
+
clearTimeout(this.autoPauseTimer);
|
|
271
|
+
this.autoPauseTimer = null;
|
|
272
|
+
}
|
|
273
|
+
// Prevent ondataavailable from pushing chunks
|
|
274
|
+
this.mediaRecorder.ondataavailable = null;
|
|
275
|
+
// Stop MediaRecorder
|
|
276
|
+
if (this.mediaRecorder.state === "recording") {
|
|
277
|
+
this.mediaRecorder.stop();
|
|
278
|
+
}
|
|
279
|
+
// Stop media stream
|
|
280
|
+
if (this.mediaStream) {
|
|
281
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
282
|
+
this.mediaStream = null;
|
|
283
|
+
}
|
|
284
|
+
this.onRecordingStatusChange?.(types_1.AudioRecordingStatus.IDLE);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
throw this.toRecordingError(error, "Failed to cancel audio recording", {
|
|
288
|
+
stage: "cancel",
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
destroy() {
|
|
293
|
+
try {
|
|
294
|
+
this.cancel();
|
|
295
|
+
if (this.mediaRecorder) {
|
|
296
|
+
this.mediaRecorder = null;
|
|
297
|
+
}
|
|
298
|
+
if (this.mediaStream) {
|
|
299
|
+
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
300
|
+
this.mediaStream = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
throw this.toRecordingError(error, "Failed to destroy audio resources", {
|
|
305
|
+
stage: "destroy",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
exports.AudioManager = AudioManager;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RecordingError } from "../../internal/Error/Error";
|
|
2
|
+
export interface AudioMetaData {
|
|
3
|
+
audio: string;
|
|
4
|
+
format: string;
|
|
5
|
+
duration: number;
|
|
6
|
+
timestamp: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AudioConfig {
|
|
9
|
+
mimeType: string;
|
|
10
|
+
audioBitsPerSecond: number;
|
|
11
|
+
maxRecordingDuration: number;
|
|
12
|
+
autoPauseEnabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
export type AudioDataCallback = (data: AudioMetaData) => void;
|
|
15
|
+
export type AudioErrorCallback = (error: RecordingError) => void;
|
|
16
|
+
export declare const AudioRecordingStatus: {
|
|
17
|
+
readonly IDLE: "idle";
|
|
18
|
+
readonly LISTENING: "listening";
|
|
19
|
+
readonly ERROR: "error";
|
|
20
|
+
readonly PROCESSING: "processing";
|
|
21
|
+
readonly TRANSCRIBING: "transcribing";
|
|
22
|
+
};
|
|
23
|
+
export type AudioRecordingStatus = (typeof AudioRecordingStatus)[keyof typeof AudioRecordingStatus];
|
|
24
|
+
export type AudioRecordingStatusCallback = (status: AudioRecordingStatus) => void;
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/media/audio/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;AAC9D,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;AACjE,eAAO,MAAM,oBAAoB;;;;;;CAMvB,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAC9B,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,OAAO,oBAAoB,CAAC,CAAC;AACnE,MAAM,MAAM,4BAA4B,GAAG,CACzC,MAAM,EAAE,oBAAoB,KACzB,IAAI,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AudioRecordingStatus = void 0;
|
|
4
|
+
exports.AudioRecordingStatus = {
|
|
5
|
+
IDLE: "idle",
|
|
6
|
+
LISTENING: "listening",
|
|
7
|
+
ERROR: "error",
|
|
8
|
+
PROCESSING: "processing",
|
|
9
|
+
TRANSCRIBING: "transcribing",
|
|
10
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare class Filemanager {
|
|
2
|
+
private pendingFiles;
|
|
3
|
+
private pendingMessage?;
|
|
4
|
+
private pendingFormat;
|
|
5
|
+
private messageId?;
|
|
6
|
+
/**
|
|
7
|
+
* Set the files for upload
|
|
8
|
+
*/
|
|
9
|
+
setFilesForUpload(files: File[], messageId?: string, message?: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Get the pending file state
|
|
12
|
+
*/
|
|
13
|
+
getPendingFileState(): {
|
|
14
|
+
files: File[];
|
|
15
|
+
messageId: string;
|
|
16
|
+
message: string;
|
|
17
|
+
format: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Upload files to presigned URLs
|
|
21
|
+
*/
|
|
22
|
+
uploadFilesToPresignedUrl(presignedUrls: string[]): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Zips multiple files into a single zip file
|
|
25
|
+
* @param files Array of files to zip
|
|
26
|
+
* @param zipFileName Name for the zip file (without extension)
|
|
27
|
+
* @returns Promise<Blob> - The zipped file as a blob
|
|
28
|
+
*/
|
|
29
|
+
zipFiles(): Promise<Blob>;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a File object from a blob with a specific name
|
|
32
|
+
* @param blob The blob to convert
|
|
33
|
+
* @param fileName The name for the file
|
|
34
|
+
* @returns File object
|
|
35
|
+
*/
|
|
36
|
+
blobToFile(blob: Blob, fileName: string): File;
|
|
37
|
+
/**
|
|
38
|
+
* Checks if multiple files should be zipped
|
|
39
|
+
* @param files Array of files
|
|
40
|
+
* @returns boolean - true if files should be zipped
|
|
41
|
+
*/
|
|
42
|
+
shouldZipFiles(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Gets the appropriate file name for upload (either zip or single file)
|
|
45
|
+
* @param files Array of files
|
|
46
|
+
* @returns string - the file name to use for upload
|
|
47
|
+
*/
|
|
48
|
+
getUploadFileName(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Clears the pending files
|
|
51
|
+
*/
|
|
52
|
+
clearPendingFilesState(): void;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=File.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"File.d.ts","sourceRoot":"","sources":["../../../src/media/file/File.ts"],"names":[],"mappings":"AAMA,qBAAa,WAAW;IACtB,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,cAAc,CAAC,CAAc;IACrC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,SAAS,CAAC,CAAc;IAEhC;;OAEG;IACI,iBAAiB,CACtB,KAAK,EAAE,IAAI,EAAE,EACb,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,IAAI;IAaP;;OAEG;IAEI,mBAAmB,IAAI;QAC5B,KAAK,EAAE,IAAI,EAAE,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB;IASD;;OAEG;IACU,yBAAyB,CACpC,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC;IA4ElB;;;;;OAKG;IACU,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IActC;;;;;OAKG;IACI,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIrD;;;;OAIG;IACI,cAAc,IAAI,OAAO;IAIhC;;;;OAIG;IACI,iBAAiB,IAAI,MAAM;IAOlC;;OAEG;IACI,sBAAsB,IAAI,IAAI;CAMtC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Filemanager = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Class to store file states
|
|
9
|
+
* Helpers: Zipping, uploading to presigned url, etc.
|
|
10
|
+
*/
|
|
11
|
+
const jszip_1 = __importDefault(require("jszip"));
|
|
12
|
+
const Error_1 = require("../../internal/Error/Error");
|
|
13
|
+
class Filemanager {
|
|
14
|
+
pendingFiles = [];
|
|
15
|
+
pendingMessage = "";
|
|
16
|
+
pendingFormat = "";
|
|
17
|
+
messageId = "";
|
|
18
|
+
/**
|
|
19
|
+
* Set the files for upload
|
|
20
|
+
*/
|
|
21
|
+
setFilesForUpload(files, messageId, message) {
|
|
22
|
+
if (this.pendingFiles.length > 0) {
|
|
23
|
+
this.clearPendingFilesState();
|
|
24
|
+
}
|
|
25
|
+
this.pendingFiles = files;
|
|
26
|
+
this.messageId = messageId || Date.now().toString();
|
|
27
|
+
if (message && message.trim()) {
|
|
28
|
+
this.pendingMessage = message;
|
|
29
|
+
}
|
|
30
|
+
this.pendingFormat =
|
|
31
|
+
files.length > 1 ? "zip" : files?.[0]?.type?.split("/")?.[1] || "unknown";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the pending file state
|
|
35
|
+
*/
|
|
36
|
+
getPendingFileState() {
|
|
37
|
+
return {
|
|
38
|
+
files: this.pendingFiles,
|
|
39
|
+
messageId: this.messageId || "",
|
|
40
|
+
message: this.pendingMessage || "",
|
|
41
|
+
format: this.pendingFormat || "",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Upload files to presigned URLs
|
|
46
|
+
*/
|
|
47
|
+
async uploadFilesToPresignedUrl(presignedUrls) {
|
|
48
|
+
let successCount = 0;
|
|
49
|
+
// Filter out invalid URLs
|
|
50
|
+
const validUrls = presignedUrls.filter((url) => url && url.trim());
|
|
51
|
+
try {
|
|
52
|
+
if (validUrls.length === 0) {
|
|
53
|
+
throw new Error_1.FileError("No valid presigned URLs provided", {
|
|
54
|
+
context: { stage: "uploadFilesToPresignedUrl" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
let fileToUpload;
|
|
58
|
+
let contentType;
|
|
59
|
+
if (this.shouldZipFiles()) {
|
|
60
|
+
// Zip multiple files into a single file
|
|
61
|
+
const zipBlob = await this.zipFiles();
|
|
62
|
+
const zipFileName = this.getUploadFileName();
|
|
63
|
+
fileToUpload = this.blobToFile(zipBlob, zipFileName);
|
|
64
|
+
contentType = "application/zip";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Single file, upload as is
|
|
68
|
+
fileToUpload = this.pendingFiles?.[0];
|
|
69
|
+
contentType = fileToUpload.type;
|
|
70
|
+
}
|
|
71
|
+
// Upload to all presigned URLs
|
|
72
|
+
for (let i = 0; i < validUrls.length; i++) {
|
|
73
|
+
const url = validUrls[i];
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: "PUT",
|
|
76
|
+
body: fileToUpload,
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": contentType,
|
|
79
|
+
"x-ms-blob-type": "BlockBlob",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error_1.FileError(`Failed to upload ${fileToUpload.name} to URL ${i + 1}: ${response.status} ${response.statusText}`, {
|
|
84
|
+
context: {
|
|
85
|
+
stage: "uploadFilesToPresignedUrl",
|
|
86
|
+
status: response.status,
|
|
87
|
+
url,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
successCount++;
|
|
92
|
+
}
|
|
93
|
+
// Call sendFileUploadComplete only once with the first presigned URL
|
|
94
|
+
// this.sendFileUploadComplete(validUrls[0]);
|
|
95
|
+
return validUrls[0];
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (successCount > 0) {
|
|
99
|
+
// this.sendFileUploadComplete(validUrls[0]);
|
|
100
|
+
return validUrls[0];
|
|
101
|
+
}
|
|
102
|
+
throw (0, Error_1.normalizeError)(error, Error_1.FileError, "Failed to upload files to presigned URL", { context: { stage: "uploadFilesToPresignedUrl" } });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Zips multiple files into a single zip file
|
|
107
|
+
* @param files Array of files to zip
|
|
108
|
+
* @param zipFileName Name for the zip file (without extension)
|
|
109
|
+
* @returns Promise<Blob> - The zipped file as a blob
|
|
110
|
+
*/
|
|
111
|
+
async zipFiles() {
|
|
112
|
+
const zip = new jszip_1.default();
|
|
113
|
+
// Add each file to the zip
|
|
114
|
+
this.pendingFiles.forEach((file) => {
|
|
115
|
+
zip.file(file.name, file);
|
|
116
|
+
});
|
|
117
|
+
// Generate the zip file as a blob
|
|
118
|
+
const zipBlob = await zip.generateAsync({ type: "blob" });
|
|
119
|
+
return zipBlob;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Creates a File object from a blob with a specific name
|
|
123
|
+
* @param blob The blob to convert
|
|
124
|
+
* @param fileName The name for the file
|
|
125
|
+
* @returns File object
|
|
126
|
+
*/
|
|
127
|
+
blobToFile(blob, fileName) {
|
|
128
|
+
return new File([blob], fileName, { type: blob.type });
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Checks if multiple files should be zipped
|
|
132
|
+
* @param files Array of files
|
|
133
|
+
* @returns boolean - true if files should be zipped
|
|
134
|
+
*/
|
|
135
|
+
shouldZipFiles() {
|
|
136
|
+
return this.pendingFiles.length > 1;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Gets the appropriate file name for upload (either zip or single file)
|
|
140
|
+
* @param files Array of files
|
|
141
|
+
* @returns string - the file name to use for upload
|
|
142
|
+
*/
|
|
143
|
+
getUploadFileName() {
|
|
144
|
+
if (this.shouldZipFiles()) {
|
|
145
|
+
return `uploaded_files_${Date.now()}.zip`;
|
|
146
|
+
}
|
|
147
|
+
return this.pendingFiles[0]?.name || "file";
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Clears the pending files
|
|
151
|
+
*/
|
|
152
|
+
clearPendingFilesState() {
|
|
153
|
+
this.pendingFiles = [];
|
|
154
|
+
this.pendingMessage = "";
|
|
155
|
+
this.pendingFormat = "";
|
|
156
|
+
this.messageId = "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.Filemanager = Filemanager;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessageManager class to handle sending and receiving messages
|
|
3
|
+
* Works with BaseConnection and Store
|
|
4
|
+
*/
|
|
5
|
+
import { type SynapseSDKCallbacks } from "../Synapse";
|
|
6
|
+
import { type IncomingSocketChatMessage, type IncomingSocketStreamMessage, type IncomingSocketErrorMessage, type IncomingSocketEndOfStreamMessage } from "../events/Events";
|
|
7
|
+
import { BaseConnection } from "../internal/connection/BaseConnection";
|
|
8
|
+
import { type SocketChatRequestData } from "./types";
|
|
9
|
+
import { type DisconnectionDetails } from "../internal/connection/types";
|
|
10
|
+
export declare class MessageManager {
|
|
11
|
+
private connection;
|
|
12
|
+
private callbacks;
|
|
13
|
+
private fileManager;
|
|
14
|
+
private audioManager;
|
|
15
|
+
constructor(connection: BaseConnection, callbacks?: SynapseSDKCallbacks);
|
|
16
|
+
/**
|
|
17
|
+
* Handle file upload process
|
|
18
|
+
*/
|
|
19
|
+
private handleFileUploadProcess;
|
|
20
|
+
/**
|
|
21
|
+
* send chat message through socket
|
|
22
|
+
*/
|
|
23
|
+
sendSocketChatMessage({ message, messageId, type, files, url, tool_use_id, tool_use_params, hidden, }: SocketChatRequestData): void;
|
|
24
|
+
/**
|
|
25
|
+
* Handle incoming chat message
|
|
26
|
+
*/
|
|
27
|
+
handleIncomingSocketChatMessage(message: IncomingSocketChatMessage): void;
|
|
28
|
+
/**
|
|
29
|
+
* Handle incoming stream message
|
|
30
|
+
*/
|
|
31
|
+
handleIncomingSocketStreamMessage(message: IncomingSocketStreamMessage): void;
|
|
32
|
+
handleDisconnect(details: DisconnectionDetails): void;
|
|
33
|
+
/**
|
|
34
|
+
* Handle incoming end of stream message
|
|
35
|
+
*/
|
|
36
|
+
handleIncomingSocketEndOfStreamMessage(_message: IncomingSocketEndOfStreamMessage): void;
|
|
37
|
+
/**
|
|
38
|
+
* Handle incoming error message
|
|
39
|
+
*/
|
|
40
|
+
handleIncomingSocketErrorMessage(message: IncomingSocketErrorMessage): void;
|
|
41
|
+
/**
|
|
42
|
+
* send authentication message
|
|
43
|
+
*/
|
|
44
|
+
sendSocketAuthMessage(sessionToken: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* send ping message
|
|
47
|
+
*/
|
|
48
|
+
sendSocketPingMessage(): void;
|
|
49
|
+
/**
|
|
50
|
+
* start recording audio
|
|
51
|
+
*/
|
|
52
|
+
private sendSocketStreamMessage;
|
|
53
|
+
/**
|
|
54
|
+
* send audio message
|
|
55
|
+
*/
|
|
56
|
+
startRecordingWithSocket(): void;
|
|
57
|
+
/**
|
|
58
|
+
* stop recording audio
|
|
59
|
+
*/
|
|
60
|
+
endRecordingWithSocket(): void;
|
|
61
|
+
/**
|
|
62
|
+
* send pong message
|
|
63
|
+
*/
|
|
64
|
+
sendSocketPongMessage(): void;
|
|
65
|
+
handleConnectionEstablished(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Cleanup message service
|
|
68
|
+
*/
|
|
69
|
+
cleanupMessageServerState(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Generate a unique message ID
|
|
72
|
+
*/
|
|
73
|
+
private generateId;
|
|
74
|
+
private emitError;
|
|
75
|
+
private toFileError;
|
|
76
|
+
private toRecordingError;
|
|
77
|
+
private assertConnection;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=MessageManager.d.ts.map
|