@avibra/pulse-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/dist/api/APIClient.d.ts +50 -0
- package/dist/api/APIClient.d.ts.map +1 -0
- package/dist/api/APIClient.js +324 -0
- package/dist/api/APIClient.js.map +1 -0
- package/dist/audio/CallRecorder.d.ts +64 -0
- package/dist/audio/CallRecorder.d.ts.map +1 -0
- package/dist/audio/CallRecorder.js +370 -0
- package/dist/audio/CallRecorder.js.map +1 -0
- package/dist/core/EventTracker.d.ts +21 -0
- package/dist/core/EventTracker.d.ts.map +1 -0
- package/dist/core/EventTracker.js +133 -0
- package/dist/core/EventTracker.js.map +1 -0
- package/dist/core/SessionManager.d.ts +18 -0
- package/dist/core/SessionManager.d.ts.map +1 -0
- package/dist/core/SessionManager.js +70 -0
- package/dist/core/SessionManager.js.map +1 -0
- package/dist/core/TriggerManager.d.ts +15 -0
- package/dist/core/TriggerManager.d.ts.map +1 -0
- package/dist/core/TriggerManager.js +61 -0
- package/dist/core/TriggerManager.js.map +1 -0
- package/dist/core/UserIdentityManager.d.ts +20 -0
- package/dist/core/UserIdentityManager.d.ts.map +1 -0
- package/dist/core/UserIdentityManager.js +71 -0
- package/dist/core/UserIdentityManager.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +64 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +8 -0
- package/dist/platform.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/crypto.d.ts +3 -0
- package/dist/utils/crypto.d.ts.map +1 -0
- package/dist/utils/crypto.js +34 -0
- package/dist/utils/crypto.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/queue.d.ts +21 -0
- package/dist/utils/queue.d.ts.map +1 -0
- package/dist/utils/queue.js +81 -0
- package/dist/utils/queue.js.map +1 -0
- package/dist/utils/retry.d.ts +12 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +66 -0
- package/dist/utils/retry.js.map +1 -0
- package/package.json +25 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// CallRecorder — Timestamp-accurate dual-track audio capture & merge
|
|
3
|
+
//
|
|
4
|
+
// Captures user mic audio and AI (Gemini) audio as timestamped chunks,
|
|
5
|
+
// then merges them into a stereo WAV file at 24 kHz (user=left, AI=right).
|
|
6
|
+
//
|
|
7
|
+
// Why timestamps matter:
|
|
8
|
+
// Concatenating AI chunks end-to-end removes the silence gaps between
|
|
9
|
+
// AI responses, causing the audio to sound rushed/sped-up. Instead,
|
|
10
|
+
// each chunk is placed at its exact timestamp offset in a full-length
|
|
11
|
+
// timeline, preserving natural conversation pacing.
|
|
12
|
+
//
|
|
13
|
+
// Resampling:
|
|
14
|
+
// User mic audio is typically 16 kHz. AI audio is 24 kHz. Both are
|
|
15
|
+
// resampled to 24 kHz (the output rate) using linear interpolation
|
|
16
|
+
// before being placed into the timeline.
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
import { logger } from '../utils/logger';
|
|
19
|
+
/** Output sample rate for the merged WAV — matches Gemini's native 24 kHz. */
|
|
20
|
+
const OUTPUT_RATE = 24000;
|
|
21
|
+
const TAG = '[CallRecorder]';
|
|
22
|
+
export class CallRecorder {
|
|
23
|
+
/**
|
|
24
|
+
* @param userInputRate - Sample rate of the user's mic audio (default 16000).
|
|
25
|
+
* Web worklet outputs at 16 kHz, RN PulseAudio captures at 16 kHz.
|
|
26
|
+
*/
|
|
27
|
+
constructor(userInputRate = 16000) {
|
|
28
|
+
this.startTime = 0;
|
|
29
|
+
this.userChunks = [];
|
|
30
|
+
this.aiChunks = [];
|
|
31
|
+
this.recording = false;
|
|
32
|
+
this.aiChunkCount = 0;
|
|
33
|
+
this.userChunkCount = 0;
|
|
34
|
+
// AI audio cursor — Gemini sends audio chunks in rapid bursts (faster than
|
|
35
|
+
// real-time). We can't use Date.now() for each chunk because they'd all get
|
|
36
|
+
// nearly identical timestamps and overwrite each other in the timeline.
|
|
37
|
+
// Instead, the first chunk of each AI turn anchors to wall-clock time, and
|
|
38
|
+
// subsequent chunks are placed sequentially based on sample duration.
|
|
39
|
+
this.aiCursorMs = 0; // next AI chunk placement position (ms from start)
|
|
40
|
+
this.aiLastArrivalMs = 0; // wall-clock ms of previous AI chunk arrival
|
|
41
|
+
// Same cursor tracking for user mic — mic chunks arrive at real-time pace
|
|
42
|
+
// from the AudioWorklet/PulseAudio (~every 2-20ms), but edge cases like
|
|
43
|
+
// GC pauses or JS thread blocking can cause micro-bursts.
|
|
44
|
+
this.userCursorMs = 0;
|
|
45
|
+
this.userLastArrivalMs = 0;
|
|
46
|
+
this.userInputRate = userInputRate;
|
|
47
|
+
}
|
|
48
|
+
/** Call when the voice session begins. Resets all state. */
|
|
49
|
+
start() {
|
|
50
|
+
this.startTime = Date.now();
|
|
51
|
+
this.userChunks = [];
|
|
52
|
+
this.aiChunks = [];
|
|
53
|
+
this.aiCursorMs = 0;
|
|
54
|
+
this.aiLastArrivalMs = 0;
|
|
55
|
+
this.userCursorMs = 0;
|
|
56
|
+
this.userLastArrivalMs = 0;
|
|
57
|
+
this.aiChunkCount = 0;
|
|
58
|
+
this.userChunkCount = 0;
|
|
59
|
+
this.recording = true;
|
|
60
|
+
logger.info(TAG, 'start() — recording enabled, startTime:', this.startTime);
|
|
61
|
+
}
|
|
62
|
+
/** Whether recording is active. */
|
|
63
|
+
get isRecording() {
|
|
64
|
+
return this.recording;
|
|
65
|
+
}
|
|
66
|
+
// ─── Chunk ingestion ────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Add a user mic audio chunk (Float32 normalized to [-1, 1]).
|
|
69
|
+
* Timestamp is auto-computed from Date.now() - startTime if not provided.
|
|
70
|
+
*/
|
|
71
|
+
addUserChunkF32(samples, timestampMs) {
|
|
72
|
+
if (!this.recording)
|
|
73
|
+
return;
|
|
74
|
+
const ts = this.placeUserChunk(samples.length, this.userInputRate, timestampMs);
|
|
75
|
+
this.userChunks.push({ samples, timestampMs: ts });
|
|
76
|
+
this.userChunkCount++;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Add a user mic audio chunk from base64 PCM (Int16, little-endian).
|
|
80
|
+
* Used by React Native where mic data arrives as base64.
|
|
81
|
+
*/
|
|
82
|
+
addUserChunkB64(base64, timestampMs) {
|
|
83
|
+
if (!this.recording)
|
|
84
|
+
return;
|
|
85
|
+
const samples = pcmBase64ToFloat32(base64);
|
|
86
|
+
const ts = this.placeUserChunk(samples.length, this.userInputRate, timestampMs);
|
|
87
|
+
this.userChunks.push({ samples, timestampMs: ts });
|
|
88
|
+
this.userChunkCount++;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Add an AI audio chunk from Gemini (base64 PCM Int16 @ 24 kHz).
|
|
92
|
+
* Call this from the WebSocket onmessage handler for each inlineData.data chunk.
|
|
93
|
+
*/
|
|
94
|
+
addAIChunkB64(base64, timestampMs) {
|
|
95
|
+
if (!this.recording)
|
|
96
|
+
return;
|
|
97
|
+
const samples = pcmBase64ToFloat32(base64);
|
|
98
|
+
const ts = this.placeAIChunk(samples.length, timestampMs);
|
|
99
|
+
this.aiChunks.push({ samples, timestampMs: ts });
|
|
100
|
+
this.aiChunkCount++;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Add an AI audio chunk as Float32 (already decoded).
|
|
104
|
+
*/
|
|
105
|
+
addAIChunkF32(samples, timestampMs) {
|
|
106
|
+
if (!this.recording)
|
|
107
|
+
return;
|
|
108
|
+
const ts = this.placeAIChunk(samples.length, timestampMs);
|
|
109
|
+
this.aiChunks.push({ samples, timestampMs: ts });
|
|
110
|
+
this.aiChunkCount++;
|
|
111
|
+
}
|
|
112
|
+
placeAIChunk(sampleCount, explicitTs) {
|
|
113
|
+
const arrivalMs = explicitTs ?? (Date.now() - this.startTime);
|
|
114
|
+
const chunkDurationMs = (sampleCount / OUTPUT_RATE) * 1000;
|
|
115
|
+
// New turn: first chunk ever, or significant wall-clock gap since last arrival
|
|
116
|
+
if (this.aiCursorMs === 0 || (arrivalMs - this.aiLastArrivalMs) > CallRecorder.GAP_MS) {
|
|
117
|
+
this.aiCursorMs = arrivalMs;
|
|
118
|
+
}
|
|
119
|
+
const placeAt = this.aiCursorMs;
|
|
120
|
+
this.aiCursorMs += chunkDurationMs;
|
|
121
|
+
this.aiLastArrivalMs = arrivalMs;
|
|
122
|
+
return placeAt;
|
|
123
|
+
}
|
|
124
|
+
placeUserChunk(sampleCount, sampleRate, explicitTs) {
|
|
125
|
+
const arrivalMs = explicitTs ?? (Date.now() - this.startTime);
|
|
126
|
+
const chunkDurationMs = (sampleCount / sampleRate) * 1000;
|
|
127
|
+
if (this.userCursorMs === 0 || (arrivalMs - this.userLastArrivalMs) > CallRecorder.GAP_MS) {
|
|
128
|
+
this.userCursorMs = arrivalMs;
|
|
129
|
+
}
|
|
130
|
+
const placeAt = this.userCursorMs;
|
|
131
|
+
this.userCursorMs += chunkDurationMs;
|
|
132
|
+
this.userLastArrivalMs = arrivalMs;
|
|
133
|
+
return placeAt;
|
|
134
|
+
}
|
|
135
|
+
// ─── Merge & encode ─────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Stop recording and build a stereo WAV (24 kHz, 16-bit PCM).
|
|
138
|
+
* Left channel = user mic, Right channel = AI.
|
|
139
|
+
*
|
|
140
|
+
* Returns ArrayBuffer (works on both Web and RN — Web callers can wrap
|
|
141
|
+
* in a Blob if needed).
|
|
142
|
+
*
|
|
143
|
+
* Returns null if no audio was captured.
|
|
144
|
+
*/
|
|
145
|
+
buildMergedWAV() {
|
|
146
|
+
this.recording = false;
|
|
147
|
+
logger.info(TAG, `buildMergedWAV() — userChunks: ${this.userChunks.length}, aiChunks: ${this.aiChunks.length}, userTotal: ${this.userChunkCount}, aiTotal: ${this.aiChunkCount}`);
|
|
148
|
+
if (this.userChunks.length === 0 && this.aiChunks.length === 0) {
|
|
149
|
+
logger.warn(TAG, 'buildMergedWAV() — NO CHUNKS, returning null');
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// 1. Determine total duration from the last chunk of each track.
|
|
153
|
+
// Duration = last chunk timestamp + last chunk's sample count in ms.
|
|
154
|
+
const userEndMs = trackEndMs(this.userChunks, this.userInputRate);
|
|
155
|
+
const aiEndMs = trackEndMs(this.aiChunks, OUTPUT_RATE);
|
|
156
|
+
const totalMs = Math.max(userEndMs, aiEndMs);
|
|
157
|
+
logger.info(TAG, `buildMergedWAV() — userEndMs: ${userEndMs.toFixed(0)}, aiEndMs: ${aiEndMs.toFixed(0)}, totalMs: ${totalMs.toFixed(0)}`);
|
|
158
|
+
if (totalMs <= 0)
|
|
159
|
+
return null;
|
|
160
|
+
// 2. Allocate full-length silent Float32 tracks at OUTPUT_RATE.
|
|
161
|
+
const totalSamples = Math.ceil((totalMs / 1000) * OUTPUT_RATE);
|
|
162
|
+
const userTrack = new Float32Array(totalSamples);
|
|
163
|
+
const aiTrack = new Float32Array(totalSamples);
|
|
164
|
+
// 3. Place user chunks — resample from userInputRate to OUTPUT_RATE.
|
|
165
|
+
// Resampling ratio: for every 1 input sample, produce (OUTPUT_RATE / userInputRate) output samples.
|
|
166
|
+
const resampleRatio = OUTPUT_RATE / this.userInputRate;
|
|
167
|
+
for (const chunk of this.userChunks) {
|
|
168
|
+
const offsetSamples = Math.floor((chunk.timestampMs / 1000) * OUTPUT_RATE);
|
|
169
|
+
if (resampleRatio === 1) {
|
|
170
|
+
// No resampling needed — same rate
|
|
171
|
+
writeSamples(userTrack, chunk.samples, offsetSamples);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// Linear interpolation resampling
|
|
175
|
+
const resampled = resampleLinear(chunk.samples, resampleRatio);
|
|
176
|
+
writeSamples(userTrack, resampled, offsetSamples);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// 4. Place AI chunks — already at OUTPUT_RATE (24 kHz), no resampling.
|
|
180
|
+
for (const chunk of this.aiChunks) {
|
|
181
|
+
const offsetSamples = Math.floor((chunk.timestampMs / 1000) * OUTPUT_RATE);
|
|
182
|
+
writeSamples(aiTrack, chunk.samples, offsetSamples);
|
|
183
|
+
}
|
|
184
|
+
// 5. Check tracks have actual audio content (not all zeros)
|
|
185
|
+
let userNonZero = 0, aiNonZero = 0;
|
|
186
|
+
for (let i = 0; i < totalSamples; i++) {
|
|
187
|
+
if (userTrack[i] !== 0)
|
|
188
|
+
userNonZero++;
|
|
189
|
+
if (aiTrack[i] !== 0)
|
|
190
|
+
aiNonZero++;
|
|
191
|
+
}
|
|
192
|
+
logger.info(TAG, `buildMergedWAV() — totalSamples: ${totalSamples}, userNonZero: ${userNonZero}, aiNonZero: ${aiNonZero}`);
|
|
193
|
+
if (aiNonZero === 0)
|
|
194
|
+
logger.warn(TAG, 'AI track is ALL ZEROS — no AI audio will be in the WAV!');
|
|
195
|
+
// 6. Encode as stereo WAV — user on left, AI on right.
|
|
196
|
+
const wav = encodeWAV(userTrack, aiTrack, OUTPUT_RATE);
|
|
197
|
+
logger.info(TAG, `buildMergedWAV() — WAV size: ${wav.byteLength} bytes (${(wav.byteLength / 1024 / 1024).toFixed(1)} MB)`);
|
|
198
|
+
return wav;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Convenience: buildMergedWAV() wrapped in a Blob for Web callers.
|
|
202
|
+
* Returns null if no audio was captured.
|
|
203
|
+
*/
|
|
204
|
+
buildMergedBlob() {
|
|
205
|
+
const buf = this.buildMergedWAV();
|
|
206
|
+
if (!buf)
|
|
207
|
+
return null;
|
|
208
|
+
return new Blob([buf], { type: 'audio/wav' });
|
|
209
|
+
}
|
|
210
|
+
/** Free chunk memory (call after upload or if session is discarded). */
|
|
211
|
+
reset() {
|
|
212
|
+
this.userChunks = [];
|
|
213
|
+
this.aiChunks = [];
|
|
214
|
+
this.aiCursorMs = 0;
|
|
215
|
+
this.aiLastArrivalMs = 0;
|
|
216
|
+
this.userCursorMs = 0;
|
|
217
|
+
this.userLastArrivalMs = 0;
|
|
218
|
+
this.recording = false;
|
|
219
|
+
this.startTime = 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ─── Cursor-based timestamp placement ───────────────────────────────────
|
|
223
|
+
// Gemini sends AI audio in rapid bursts (faster than real-time). Multiple
|
|
224
|
+
// chunks arrive within milliseconds but represent sequential audio. If we
|
|
225
|
+
// stamp each with Date.now(), they overlap and overwrite each other.
|
|
226
|
+
//
|
|
227
|
+
// Strategy: the first chunk of a new turn uses wall-clock time as anchor.
|
|
228
|
+
// Subsequent chunks in the same burst are placed sequentially (cursor
|
|
229
|
+
// advances by each chunk's duration). A gap > GAP_MS between arrivals
|
|
230
|
+
// signals a new turn, resetting the anchor to wall-clock time.
|
|
231
|
+
/** Gap threshold — if wall-clock gap between arrivals exceeds this,
|
|
232
|
+
* treat it as a new turn and re-anchor to wall-clock time. */
|
|
233
|
+
CallRecorder.GAP_MS = 800;
|
|
234
|
+
// ─── Pure functions ─────────────────────────────────────────────────────────
|
|
235
|
+
/** Compute the end time (ms) of the last chunk in a track. */
|
|
236
|
+
function trackEndMs(chunks, sampleRate) {
|
|
237
|
+
if (chunks.length === 0)
|
|
238
|
+
return 0;
|
|
239
|
+
const last = chunks[chunks.length - 1];
|
|
240
|
+
const durationMs = (last.samples.length / sampleRate) * 1000;
|
|
241
|
+
return last.timestampMs + durationMs;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Write samples into a target Float32Array at the given offset.
|
|
245
|
+
* If the target position is silent (zero), write directly.
|
|
246
|
+
* If it already has audio (overlapping chunks from interruptions),
|
|
247
|
+
* average the two to avoid destroying either.
|
|
248
|
+
* Bounds-checked to avoid overrun.
|
|
249
|
+
*/
|
|
250
|
+
function writeSamples(target, source, offset) {
|
|
251
|
+
const end = Math.min(source.length, target.length - offset);
|
|
252
|
+
for (let i = 0; i < end; i++) {
|
|
253
|
+
if (target[offset + i] === 0) {
|
|
254
|
+
target[offset + i] = source[i];
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
target[offset + i] = (target[offset + i] + source[i]) / 2;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Resample a Float32 audio buffer using linear interpolation.
|
|
263
|
+
*
|
|
264
|
+
* @param input - Source samples (normalized Float32)
|
|
265
|
+
* @param ratio - Output length / input length (e.g. 1.5 for 16kHz → 24kHz)
|
|
266
|
+
* @returns Resampled Float32Array
|
|
267
|
+
*
|
|
268
|
+
* Linear interpolation avoids the aliasing artifacts of nearest-neighbor
|
|
269
|
+
* (point sampling) while being fast enough for real-time JS execution.
|
|
270
|
+
*/
|
|
271
|
+
function resampleLinear(input, ratio) {
|
|
272
|
+
const outLen = Math.round(input.length * ratio);
|
|
273
|
+
const output = new Float32Array(outLen);
|
|
274
|
+
for (let i = 0; i < outLen; i++) {
|
|
275
|
+
// Map output index back to input position
|
|
276
|
+
const srcPos = i / ratio;
|
|
277
|
+
const srcIdx = Math.floor(srcPos);
|
|
278
|
+
const frac = srcPos - srcIdx;
|
|
279
|
+
const s0 = input[srcIdx] ?? 0;
|
|
280
|
+
const s1 = input[Math.min(srcIdx + 1, input.length - 1)] ?? 0;
|
|
281
|
+
output[i] = s0 + frac * (s1 - s0);
|
|
282
|
+
}
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Decode base64 PCM (Int16 little-endian) → Float32 normalized to [-1, 1].
|
|
287
|
+
*
|
|
288
|
+
* Proper decoding path: base64 → bytes → DataView.getInt16(LE) → /32768.
|
|
289
|
+
* This avoids endianness bugs from direct Int16Array aliasing on big-endian
|
|
290
|
+
* platforms (rare, but correctness matters for audio).
|
|
291
|
+
*/
|
|
292
|
+
function pcmBase64ToFloat32(base64) {
|
|
293
|
+
// Decode base64 to bytes
|
|
294
|
+
const rawLen = base64.length;
|
|
295
|
+
const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
|
|
296
|
+
const byteLen = (rawLen * 3 / 4) - padding;
|
|
297
|
+
const bytes = new Uint8Array(byteLen);
|
|
298
|
+
const lookup = getBase64Lookup();
|
|
299
|
+
let j = 0;
|
|
300
|
+
for (let i = 0; i < rawLen; i += 4) {
|
|
301
|
+
const a = lookup[base64.charCodeAt(i)];
|
|
302
|
+
const b = lookup[base64.charCodeAt(i + 1)];
|
|
303
|
+
const c = i + 2 < rawLen && base64[i + 2] !== '=' ? lookup[base64.charCodeAt(i + 2)] : 0;
|
|
304
|
+
const d = i + 3 < rawLen && base64[i + 3] !== '=' ? lookup[base64.charCodeAt(i + 3)] : 0;
|
|
305
|
+
bytes[j++] = (a << 2) | (b >> 4);
|
|
306
|
+
if (j < byteLen)
|
|
307
|
+
bytes[j++] = ((b & 15) << 4) | (c >> 2);
|
|
308
|
+
if (j < byteLen)
|
|
309
|
+
bytes[j++] = ((c & 3) << 6) | d;
|
|
310
|
+
}
|
|
311
|
+
// Int16 LE → Float32
|
|
312
|
+
const sampleCount = byteLen >> 1;
|
|
313
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
314
|
+
const float32 = new Float32Array(sampleCount);
|
|
315
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
316
|
+
float32[i] = view.getInt16(i * 2, true) / 32768.0;
|
|
317
|
+
}
|
|
318
|
+
return float32;
|
|
319
|
+
}
|
|
320
|
+
/** Encode two Float32 tracks → interleaved stereo 16-bit WAV ArrayBuffer. */
|
|
321
|
+
function encodeWAV(leftTrack, rightTrack, sampleRate) {
|
|
322
|
+
const numSamples = leftTrack.length; // both tracks are the same length
|
|
323
|
+
const dataLen = numSamples * 2 * 2; // 2 channels * 2 bytes per sample
|
|
324
|
+
const buf = new ArrayBuffer(44 + dataLen);
|
|
325
|
+
const view = new DataView(buf);
|
|
326
|
+
const writeStr = (pos, str) => {
|
|
327
|
+
for (let i = 0; i < str.length; i++)
|
|
328
|
+
view.setUint8(pos + i, str.charCodeAt(i));
|
|
329
|
+
};
|
|
330
|
+
// RIFF header
|
|
331
|
+
writeStr(0, 'RIFF');
|
|
332
|
+
view.setUint32(4, 36 + dataLen, true); // file size - 8
|
|
333
|
+
writeStr(8, 'WAVE');
|
|
334
|
+
// fmt subchunk
|
|
335
|
+
writeStr(12, 'fmt ');
|
|
336
|
+
view.setUint32(16, 16, true); // subchunk size (PCM = 16)
|
|
337
|
+
view.setUint16(20, 1, true); // audio format (1 = PCM)
|
|
338
|
+
view.setUint16(22, 2, true); // num channels (stereo)
|
|
339
|
+
view.setUint32(24, sampleRate, true); // sample rate
|
|
340
|
+
view.setUint32(28, sampleRate * 2 * 2, true); // byte rate (sampleRate * channels * bytesPerSample)
|
|
341
|
+
view.setUint16(32, 4, true); // block align (channels * bytesPerSample)
|
|
342
|
+
view.setUint16(34, 16, true); // bits per sample
|
|
343
|
+
// data subchunk
|
|
344
|
+
writeStr(36, 'data');
|
|
345
|
+
view.setUint32(40, dataLen, true);
|
|
346
|
+
// Interleave: left (user), right (AI) — Float32 → Int16
|
|
347
|
+
let offset = 44;
|
|
348
|
+
for (let i = 0; i < numSamples; i++) {
|
|
349
|
+
const l = Math.max(-1, Math.min(1, leftTrack[i]));
|
|
350
|
+
view.setInt16(offset, l < 0 ? l * 0x8000 : l * 0x7FFF, true);
|
|
351
|
+
offset += 2;
|
|
352
|
+
const r = Math.max(-1, Math.min(1, rightTrack[i]));
|
|
353
|
+
view.setInt16(offset, r < 0 ? r * 0x8000 : r * 0x7FFF, true);
|
|
354
|
+
offset += 2;
|
|
355
|
+
}
|
|
356
|
+
return buf;
|
|
357
|
+
}
|
|
358
|
+
// ─── Base64 lookup (lazy singleton) ─────────────────────────────────────────
|
|
359
|
+
const B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
360
|
+
let _b64Lookup = null;
|
|
361
|
+
function getBase64Lookup() {
|
|
362
|
+
if (!_b64Lookup) {
|
|
363
|
+
_b64Lookup = new Uint8Array(128);
|
|
364
|
+
for (let i = 0; i < B64_CHARS.length; i++) {
|
|
365
|
+
_b64Lookup[B64_CHARS.charCodeAt(i)] = i;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return _b64Lookup;
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=CallRecorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CallRecorder.js","sourceRoot":"","sources":["../../src/audio/CallRecorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { APIClient } from '../api/APIClient';
|
|
2
|
+
import type { EventBusAdapter, PlatformInfoAdapter } from '../platform';
|
|
3
|
+
import { IdentifyPayload, PulseEventPayload } from '../types';
|
|
4
|
+
export declare class EventTracker {
|
|
5
|
+
private readonly apiClient;
|
|
6
|
+
private readonly getUserId;
|
|
7
|
+
private readonly getSessionId;
|
|
8
|
+
private readonly getIdentity;
|
|
9
|
+
private readonly partnerId;
|
|
10
|
+
private readonly events;
|
|
11
|
+
private readonly platformInfo;
|
|
12
|
+
private eventTimestamps;
|
|
13
|
+
private lastEventMap;
|
|
14
|
+
private inFlightEvents;
|
|
15
|
+
constructor(apiClient: APIClient, partnerId: string, getUserId: () => string, getSessionId: () => string, getIdentity: () => Readonly<IdentifyPayload> | null, events: EventBusAdapter, platformInfo: PlatformInfoAdapter);
|
|
16
|
+
track(name: string, properties?: PulseEventPayload['properties']): void;
|
|
17
|
+
private enrich;
|
|
18
|
+
private checkRateLimit;
|
|
19
|
+
private checkDebounce;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=EventTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventTracker.d.ts","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,eAAe,EAAqB,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAOjF,qBAAa,YAAY;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyC;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IAEnD,OAAO,CAAC,eAAe,CAAgB;IACvC,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,cAAc,CAA0B;gBAG5C,SAAS,EAAE,SAAS,EACpB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,MAAM,EACvB,YAAY,EAAE,MAAM,MAAM,EAC1B,WAAW,EAAE,MAAM,QAAQ,CAAC,eAAe,CAAC,GAAG,IAAI,EACnD,MAAM,EAAE,eAAe,EACvB,YAAY,EAAE,mBAAmB;IAW9B,KAAK,CACR,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,GAC7C,IAAI;IAoCP,OAAO,CAAC,MAAM;IAiDd,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;CAOxB"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Pulse Core — Event Tracker
|
|
3
|
+
// Uses EventBusAdapter + PlatformInfoAdapter instead of DOM/mitt directly.
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
import { logger } from '../utils/logger';
|
|
6
|
+
const RATE_LIMIT_MAX_EVENTS = 100;
|
|
7
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
8
|
+
const DEBOUNCE_MS = 500;
|
|
9
|
+
export class EventTracker {
|
|
10
|
+
constructor(apiClient, partnerId, getUserId, getSessionId, getIdentity, events, platformInfo) {
|
|
11
|
+
this.eventTimestamps = [];
|
|
12
|
+
this.lastEventMap = new Map();
|
|
13
|
+
this.inFlightEvents = new Set();
|
|
14
|
+
this.apiClient = apiClient;
|
|
15
|
+
this.partnerId = partnerId;
|
|
16
|
+
this.getUserId = getUserId;
|
|
17
|
+
this.getSessionId = getSessionId;
|
|
18
|
+
this.getIdentity = getIdentity;
|
|
19
|
+
this.events = events;
|
|
20
|
+
this.platformInfo = platformInfo;
|
|
21
|
+
}
|
|
22
|
+
track(name, properties) {
|
|
23
|
+
if (!this.checkRateLimit()) {
|
|
24
|
+
logger.warn(`[EventTracker] Rate limit exceeded — dropping event "${name}"`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!this.checkDebounce(name)) {
|
|
28
|
+
logger.debug(`[EventTracker] Debounced duplicate event "${name}"`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (this.inFlightEvents.has(name)) {
|
|
32
|
+
logger.debug(`[EventTracker] In-flight dedup — dropping event "${name}"`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const enriched = this.enrich(name, properties);
|
|
36
|
+
logger.warn(`[EventTracker] Tracking event: ${name}`, JSON.stringify(enriched));
|
|
37
|
+
this.inFlightEvents.add(name);
|
|
38
|
+
void this.apiClient
|
|
39
|
+
.postEvent(enriched)
|
|
40
|
+
.then((response) => {
|
|
41
|
+
logger.warn(`[EventTracker] Track API response for "${name}":`, JSON.stringify(response));
|
|
42
|
+
if (response.success && response.data?.trigger) {
|
|
43
|
+
logger.info(`[EventTracker] Received inline trigger: ${response.data.trigger.trigger_id}`);
|
|
44
|
+
this.events.emit('pulse:trigger-received', { trigger: response.data.trigger });
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
logger.error(`[EventTracker] Failed to post event "${name}":`, err);
|
|
49
|
+
})
|
|
50
|
+
.finally(() => {
|
|
51
|
+
this.inFlightEvents.delete(name);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
enrich(name, properties) {
|
|
55
|
+
const identity = this.getIdentity();
|
|
56
|
+
const payload = {
|
|
57
|
+
partner_id: this.partnerId,
|
|
58
|
+
user_id: this.getUserId(),
|
|
59
|
+
event: {
|
|
60
|
+
name,
|
|
61
|
+
event_time: new Date().toISOString(),
|
|
62
|
+
properties: properties ?? {},
|
|
63
|
+
context: {
|
|
64
|
+
os: this.platformInfo.getOS(),
|
|
65
|
+
location: this.platformInfo.getUrl(),
|
|
66
|
+
session_id: this.getSessionId(),
|
|
67
|
+
device_id: this.platformInfo.getDeviceId(),
|
|
68
|
+
is_webview: this.platformInfo.isWebView(),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
if (identity) {
|
|
73
|
+
const user = {};
|
|
74
|
+
if (identity.anonymous_id !== undefined)
|
|
75
|
+
user.anonymous_id = identity.anonymous_id;
|
|
76
|
+
if (identity.avatar_url !== undefined)
|
|
77
|
+
user.avatar_url = identity.avatar_url;
|
|
78
|
+
if (identity.first_name !== undefined)
|
|
79
|
+
user.first_name = identity.first_name;
|
|
80
|
+
if (identity.last_name !== undefined)
|
|
81
|
+
user.last_name = identity.last_name;
|
|
82
|
+
if (identity.email !== undefined)
|
|
83
|
+
user.email = identity.email;
|
|
84
|
+
if (identity.phone !== undefined)
|
|
85
|
+
user.phone = identity.phone;
|
|
86
|
+
if (identity.dob !== undefined)
|
|
87
|
+
user.dob = identity.dob;
|
|
88
|
+
if (identity.gender !== undefined)
|
|
89
|
+
user.gender = identity.gender;
|
|
90
|
+
if (identity.city !== undefined)
|
|
91
|
+
user.city = identity.city;
|
|
92
|
+
if (identity.zip !== undefined)
|
|
93
|
+
user.zip = identity.zip;
|
|
94
|
+
if (identity.country !== undefined)
|
|
95
|
+
user.country = identity.country;
|
|
96
|
+
if (identity.language !== undefined)
|
|
97
|
+
user.language = identity.language;
|
|
98
|
+
if (identity.locale !== undefined)
|
|
99
|
+
user.locale = identity.locale;
|
|
100
|
+
if (identity.time_zone !== undefined)
|
|
101
|
+
user.time_zone = identity.time_zone;
|
|
102
|
+
if (identity.email_subscribed !== undefined)
|
|
103
|
+
user.email_subscribed = identity.email_subscribed;
|
|
104
|
+
if (identity.sms_subscribed !== undefined)
|
|
105
|
+
user.sms_subscribed = identity.sms_subscribed;
|
|
106
|
+
if (identity.push_subscribed !== undefined)
|
|
107
|
+
user.push_subscribed = identity.push_subscribed;
|
|
108
|
+
if (identity.attributes !== undefined)
|
|
109
|
+
user.attributes = identity.attributes;
|
|
110
|
+
if (Object.keys(user).length > 0) {
|
|
111
|
+
payload.user = user;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return payload;
|
|
115
|
+
}
|
|
116
|
+
checkRateLimit() {
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
this.eventTimestamps = this.eventTimestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
|
|
119
|
+
if (this.eventTimestamps.length >= RATE_LIMIT_MAX_EVENTS)
|
|
120
|
+
return false;
|
|
121
|
+
this.eventTimestamps.push(now);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
checkDebounce(name) {
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
const last = this.lastEventMap.get(name);
|
|
127
|
+
if (last !== undefined && now - last < DEBOUNCE_MS)
|
|
128
|
+
return false;
|
|
129
|
+
this.lastEventMap.set(name, now);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=EventTracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventTracker.js","sourceRoot":"","sources":["../../src/core/EventTracker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StorageAdapter, PlatformInfoAdapter } from '../platform';
|
|
2
|
+
import { SessionData } from '../types';
|
|
3
|
+
export declare class SessionManager {
|
|
4
|
+
private session;
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private readonly platformInfo;
|
|
7
|
+
private unsubscribeForeground;
|
|
8
|
+
constructor(storage: StorageAdapter, platformInfo: PlatformInfoAdapter);
|
|
9
|
+
getSession(): Readonly<SessionData>;
|
|
10
|
+
getSessionId(): string;
|
|
11
|
+
getSessionDurationMs(): number;
|
|
12
|
+
refreshSession(screenName?: string): void;
|
|
13
|
+
destroy(): void;
|
|
14
|
+
private createSession;
|
|
15
|
+
private persist;
|
|
16
|
+
private loadFromStorage;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=SessionManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SessionManager.d.ts","sourceRoot":"","sources":["../../src/core/SessionManager.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAMvC,qBAAa,cAAc;IACvB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,qBAAqB,CAA6B;gBAE9C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,mBAAmB;IAe/D,UAAU,IAAI,QAAQ,CAAC,WAAW,CAAC;IACnC,YAAY,IAAI,MAAM;IACtB,oBAAoB,IAAI,MAAM;IAE9B,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAWzC,OAAO,IAAI,IAAI;IAKtB,OAAO,CAAC,aAAa;YAWP,OAAO;YAMP,eAAe;CAWhC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// Pulse Core — Session Manager
|
|
3
|
+
// Uses StorageAdapter + PlatformInfoAdapter instead of sessionStorage/window.
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
import { generateUUID } from '../utils/crypto';
|
|
6
|
+
import { logger } from '../utils/logger';
|
|
7
|
+
const SESSION_KEY = '__pulse_session__';
|
|
8
|
+
export class SessionManager {
|
|
9
|
+
constructor(storage, platformInfo) {
|
|
10
|
+
this.unsubscribeForeground = null;
|
|
11
|
+
this.storage = storage;
|
|
12
|
+
this.platformInfo = platformInfo;
|
|
13
|
+
this.session = this.createSession();
|
|
14
|
+
void this.loadFromStorage();
|
|
15
|
+
// RN: reset session when app re-foregrounds
|
|
16
|
+
if (platformInfo.onForeground) {
|
|
17
|
+
this.unsubscribeForeground = platformInfo.onForeground(() => {
|
|
18
|
+
logger.debug('[SessionManager] App foregrounded — resetting session');
|
|
19
|
+
this.session = this.createSession();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
getSession() { return this.session; }
|
|
24
|
+
getSessionId() { return this.session.sessionId; }
|
|
25
|
+
getSessionDurationMs() { return Date.now() - this.session.startedAt; }
|
|
26
|
+
refreshSession(screenName) {
|
|
27
|
+
this.session = {
|
|
28
|
+
sessionId: generateUUID(),
|
|
29
|
+
startedAt: Date.now(),
|
|
30
|
+
url: screenName ?? this.platformInfo.getUrl(),
|
|
31
|
+
referrer: this.session.url,
|
|
32
|
+
};
|
|
33
|
+
void this.persist();
|
|
34
|
+
logger.debug('[SessionManager] Session refreshed:', this.session.sessionId);
|
|
35
|
+
}
|
|
36
|
+
destroy() {
|
|
37
|
+
this.unsubscribeForeground?.();
|
|
38
|
+
this.unsubscribeForeground = null;
|
|
39
|
+
}
|
|
40
|
+
createSession() {
|
|
41
|
+
const newSession = {
|
|
42
|
+
sessionId: generateUUID(),
|
|
43
|
+
startedAt: Date.now(),
|
|
44
|
+
url: this.platformInfo.getUrl(),
|
|
45
|
+
referrer: this.platformInfo.getReferrer(),
|
|
46
|
+
};
|
|
47
|
+
logger.debug('[SessionManager] New session:', newSession.sessionId);
|
|
48
|
+
return newSession;
|
|
49
|
+
}
|
|
50
|
+
async persist() {
|
|
51
|
+
try {
|
|
52
|
+
await this.storage.setItem(SESSION_KEY, JSON.stringify(this.session));
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
async loadFromStorage() {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await this.storage.getItem(SESSION_KEY);
|
|
59
|
+
if (!raw)
|
|
60
|
+
return;
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (parsed.sessionId && parsed.startedAt) {
|
|
63
|
+
this.session = parsed;
|
|
64
|
+
logger.debug('[SessionManager] Restored session:', this.session.sessionId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=SessionManager.js.map
|