@memberjunction/ai-agents 5.43.0 → 5.44.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 +8 -0
- package/dist/DuplicateReasoningAgentProvider.d.ts +35 -0
- package/dist/DuplicateReasoningAgentProvider.d.ts.map +1 -0
- package/dist/DuplicateReasoningAgentProvider.js +95 -0
- package/dist/DuplicateReasoningAgentProvider.js.map +1 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts +7 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts.map +1 -1
- package/dist/MJAIAgentRequestEntityServer.js +28 -1
- package/dist/MJAIAgentRequestEntityServer.js.map +1 -1
- package/dist/SkillImportExportService.d.ts +63 -0
- package/dist/SkillImportExportService.d.ts.map +1 -0
- package/dist/SkillImportExportService.js +148 -0
- package/dist/SkillImportExportService.js.map +1 -0
- package/dist/SkillMarkdownConverter.d.ts +82 -0
- package/dist/SkillMarkdownConverter.d.ts.map +1 -0
- package/dist/SkillMarkdownConverter.js +145 -0
- package/dist/SkillMarkdownConverter.js.map +1 -0
- package/dist/agent-types/loop-agent-response-type.d.ts +17 -2
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +33 -1
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +373 -12
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1007 -78
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +39 -0
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +118 -5
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/operations/AISkillMarkdownOperations.d.ts +11 -0
- package/dist/operations/AISkillMarkdownOperations.d.ts.map +1 -0
- package/dist/operations/AISkillMarkdownOperations.js +57 -0
- package/dist/operations/AISkillMarkdownOperations.js.map +1 -0
- package/dist/prompt-component-resolver.d.ts +73 -0
- package/dist/prompt-component-resolver.d.ts.map +1 -0
- package/dist/prompt-component-resolver.js +166 -0
- package/dist/prompt-component-resolver.js.map +1 -0
- package/dist/realtime/agent-media-library.d.ts +65 -0
- package/dist/realtime/agent-media-library.d.ts.map +1 -0
- package/dist/realtime/agent-media-library.js +160 -0
- package/dist/realtime/agent-media-library.js.map +1 -0
- package/dist/realtime/client-context-channel-server.d.ts +59 -0
- package/dist/realtime/client-context-channel-server.d.ts.map +1 -0
- package/dist/realtime/client-context-channel-server.js +78 -0
- package/dist/realtime/client-context-channel-server.js.map +1 -0
- package/dist/realtime/media-channel-server.d.ts +73 -0
- package/dist/realtime/media-channel-server.d.ts.map +1 -0
- package/dist/realtime/media-channel-server.js +145 -0
- package/dist/realtime/media-channel-server.js.map +1 -0
- package/dist/realtime/realtime-channel-server-data-context.d.ts +42 -0
- package/dist/realtime/realtime-channel-server-data-context.d.ts.map +1 -0
- package/dist/realtime/realtime-channel-server-data-context.js +29 -0
- package/dist/realtime/realtime-channel-server-data-context.js.map +1 -0
- package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -1
- package/dist/realtime/realtime-channel-server-host.js +8 -2
- package/dist/realtime/realtime-channel-server-host.js.map +1 -1
- package/dist/realtime/realtime-client-session-service.d.ts +85 -5
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +234 -25
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +82 -3
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +147 -6
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-recording-capture.d.ts +125 -0
- package/dist/realtime/realtime-recording-capture.d.ts.map +1 -0
- package/dist/realtime/realtime-recording-capture.js +317 -0
- package/dist/realtime/realtime-recording-capture.js.map +1 -0
- package/dist/realtime/realtime-recording-store.d.ts +105 -0
- package/dist/realtime/realtime-recording-store.d.ts.map +1 -0
- package/dist/realtime/realtime-recording-store.js +216 -0
- package/dist/realtime/realtime-recording-store.js.map +1 -0
- package/dist/realtime/realtime-session-runner.d.ts +33 -2
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
- package/dist/realtime/realtime-session-runner.js +45 -2
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/realtime/realtime-tool-broker.d.ts +41 -3
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -1
- package/dist/realtime/realtime-tool-broker.js +67 -3
- package/dist/realtime/realtime-tool-broker.js.map +1 -1
- package/package.json +18 -17
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure, zero-dependency server-side audio recording utility for
|
|
3
|
+
* MemberJunction realtime sessions.
|
|
4
|
+
*
|
|
5
|
+
* Accumulates realtime PCM audio frames (model output + user input), mixes them
|
|
6
|
+
* into a single mono PCM16 timeline, and encodes a canonical WAV file. The only
|
|
7
|
+
* runtime dependency is the Node `Buffer` global — no external npm packages.
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/ai-agents/realtime/realtime-recording-capture
|
|
10
|
+
*/
|
|
11
|
+
/** Lowest value representable by signed 16-bit PCM. */
|
|
12
|
+
const PCM16_MIN = -32768;
|
|
13
|
+
/** Highest value representable by signed 16-bit PCM. */
|
|
14
|
+
const PCM16_MAX = 32767;
|
|
15
|
+
/** Canonical WAV/PCM header size in bytes. */
|
|
16
|
+
const WAV_HEADER_BYTES = 44;
|
|
17
|
+
/** Default waveform resolution (bucket count) produced by {@link RealtimeRecordingController.GetPeaks}. */
|
|
18
|
+
const DEFAULT_PEAK_BUCKETS = 600;
|
|
19
|
+
/**
|
|
20
|
+
* Accumulates realtime PCM audio frames from both the model (outbound) and the
|
|
21
|
+
* user (inbound) and mixes them into a single mono PCM16 WAV on demand.
|
|
22
|
+
*
|
|
23
|
+
* Lifecycle: construct → {@link Start} → {@link AppendOutbound}/{@link AppendInbound}
|
|
24
|
+
* (many) → {@link Stop} → {@link EncodeWav}. {@link Reset} returns the instance to
|
|
25
|
+
* a pristine state so it can be reused.
|
|
26
|
+
*/
|
|
27
|
+
export class RealtimeRecordingController {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.recording = false;
|
|
30
|
+
this.startedAt = null;
|
|
31
|
+
this.startEpochMs = null;
|
|
32
|
+
this.outboundFrames = [];
|
|
33
|
+
this.inboundFrames = [];
|
|
34
|
+
this.outputSampleRate = options?.OutputSampleRate ?? 24000;
|
|
35
|
+
this.inputSampleRate = options?.InputSampleRate ?? 24000;
|
|
36
|
+
this.media = options?.Media ?? 'Audio';
|
|
37
|
+
this.now = options?.Now ?? (() => Date.now());
|
|
38
|
+
}
|
|
39
|
+
/** Stamp t0 and begin accepting frames. Idempotent (second call is a no-op). */
|
|
40
|
+
Start() {
|
|
41
|
+
if (this.recording) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const epoch = this.now();
|
|
45
|
+
this.startEpochMs = epoch;
|
|
46
|
+
this.startedAt = new Date(epoch);
|
|
47
|
+
this.recording = true;
|
|
48
|
+
}
|
|
49
|
+
/** Stop accepting frames. Idempotent. Does NOT clear buffers. */
|
|
50
|
+
Stop() {
|
|
51
|
+
this.recording = false;
|
|
52
|
+
}
|
|
53
|
+
/** Clear all state so the instance can be reused. */
|
|
54
|
+
Reset() {
|
|
55
|
+
this.recording = false;
|
|
56
|
+
this.startedAt = null;
|
|
57
|
+
this.startEpochMs = null;
|
|
58
|
+
this.outboundFrames = [];
|
|
59
|
+
this.inboundFrames = [];
|
|
60
|
+
}
|
|
61
|
+
get IsRecording() {
|
|
62
|
+
return this.recording;
|
|
63
|
+
}
|
|
64
|
+
/** The wall-clock moment {@link Start} stamped, or null if never started. */
|
|
65
|
+
get StartedAt() {
|
|
66
|
+
return this.startedAt;
|
|
67
|
+
}
|
|
68
|
+
get Media() {
|
|
69
|
+
return this.media;
|
|
70
|
+
}
|
|
71
|
+
/** Whether any non-empty frame was captured. */
|
|
72
|
+
get HasAudio() {
|
|
73
|
+
return this.frameHasSamples(this.outboundFrames) || this.frameHasSamples(this.inboundFrames);
|
|
74
|
+
}
|
|
75
|
+
/** ms since {@link Start} per the injected clock; 0 if not started (never negative). */
|
|
76
|
+
NowOffsetMs() {
|
|
77
|
+
if (this.startEpochMs == null) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const delta = this.now() - this.startEpochMs;
|
|
81
|
+
return delta > 0 ? delta : 0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Append a model-output audio frame (ArrayBuffer of little-endian PCM16).
|
|
85
|
+
* No-op if not recording or empty. The bytes are copied — the caller's
|
|
86
|
+
* ArrayBuffer is not retained.
|
|
87
|
+
*/
|
|
88
|
+
AppendOutbound(chunk) {
|
|
89
|
+
this.appendFrame(this.outboundFrames, chunk);
|
|
90
|
+
}
|
|
91
|
+
/** Append a user-input audio frame. Same contract as {@link AppendOutbound}. */
|
|
92
|
+
AppendInbound(chunk) {
|
|
93
|
+
this.appendFrame(this.inboundFrames, chunk);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Mix everything captured into a mono PCM16 WAV.
|
|
97
|
+
* @returns The encoded buffer, duration, and sample rate; or null if {@link HasAudio} is false.
|
|
98
|
+
*/
|
|
99
|
+
EncodeWav() {
|
|
100
|
+
const mixed = this.mixTimeline();
|
|
101
|
+
if (!mixed) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const buffer = this.encodeWavBuffer(mixed.Pcm, mixed.SampleRate);
|
|
105
|
+
const durationMs = (mixed.Pcm.length / mixed.SampleRate) * 1000;
|
|
106
|
+
return { Buffer: buffer, DurationMs: durationMs, SampleRate: mixed.SampleRate };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Compute capture-time waveform peaks from the accumulated PCM: a normalized `0..1` max-abs
|
|
110
|
+
* amplitude per bucket (one bucket per rendered waveform bar). Mirrors the client-direct peak
|
|
111
|
+
* math so server-bridged recordings render the same kind of waveform; the result is persisted
|
|
112
|
+
* as a `peaks.json` sidecar so a viewer renders the waveform without re-decoding the audio.
|
|
113
|
+
*
|
|
114
|
+
* @param buckets Desired bucket count (waveform resolution). Default 600; clamped to `>= 1` and
|
|
115
|
+
* never exceeds the sample count (a short recording yields at most one peak per sample).
|
|
116
|
+
* @returns A bounded array of normalized `0..1` peaks; `[]` when there is no audio (or silence).
|
|
117
|
+
*/
|
|
118
|
+
GetPeaks(buckets = DEFAULT_PEAK_BUCKETS) {
|
|
119
|
+
const mixed = this.mixTimeline();
|
|
120
|
+
if (!mixed) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
return this.computePeaks(mixed.Pcm, buckets);
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------
|
|
126
|
+
// Internal helpers
|
|
127
|
+
// ---------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Mix all captured frames into a single mono PCM16 timeline at the output sample rate.
|
|
130
|
+
* Shared by {@link EncodeWav} and {@link GetPeaks} so the WAV and the peaks describe the
|
|
131
|
+
* exact same signal. Returns `null` when nothing was captured.
|
|
132
|
+
*/
|
|
133
|
+
mixTimeline() {
|
|
134
|
+
if (!this.HasAudio) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const targetRate = this.outputSampleRate;
|
|
138
|
+
const timelineLength = this.computeTimelineLength(targetRate);
|
|
139
|
+
if (timelineLength <= 0) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const accumulator = new Float64Array(timelineLength);
|
|
143
|
+
this.mixFrames(accumulator, this.outboundFrames, targetRate, this.outputSampleRate);
|
|
144
|
+
this.mixFrames(accumulator, this.inboundFrames, targetRate, this.inputSampleRate);
|
|
145
|
+
return { Pcm: this.clampToInt16(accumulator), SampleRate: targetRate };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Downsample a PCM16 timeline into `buckets` normalized `0..1` max-abs peaks. Contiguous,
|
|
149
|
+
* index-ordered buckets; the loudest bucket becomes `1`. An all-silent timeline yields `[]`.
|
|
150
|
+
*/
|
|
151
|
+
computePeaks(pcm, buckets) {
|
|
152
|
+
const length = pcm.length;
|
|
153
|
+
if (length === 0) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
const count = Math.min(Math.max(1, Math.floor(buckets)), length);
|
|
157
|
+
const out = new Array(count).fill(0);
|
|
158
|
+
let globalMax = 0;
|
|
159
|
+
for (let b = 0; b < count; b++) {
|
|
160
|
+
const start = Math.floor((b * length) / count);
|
|
161
|
+
const end = b === count - 1 ? length : Math.floor(((b + 1) * length) / count);
|
|
162
|
+
let localMax = 0;
|
|
163
|
+
for (let i = start; i < end; i++) {
|
|
164
|
+
const abs = Math.abs(pcm[i]);
|
|
165
|
+
if (abs > localMax) {
|
|
166
|
+
localMax = abs;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
out[b] = localMax;
|
|
170
|
+
if (localMax > globalMax) {
|
|
171
|
+
globalMax = localMax;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (globalMax <= 0) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
return out.map((v) => v / globalMax);
|
|
178
|
+
}
|
|
179
|
+
/** Decode + copy a chunk into a frame and push it onto the given list. */
|
|
180
|
+
appendFrame(list, chunk) {
|
|
181
|
+
if (!this.recording || chunk == null || chunk.byteLength < 2) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const samples = this.decodePcm16(chunk);
|
|
185
|
+
if (samples.length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
list.push({ offsetMs: this.NowOffsetMs(), samples });
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Decode a little-endian PCM16 ArrayBuffer into a copied Int16Array.
|
|
192
|
+
* Odd-length buffers (not a multiple of 2) drop the trailing byte.
|
|
193
|
+
*/
|
|
194
|
+
decodePcm16(chunk) {
|
|
195
|
+
const usableBytes = chunk.byteLength - (chunk.byteLength % 2);
|
|
196
|
+
// Copy the bytes so later mutation of the caller's source is safe.
|
|
197
|
+
const copy = chunk.slice(0, usableBytes);
|
|
198
|
+
return new Int16Array(copy);
|
|
199
|
+
}
|
|
200
|
+
/** Whether any frame in the list carries at least one sample. */
|
|
201
|
+
frameHasSamples(list) {
|
|
202
|
+
return list.some((frame) => frame.samples.length > 0);
|
|
203
|
+
}
|
|
204
|
+
/** The number of target-rate samples a frame occupies (after any resample). */
|
|
205
|
+
frameTargetLength(frame, targetRate, sourceRate) {
|
|
206
|
+
if (sourceRate === targetRate) {
|
|
207
|
+
return frame.samples.length;
|
|
208
|
+
}
|
|
209
|
+
return Math.round((frame.samples.length * targetRate) / sourceRate);
|
|
210
|
+
}
|
|
211
|
+
/** Start sample index for a frame given its offset and the target rate. */
|
|
212
|
+
frameStartSample(frame, targetRate) {
|
|
213
|
+
return Math.round((frame.offsetMs * targetRate) / 1000);
|
|
214
|
+
}
|
|
215
|
+
/** The total length (in target-rate samples) needed to cover the furthest frame end. */
|
|
216
|
+
computeTimelineLength(targetRate) {
|
|
217
|
+
let maxEnd = 0;
|
|
218
|
+
const consider = (list, sourceRate) => {
|
|
219
|
+
for (const frame of list) {
|
|
220
|
+
if (frame.samples.length === 0) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const start = this.frameStartSample(frame, targetRate);
|
|
224
|
+
const end = start + this.frameTargetLength(frame, targetRate, sourceRate);
|
|
225
|
+
if (end > maxEnd) {
|
|
226
|
+
maxEnd = end;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
consider(this.outboundFrames, this.outputSampleRate);
|
|
231
|
+
consider(this.inboundFrames, this.inputSampleRate);
|
|
232
|
+
return maxEnd;
|
|
233
|
+
}
|
|
234
|
+
/** Sum each frame's (possibly resampled) samples into the mono accumulator. */
|
|
235
|
+
mixFrames(accumulator, list, targetRate, sourceRate) {
|
|
236
|
+
for (const frame of list) {
|
|
237
|
+
if (frame.samples.length === 0) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const start = this.frameStartSample(frame, targetRate);
|
|
241
|
+
const resampled = sourceRate === targetRate ? frame.samples : this.resampleLinear(frame.samples, sourceRate, targetRate);
|
|
242
|
+
for (let i = 0; i < resampled.length; i++) {
|
|
243
|
+
const idx = start + i;
|
|
244
|
+
if (idx >= 0 && idx < accumulator.length) {
|
|
245
|
+
accumulator[idx] += resampled[i];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Resample PCM16 samples from sourceRate to targetRate via simple linear
|
|
252
|
+
* interpolation. Returns a Float64Array (caller sums into the accumulator).
|
|
253
|
+
*/
|
|
254
|
+
resampleLinear(samples, sourceRate, targetRate) {
|
|
255
|
+
const outLength = Math.round((samples.length * targetRate) / sourceRate);
|
|
256
|
+
const out = new Float64Array(outLength);
|
|
257
|
+
if (samples.length === 1) {
|
|
258
|
+
out.fill(samples[0]);
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
const ratio = (samples.length - 1) / Math.max(1, outLength - 1);
|
|
262
|
+
for (let i = 0; i < outLength; i++) {
|
|
263
|
+
const srcPos = i * ratio;
|
|
264
|
+
const lower = Math.floor(srcPos);
|
|
265
|
+
const upper = Math.min(lower + 1, samples.length - 1);
|
|
266
|
+
const frac = srcPos - lower;
|
|
267
|
+
out[i] = samples[lower] * (1 - frac) + samples[upper] * frac;
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
/** Clamp the float accumulator into the Int16 range and round to integers. */
|
|
272
|
+
clampToInt16(accumulator) {
|
|
273
|
+
const out = new Int16Array(accumulator.length);
|
|
274
|
+
for (let i = 0; i < accumulator.length; i++) {
|
|
275
|
+
let value = Math.round(accumulator[i]);
|
|
276
|
+
if (value > PCM16_MAX) {
|
|
277
|
+
value = PCM16_MAX;
|
|
278
|
+
}
|
|
279
|
+
else if (value < PCM16_MIN) {
|
|
280
|
+
value = PCM16_MIN;
|
|
281
|
+
}
|
|
282
|
+
out[i] = value;
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** Build a canonical 44-byte WAV/PCM header followed by the sample data. */
|
|
287
|
+
encodeWavBuffer(pcm, sampleRate) {
|
|
288
|
+
const numChannels = 1;
|
|
289
|
+
const bitsPerSample = 16;
|
|
290
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
291
|
+
const blockAlign = numChannels * bytesPerSample;
|
|
292
|
+
const byteRate = sampleRate * blockAlign;
|
|
293
|
+
const dataSize = pcm.length * bytesPerSample;
|
|
294
|
+
const buffer = Buffer.alloc(WAV_HEADER_BYTES + dataSize);
|
|
295
|
+
// RIFF chunk descriptor
|
|
296
|
+
buffer.write('RIFF', 0, 'ascii');
|
|
297
|
+
buffer.writeUInt32LE(36 + dataSize, 4); // ChunkSize = 36 + Subchunk2Size
|
|
298
|
+
buffer.write('WAVE', 8, 'ascii');
|
|
299
|
+
// fmt sub-chunk
|
|
300
|
+
buffer.write('fmt ', 12, 'ascii');
|
|
301
|
+
buffer.writeUInt32LE(16, 16); // Subchunk1Size for PCM
|
|
302
|
+
buffer.writeUInt16LE(1, 20); // AudioFormat = PCM
|
|
303
|
+
buffer.writeUInt16LE(numChannels, 22);
|
|
304
|
+
buffer.writeUInt32LE(sampleRate, 24);
|
|
305
|
+
buffer.writeUInt32LE(byteRate, 28);
|
|
306
|
+
buffer.writeUInt16LE(blockAlign, 32);
|
|
307
|
+
buffer.writeUInt16LE(bitsPerSample, 34);
|
|
308
|
+
// data sub-chunk
|
|
309
|
+
buffer.write('data', 36, 'ascii');
|
|
310
|
+
buffer.writeUInt32LE(dataSize, 40);
|
|
311
|
+
for (let i = 0; i < pcm.length; i++) {
|
|
312
|
+
buffer.writeInt16LE(pcm[i], WAV_HEADER_BYTES + i * bytesPerSample);
|
|
313
|
+
}
|
|
314
|
+
return buffer;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=realtime-recording-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-recording-capture.js","sourceRoot":"","sources":["../../src/realtime/realtime-recording-capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared MJStorage finalize for realtime session recordings — the single code path both
|
|
3
|
+
* the **server-bridged** capture (`BaseAgent.finalizeRealtimeRecording`) and the **client-direct**
|
|
4
|
+
* browser upload (`RealtimeClientSessionResolver.UploadRealtimeRecording`) use to: resolve the agent's
|
|
5
|
+
* recording storage account, upload the audio, link it to the `AIAgentSession`, and stamp the session's
|
|
6
|
+
* recording fields. Keeps the storage policy in one place rather than duplicated per topology.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/ai-agents
|
|
9
|
+
*/
|
|
10
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
11
|
+
import { MJAIAgentEntity } from '@memberjunction/core-entities';
|
|
12
|
+
import { RealtimeRecordingMedia } from './realtime-recording-capture.js';
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the storage account a session recording should be written to: the agent's
|
|
15
|
+
* `RecordingStorageProviderID` if set, else its `AttachmentStorageProviderID` (recordings default to the
|
|
16
|
+
* attachments account), resolved to that provider's first account. Returns `null` when nothing is
|
|
17
|
+
* configured — callers treat that as "do not record" (fail-closed).
|
|
18
|
+
*
|
|
19
|
+
* @param agent The agent whose recording/attachment storage configuration drives the choice.
|
|
20
|
+
* @returns The resolved storage account id, or `null`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveRecordingStorageAccountID(agent: MJAIAgentEntity, contextUser: UserInfo, provider?: IMetadataProvider): Promise<string | null>;
|
|
23
|
+
/** Input to {@link storeRealtimeRecording}. */
|
|
24
|
+
export interface StoreRealtimeRecordingInput {
|
|
25
|
+
/** The encoded recording bytes (e.g. a WAV from the server mixer, or the browser's seekable WAV). */
|
|
26
|
+
Audio: Buffer;
|
|
27
|
+
/** MIME type of the audio (`audio/wav`, `audio/webm`, `audio/ogg`, `audio/mp4`). */
|
|
28
|
+
MimeType: string;
|
|
29
|
+
/** What was captured, stamped on the session. */
|
|
30
|
+
Media: RealtimeRecordingMedia;
|
|
31
|
+
/** The recording `t0` (alignment origin), stamped on the session. */
|
|
32
|
+
StartedAt: Date;
|
|
33
|
+
/** The resolved storage account id (see {@link resolveRecordingStorageAccountID}). */
|
|
34
|
+
StorageAccountID: string;
|
|
35
|
+
/** The `AIAgentSession` id the recording belongs to. */
|
|
36
|
+
SessionID: string;
|
|
37
|
+
ContextUser: UserInfo;
|
|
38
|
+
Provider: IMetadataProvider;
|
|
39
|
+
/**
|
|
40
|
+
* Optional capture-time waveform peaks (max-abs per bucket, normalized 0..1). When present, written
|
|
41
|
+
* as a `peaks.json` sidecar in the SAME session folder as the recording so a viewer can render the
|
|
42
|
+
* waveform without re-decoding the audio. Best-effort — a sidecar failure never fails the recording.
|
|
43
|
+
*/
|
|
44
|
+
Peaks?: number[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Reads a stored recording's bytes back through **authenticated** MJStorage (server-side `GetObject` on
|
|
48
|
+
* the file's own provider/account) — NOT a public pre-signed link. Used to stream a recording to an
|
|
49
|
+
* authorized browser securely. Never throws.
|
|
50
|
+
*
|
|
51
|
+
* @param fileID The `MJ: Files` id of the recording.
|
|
52
|
+
* @returns `{ Bytes, MimeType }` or `null` when the file/account/object can't be resolved.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readRealtimeRecordingFile(fileID: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<{
|
|
55
|
+
Bytes: Buffer;
|
|
56
|
+
MimeType: string;
|
|
57
|
+
} | null>;
|
|
58
|
+
/** Input to {@link writeRealtimeRecordingSegment}. */
|
|
59
|
+
export interface WriteRecordingSegmentInput {
|
|
60
|
+
SessionID: string;
|
|
61
|
+
/** 0-based index of this ~15s shard within the session. */
|
|
62
|
+
SegmentIndex: number;
|
|
63
|
+
Audio: Buffer;
|
|
64
|
+
MimeType: string;
|
|
65
|
+
StorageAccountID: string;
|
|
66
|
+
ContextUser: UserInfo;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Writes ONE crash-recovery segment shard (`seg-NNNN.<ext>`) into the session's folder as a RAW
|
|
70
|
+
* storage object — no `MJ: Files` row, no session stamping. Shards are durability insurance during a
|
|
71
|
+
* live call (so a browser/tab death loses at most the last window); they are byte-slices of one
|
|
72
|
+
* continuous stream (only the first carries the container header), so they are NOT individually
|
|
73
|
+
* playable — recovery is "concatenate the folder's shards in order". They are deleted once the
|
|
74
|
+
* canonical consolidated file lands ({@link deleteRealtimeRecordingSegments}). Never throws.
|
|
75
|
+
*
|
|
76
|
+
* @returns `true` on success.
|
|
77
|
+
*/
|
|
78
|
+
export declare function writeRealtimeRecordingSegment(input: WriteRecordingSegmentInput): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* Writes the capture-time waveform peaks as a `peaks.json` sidecar (a JSON array of numbers) into the
|
|
81
|
+
* session's recording folder via the storage driver. Best-effort and tolerant — a missing/empty peaks
|
|
82
|
+
* array is a no-op, and any storage failure is logged and swallowed (the recording itself already
|
|
83
|
+
* succeeded). Never throws.
|
|
84
|
+
*
|
|
85
|
+
* @returns `true` when a sidecar was written.
|
|
86
|
+
*/
|
|
87
|
+
export declare function writeRecordingPeaksSidecar(sessionID: string, storageAccountID: string, peaks: number[] | undefined, contextUser: UserInfo): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Deletes the `seg-*` shards in a session's folder, leaving the consolidated `recording.*` file. Called
|
|
90
|
+
* after {@link storeRealtimeRecording} writes the canonical file at end of call. Never throws.
|
|
91
|
+
*
|
|
92
|
+
* @returns The number of shards deleted.
|
|
93
|
+
*/
|
|
94
|
+
export declare function deleteRealtimeRecordingSegments(sessionID: string, storageAccountID: string, contextUser: UserInfo): Promise<number>;
|
|
95
|
+
/**
|
|
96
|
+
* Uploads a session recording to MJStorage, links it to the `AIAgentSession` (via
|
|
97
|
+
* `MJ: File Entity Record Links`), and stamps `RecordingFileID` / `RecordingMedia` / `RecordingStartedAt`
|
|
98
|
+
* on the session. Never throws — a recording-storage failure must not fail the session; failures are
|
|
99
|
+
* logged and surfaced as a `null` return.
|
|
100
|
+
*
|
|
101
|
+
* @param input The recording bytes + storage account + session context.
|
|
102
|
+
* @returns The created `MJ: Files` id, or `null` on failure.
|
|
103
|
+
*/
|
|
104
|
+
export declare function storeRealtimeRecording(input: StoreRealtimeRecordingInput): Promise<string | null>;
|
|
105
|
+
//# sourceMappingURL=realtime-recording-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-recording-store.d.ts","sourceRoot":"","sources":["../../src/realtime/realtime-recording-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAY,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAsE,MAAM,+BAA+B,CAAC;AAEpI,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAEtE;;;;;;;;GAQG;AACH,wBAAsB,gCAAgC,CAClD,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAC5E,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBxB;AAED,+CAA+C;AAC/C,MAAM,WAAW,2BAA2B;IACxC,qGAAqG;IACrG,KAAK,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,KAAK,EAAE,sBAAsB,CAAC;IAC9B,qEAAqE;IACrE,SAAS,EAAE,IAAI,CAAC;IAChB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,CAAC;IACzB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,QAAQ,CAAC;IACtB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAkBD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC3C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,iBAAiB,GACnE,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA2BrD;AAED,sDAAsD;AACtD,MAAM,WAAW,0BAA0B;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,QAAQ,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,6BAA6B,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,CAUvG;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC5C,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,WAAW,EAAE,QAAQ,GAChG,OAAO,CAAC,OAAO,CAAC,CAYlB;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBzI;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8CvG"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared MJStorage finalize for realtime session recordings — the single code path both
|
|
3
|
+
* the **server-bridged** capture (`BaseAgent.finalizeRealtimeRecording`) and the **client-direct**
|
|
4
|
+
* browser upload (`RealtimeClientSessionResolver.UploadRealtimeRecording`) use to: resolve the agent's
|
|
5
|
+
* recording storage account, upload the audio, link it to the `AIAgentSession`, and stamp the session's
|
|
6
|
+
* recording fields. Keeps the storage policy in one place rather than duplicated per topology.
|
|
7
|
+
*
|
|
8
|
+
* @module @memberjunction/ai-agents
|
|
9
|
+
*/
|
|
10
|
+
import { LogError } from '@memberjunction/core';
|
|
11
|
+
import { FileStorageEngine } from '@memberjunction/storage';
|
|
12
|
+
/**
|
|
13
|
+
* Resolves the storage account a session recording should be written to: the agent's
|
|
14
|
+
* `RecordingStorageProviderID` if set, else its `AttachmentStorageProviderID` (recordings default to the
|
|
15
|
+
* attachments account), resolved to that provider's first account. Returns `null` when nothing is
|
|
16
|
+
* configured — callers treat that as "do not record" (fail-closed).
|
|
17
|
+
*
|
|
18
|
+
* @param agent The agent whose recording/attachment storage configuration drives the choice.
|
|
19
|
+
* @returns The resolved storage account id, or `null`.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveRecordingStorageAccountID(agent, contextUser, provider) {
|
|
22
|
+
const providerID = agent.RecordingStorageProviderID || agent.AttachmentStorageProviderID;
|
|
23
|
+
if (!providerID) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// Ensure the storage engine has loaded its accounts before reading them. The engine may be
|
|
27
|
+
// unconfigured in this process, or configured BEFORE this storage account was provisioned (a stale
|
|
28
|
+
// cache). Try the loaded cache first; if the provider's account isn't found, force a one-time refresh
|
|
29
|
+
// so a newly-provisioned account is picked up WITHOUT requiring a server restart.
|
|
30
|
+
await FileStorageEngine.Instance.Config(false, contextUser, provider);
|
|
31
|
+
let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(providerID);
|
|
32
|
+
if (accounts.length === 0) {
|
|
33
|
+
await FileStorageEngine.Instance.Config(true, contextUser, provider);
|
|
34
|
+
accounts = FileStorageEngine.Instance.GetAccountsByProviderID(providerID);
|
|
35
|
+
}
|
|
36
|
+
return accounts[0]?.ID ?? null;
|
|
37
|
+
}
|
|
38
|
+
/** A short, stable file extension for the recording's MIME type. */
|
|
39
|
+
function extensionForMime(mimeType) {
|
|
40
|
+
if (mimeType.includes('webm'))
|
|
41
|
+
return 'webm';
|
|
42
|
+
if (mimeType.includes('ogg'))
|
|
43
|
+
return 'ogg';
|
|
44
|
+
if (mimeType.includes('mp4') || mimeType.includes('m4a'))
|
|
45
|
+
return 'm4a';
|
|
46
|
+
// Header-less raw PCM crash-recovery shards (audio/L16 / audio/pcm) — NOT individually playable;
|
|
47
|
+
// recovery concatenates them in order and WAV-wraps. The consolidated file is always WAV.
|
|
48
|
+
if (mimeType.includes('L16') || mimeType.includes('pcm'))
|
|
49
|
+
return 'pcm';
|
|
50
|
+
return 'wav';
|
|
51
|
+
}
|
|
52
|
+
/** The per-session folder all of a session's recording artifacts live in (shards + final file). */
|
|
53
|
+
function recordingFolder(sessionID) {
|
|
54
|
+
return `realtime-recordings/${sessionID}`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads a stored recording's bytes back through **authenticated** MJStorage (server-side `GetObject` on
|
|
58
|
+
* the file's own provider/account) — NOT a public pre-signed link. Used to stream a recording to an
|
|
59
|
+
* authorized browser securely. Never throws.
|
|
60
|
+
*
|
|
61
|
+
* @param fileID The `MJ: Files` id of the recording.
|
|
62
|
+
* @returns `{ Bytes, MimeType }` or `null` when the file/account/object can't be resolved.
|
|
63
|
+
*/
|
|
64
|
+
export async function readRealtimeRecordingFile(fileID, contextUser, provider) {
|
|
65
|
+
try {
|
|
66
|
+
const file = await provider.GetEntityObject('MJ: Files', contextUser);
|
|
67
|
+
if (!await file.Load(fileID) || !file.ProviderKey) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// Ensure the engine knows this provider's accounts (idempotent; force-refresh if missing).
|
|
71
|
+
await FileStorageEngine.Instance.Config(false, contextUser, provider);
|
|
72
|
+
let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
73
|
+
if (accounts.length === 0) {
|
|
74
|
+
await FileStorageEngine.Instance.Config(true, contextUser, provider);
|
|
75
|
+
accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
76
|
+
}
|
|
77
|
+
const account = accounts[0];
|
|
78
|
+
if (!account) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const driver = await FileStorageEngine.Instance.GetDriver(account.ID, contextUser);
|
|
82
|
+
const bytes = await driver.GetObject({ fullPath: file.ProviderKey });
|
|
83
|
+
if (!bytes || bytes.length === 0) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { Bytes: bytes, MimeType: file.ContentType ?? 'audio/webm' };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
LogError(`readRealtimeRecordingFile failed (file ${fileID}): ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Writes ONE crash-recovery segment shard (`seg-NNNN.<ext>`) into the session's folder as a RAW
|
|
95
|
+
* storage object — no `MJ: Files` row, no session stamping. Shards are durability insurance during a
|
|
96
|
+
* live call (so a browser/tab death loses at most the last window); they are byte-slices of one
|
|
97
|
+
* continuous stream (only the first carries the container header), so they are NOT individually
|
|
98
|
+
* playable — recovery is "concatenate the folder's shards in order". They are deleted once the
|
|
99
|
+
* canonical consolidated file lands ({@link deleteRealtimeRecordingSegments}). Never throws.
|
|
100
|
+
*
|
|
101
|
+
* @returns `true` on success.
|
|
102
|
+
*/
|
|
103
|
+
export async function writeRealtimeRecordingSegment(input) {
|
|
104
|
+
const { SessionID, SegmentIndex, Audio, MimeType, StorageAccountID, ContextUser } = input;
|
|
105
|
+
try {
|
|
106
|
+
const driver = await FileStorageEngine.Instance.GetDriver(StorageAccountID, ContextUser);
|
|
107
|
+
const name = `seg-${String(SegmentIndex).padStart(4, '0')}.${extensionForMime(MimeType)}`;
|
|
108
|
+
return await driver.PutObject(`${recordingFolder(SessionID)}/${name}`, Audio, MimeType);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
LogError(`writeRealtimeRecordingSegment failed (session ${SessionID}, seg ${SegmentIndex}): ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Writes the capture-time waveform peaks as a `peaks.json` sidecar (a JSON array of numbers) into the
|
|
117
|
+
* session's recording folder via the storage driver. Best-effort and tolerant — a missing/empty peaks
|
|
118
|
+
* array is a no-op, and any storage failure is logged and swallowed (the recording itself already
|
|
119
|
+
* succeeded). Never throws.
|
|
120
|
+
*
|
|
121
|
+
* @returns `true` when a sidecar was written.
|
|
122
|
+
*/
|
|
123
|
+
export async function writeRecordingPeaksSidecar(sessionID, storageAccountID, peaks, contextUser) {
|
|
124
|
+
if (!Array.isArray(peaks) || peaks.length === 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const driver = await FileStorageEngine.Instance.GetDriver(storageAccountID, contextUser);
|
|
129
|
+
const payload = Buffer.from(JSON.stringify(peaks), 'utf8');
|
|
130
|
+
return await driver.PutObject(`${recordingFolder(sessionID)}/peaks.json`, payload, 'application/json');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
LogError(`writeRecordingPeaksSidecar failed (session ${sessionID}): ${error instanceof Error ? error.message : String(error)}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Deletes the `seg-*` shards in a session's folder, leaving the consolidated `recording.*` file. Called
|
|
139
|
+
* after {@link storeRealtimeRecording} writes the canonical file at end of call. Never throws.
|
|
140
|
+
*
|
|
141
|
+
* @returns The number of shards deleted.
|
|
142
|
+
*/
|
|
143
|
+
export async function deleteRealtimeRecordingSegments(sessionID, storageAccountID, contextUser) {
|
|
144
|
+
try {
|
|
145
|
+
const driver = await FileStorageEngine.Instance.GetDriver(storageAccountID, contextUser);
|
|
146
|
+
const folder = recordingFolder(sessionID);
|
|
147
|
+
const listed = await driver.ListObjects(folder);
|
|
148
|
+
let deleted = 0;
|
|
149
|
+
for (const obj of listed.objects ?? []) {
|
|
150
|
+
const base = (obj.name.split('/').pop() ?? obj.name);
|
|
151
|
+
if (base.startsWith('seg-') && await driver.DeleteObject(`${folder}/${base}`)) {
|
|
152
|
+
deleted++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return deleted;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
LogError(`deleteRealtimeRecordingSegments failed (session ${sessionID}): ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Uploads a session recording to MJStorage, links it to the `AIAgentSession` (via
|
|
164
|
+
* `MJ: File Entity Record Links`), and stamps `RecordingFileID` / `RecordingMedia` / `RecordingStartedAt`
|
|
165
|
+
* on the session. Never throws — a recording-storage failure must not fail the session; failures are
|
|
166
|
+
* logged and surfaced as a `null` return.
|
|
167
|
+
*
|
|
168
|
+
* @param input The recording bytes + storage account + session context.
|
|
169
|
+
* @returns The created `MJ: Files` id, or `null` on failure.
|
|
170
|
+
*/
|
|
171
|
+
export async function storeRealtimeRecording(input) {
|
|
172
|
+
const { Audio, MimeType, Media, StartedAt, StorageAccountID, SessionID, ContextUser, Provider, Peaks } = input;
|
|
173
|
+
try {
|
|
174
|
+
// Canonical consolidated file in the session's own folder, alongside (then replacing) its shards.
|
|
175
|
+
const uploaded = await FileStorageEngine.Instance.UploadFile({
|
|
176
|
+
content: Audio,
|
|
177
|
+
fileName: `recording.${extensionForMime(MimeType)}`,
|
|
178
|
+
mimeType: MimeType,
|
|
179
|
+
contextUser: ContextUser,
|
|
180
|
+
storageAccountId: StorageAccountID,
|
|
181
|
+
provider: Provider,
|
|
182
|
+
pathPrefix: recordingFolder(SessionID)
|
|
183
|
+
});
|
|
184
|
+
// Best-effort waveform-peaks sidecar (peaks.json) next to the recording, for fast waveform
|
|
185
|
+
// rendering without re-decoding the audio. A sidecar failure never fails the recording itself.
|
|
186
|
+
await writeRecordingPeaksSidecar(SessionID, StorageAccountID, Peaks, ContextUser);
|
|
187
|
+
// Link the file to the session record so it's discoverable via MJ: File Entity Record Links.
|
|
188
|
+
const sessionEntityID = Provider.EntityByName('MJ: AI Agent Sessions')?.ID;
|
|
189
|
+
if (sessionEntityID) {
|
|
190
|
+
const link = await Provider.GetEntityObject('MJ: File Entity Record Links', ContextUser);
|
|
191
|
+
link.NewRecord();
|
|
192
|
+
link.FileID = uploaded.FileID;
|
|
193
|
+
link.EntityID = sessionEntityID;
|
|
194
|
+
link.RecordID = SessionID;
|
|
195
|
+
if (!await link.Save()) {
|
|
196
|
+
LogError(`storeRealtimeRecording: failed to link recording to session ${SessionID}: ${link.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Stamp the recording fields on the session (file + media kind + t0).
|
|
200
|
+
const session = await Provider.GetEntityObject('MJ: AI Agent Sessions', ContextUser);
|
|
201
|
+
if (await session.Load(SessionID)) {
|
|
202
|
+
session.RecordingFileID = uploaded.FileID;
|
|
203
|
+
session.RecordingMedia = Media;
|
|
204
|
+
session.RecordingStartedAt = StartedAt;
|
|
205
|
+
if (!await session.Save()) {
|
|
206
|
+
LogError(`storeRealtimeRecording: failed to stamp recording fields on session ${SessionID}: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return uploaded.FileID;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
LogError(`storeRealtimeRecording failed for session ${SessionID}: ${error instanceof Error ? error.message : String(error)}`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=realtime-recording-store.js.map
|