@absolutejs/voice 0.0.22-beta.463 → 0.0.22-beta.465
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/angular/index.js +5 -662
- package/dist/client/index.js +5 -662
- package/dist/generated/htmxBootstrapBundle.d.ts +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +323 -689
- package/dist/mediaPipelineRoutes.d.ts +55 -1
- package/dist/mediaPipelineSurfaces.d.ts +48 -0
- package/dist/react/index.js +5 -662
- package/dist/svelte/index.js +5 -662
- package/dist/testing/index.js +51 -708
- package/dist/vue/index.js +5 -662
- package/package.json +3 -3
package/dist/testing/index.js
CHANGED
|
@@ -2159,669 +2159,12 @@ var serverMessageToAction = (message) => {
|
|
|
2159
2159
|
}
|
|
2160
2160
|
};
|
|
2161
2161
|
|
|
2162
|
-
// node_modules/@absolutejs/media/dist/index.js
|
|
2163
|
-
var formatLabel = (format) => `${format.container}/${format.encoding}/${String(format.sampleRateHz)}hz/${String(format.channels)}ch`;
|
|
2164
|
-
var formatMatches = (actual, expected) => actual.container === expected.container && actual.encoding === expected.encoding && actual.sampleRateHz === expected.sampleRateHz && actual.channels === expected.channels;
|
|
2165
|
-
var pushIssue = (issues, severity, code, message) => {
|
|
2166
|
-
issues.push({ code, message, severity });
|
|
2167
|
-
};
|
|
2168
|
-
var numericMetadata = (frame, key) => {
|
|
2169
|
-
const value = frame.metadata?.[key];
|
|
2170
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2171
|
-
};
|
|
2172
|
-
var average3 = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
2173
|
-
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
2174
|
-
var min = (values) => values.length === 0 ? undefined : Math.min(...values);
|
|
2175
|
-
var numericStat = (stat, key) => {
|
|
2176
|
-
const value = stat[key];
|
|
2177
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2178
|
-
};
|
|
2179
|
-
var booleanStat = (stat, key) => {
|
|
2180
|
-
const value = stat[key];
|
|
2181
|
-
return typeof value === "boolean" ? value : undefined;
|
|
2182
|
-
};
|
|
2183
|
-
var stringStat = (stat, key) => {
|
|
2184
|
-
const value = stat[key];
|
|
2185
|
-
return typeof value === "string" ? value : undefined;
|
|
2186
|
-
};
|
|
2187
|
-
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
2188
|
-
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
2189
|
-
var DEFAULT_TELEPHONY_FORMAT = {
|
|
2190
|
-
channels: 1,
|
|
2191
|
-
container: "raw",
|
|
2192
|
-
encoding: "mulaw",
|
|
2193
|
-
sampleRateHz: 8000
|
|
2194
|
-
};
|
|
2195
|
-
var bytesToBase64 = (audio) => {
|
|
2196
|
-
const bytes = audio instanceof ArrayBuffer ? new Uint8Array(audio) : new Uint8Array(audio.buffer, audio.byteOffset, audio.byteLength);
|
|
2197
|
-
return Buffer.from(bytes).toString("base64");
|
|
2198
|
-
};
|
|
2199
|
-
var base64ToBytes = (value) => new Uint8Array(Buffer.from(value, "base64"));
|
|
2200
|
-
var unknownRecord = (value) => value && typeof value === "object" ? value : {};
|
|
2201
|
-
var firstString = (records, keys) => {
|
|
2202
|
-
for (const record of records) {
|
|
2203
|
-
for (const key of keys) {
|
|
2204
|
-
const value = record[key];
|
|
2205
|
-
if (typeof value === "string" && value.length > 0) {
|
|
2206
|
-
return value;
|
|
2207
|
-
}
|
|
2208
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2209
|
-
return String(value);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
return;
|
|
2214
|
-
};
|
|
2215
|
-
var firstNumber = (records, keys) => {
|
|
2216
|
-
for (const record of records) {
|
|
2217
|
-
for (const key of keys) {
|
|
2218
|
-
const value = record[key];
|
|
2219
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2220
|
-
return value;
|
|
2221
|
-
}
|
|
2222
|
-
if (typeof value === "string") {
|
|
2223
|
-
const parsed = Number(value);
|
|
2224
|
-
if (Number.isFinite(parsed)) {
|
|
2225
|
-
return parsed;
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
return;
|
|
2231
|
-
};
|
|
2232
|
-
var telephonyDirection = (track) => {
|
|
2233
|
-
const normalized = track?.toLowerCase();
|
|
2234
|
-
if (!normalized) {
|
|
2235
|
-
return "unknown";
|
|
2236
|
-
}
|
|
2237
|
-
if (normalized.includes("inbound") || normalized.includes("caller") || normalized.includes("in")) {
|
|
2238
|
-
return "inbound";
|
|
2239
|
-
}
|
|
2240
|
-
if (normalized.includes("outbound") || normalized.includes("assistant") || normalized.includes("out")) {
|
|
2241
|
-
return "outbound";
|
|
2242
|
-
}
|
|
2243
|
-
return "unknown";
|
|
2244
|
-
};
|
|
2245
|
-
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
2246
|
-
var telephonyEventKind = (envelope) => {
|
|
2247
|
-
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
2248
|
-
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
2249
|
-
if (!normalized) {
|
|
2250
|
-
return "unknown";
|
|
2251
|
-
}
|
|
2252
|
-
if (normalized.includes("connected")) {
|
|
2253
|
-
return "connected";
|
|
2254
|
-
}
|
|
2255
|
-
if (normalized.includes("start")) {
|
|
2256
|
-
return "start";
|
|
2257
|
-
}
|
|
2258
|
-
if (normalized.includes("media")) {
|
|
2259
|
-
return "media";
|
|
2260
|
-
}
|
|
2261
|
-
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
2262
|
-
return "stop";
|
|
2263
|
-
}
|
|
2264
|
-
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
2265
|
-
return "error";
|
|
2266
|
-
}
|
|
2267
|
-
return "unknown";
|
|
2268
|
-
};
|
|
2269
|
-
var normalizeWebRTCStat = (stat) => {
|
|
2270
|
-
const sample = {};
|
|
2271
|
-
for (const [key, value] of Object.entries(stat)) {
|
|
2272
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2273
|
-
sample[key] = value;
|
|
2274
|
-
}
|
|
2275
|
-
}
|
|
2276
|
-
return sample;
|
|
2277
|
-
};
|
|
2278
|
-
var parseTelephonyMediaFrame = (input) => {
|
|
2279
|
-
const envelope = input.envelope;
|
|
2280
|
-
const media = unknownRecord(envelope.media);
|
|
2281
|
-
const payload = firstString([media, envelope], ["payload", "audio", "data"]) ?? firstString([unknownRecord(envelope.message)], ["payload"]);
|
|
2282
|
-
if (!payload) {
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
const carrier = input.carrier ?? firstString([envelope], ["provider"]) ?? "telephony";
|
|
2286
|
-
const streamId = firstString([media, envelope], ["streamSid", "stream_id", "streamId", "streamId", "callSid", "call_id"]);
|
|
2287
|
-
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
2288
|
-
const track = firstString([media, envelope], ["track", "direction"]);
|
|
2289
|
-
const direction = telephonyDirection(track);
|
|
2290
|
-
const timestamp = firstNumber([media, envelope], ["timestamp", "time", "startedAt"]);
|
|
2291
|
-
return {
|
|
2292
|
-
at: timestamp,
|
|
2293
|
-
audio: base64ToBytes(payload),
|
|
2294
|
-
format: input.format ?? DEFAULT_TELEPHONY_FORMAT,
|
|
2295
|
-
id: [
|
|
2296
|
-
carrier,
|
|
2297
|
-
streamId ?? input.sessionId ?? "stream",
|
|
2298
|
-
sequenceNumber ?? timestamp ?? Date.now()
|
|
2299
|
-
].join(":"),
|
|
2300
|
-
kind: telephonyFrameKind(direction),
|
|
2301
|
-
metadata: {
|
|
2302
|
-
carrier,
|
|
2303
|
-
direction,
|
|
2304
|
-
event: firstString([envelope], ["event", "type"]),
|
|
2305
|
-
sequenceNumber,
|
|
2306
|
-
streamId,
|
|
2307
|
-
track
|
|
2308
|
-
},
|
|
2309
|
-
sessionId: input.sessionId ?? streamId,
|
|
2310
|
-
source: "telephony"
|
|
2311
|
-
};
|
|
2312
|
-
};
|
|
2313
|
-
var serializeTelephonyMediaFrame = (input) => {
|
|
2314
|
-
const carrier = input.carrier ?? input.frame.metadata?.carrier ?? "telephony";
|
|
2315
|
-
const streamId = input.streamId ?? (typeof input.frame.metadata?.streamId === "string" ? input.frame.metadata.streamId : input.frame.sessionId);
|
|
2316
|
-
const sequenceNumber = input.sequenceNumber ?? (typeof input.frame.metadata?.sequenceNumber === "string" || typeof input.frame.metadata?.sequenceNumber === "number" ? input.frame.metadata.sequenceNumber : undefined);
|
|
2317
|
-
const direction = input.frame.kind === "assistant-audio" ? "outbound" : "inbound";
|
|
2318
|
-
const payload = input.frame.audio ? bytesToBase64(input.frame.audio) : "";
|
|
2319
|
-
if (carrier === "twilio") {
|
|
2320
|
-
return {
|
|
2321
|
-
event: "media",
|
|
2322
|
-
sequenceNumber,
|
|
2323
|
-
streamSid: streamId,
|
|
2324
|
-
media: {
|
|
2325
|
-
payload,
|
|
2326
|
-
timestamp: input.frame.at,
|
|
2327
|
-
track: direction
|
|
2328
|
-
}
|
|
2329
|
-
};
|
|
2330
|
-
}
|
|
2331
|
-
if (carrier === "telnyx") {
|
|
2332
|
-
return {
|
|
2333
|
-
event: "media",
|
|
2334
|
-
stream_id: streamId,
|
|
2335
|
-
sequence_number: sequenceNumber,
|
|
2336
|
-
media: {
|
|
2337
|
-
payload,
|
|
2338
|
-
timestamp: input.frame.at,
|
|
2339
|
-
track: direction
|
|
2340
|
-
}
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
|
-
if (carrier === "plivo") {
|
|
2344
|
-
return {
|
|
2345
|
-
event: "media",
|
|
2346
|
-
streamId,
|
|
2347
|
-
sequenceNumber,
|
|
2348
|
-
media: {
|
|
2349
|
-
payload,
|
|
2350
|
-
timestamp: input.frame.at,
|
|
2351
|
-
track: direction
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
return {
|
|
2356
|
-
event: "media",
|
|
2357
|
-
provider: carrier,
|
|
2358
|
-
sequenceNumber,
|
|
2359
|
-
streamId,
|
|
2360
|
-
media: {
|
|
2361
|
-
payload,
|
|
2362
|
-
timestamp: input.frame.at,
|
|
2363
|
-
track: direction
|
|
2364
|
-
}
|
|
2365
|
-
};
|
|
2366
|
-
};
|
|
2367
|
-
var createTelephonyMediaSerializer = (input) => {
|
|
2368
|
-
const format = input.format ?? DEFAULT_TELEPHONY_FORMAT;
|
|
2369
|
-
return {
|
|
2370
|
-
carrier: input.carrier,
|
|
2371
|
-
format,
|
|
2372
|
-
parse: (envelope) => parseTelephonyMediaFrame({
|
|
2373
|
-
carrier: input.carrier,
|
|
2374
|
-
envelope,
|
|
2375
|
-
format,
|
|
2376
|
-
sessionId: input.sessionId ?? input.streamId
|
|
2377
|
-
}),
|
|
2378
|
-
serialize: (frame) => serializeTelephonyMediaFrame({
|
|
2379
|
-
carrier: input.carrier,
|
|
2380
|
-
frame,
|
|
2381
|
-
streamId: input.streamId
|
|
2382
|
-
})
|
|
2383
|
-
};
|
|
2384
|
-
};
|
|
2385
|
-
var parseTelephonyStreamEvent = (input) => {
|
|
2386
|
-
const envelope = input.envelope;
|
|
2387
|
-
const media = unknownRecord(envelope.media);
|
|
2388
|
-
const start = unknownRecord(envelope.start);
|
|
2389
|
-
const stop = unknownRecord(envelope.stop);
|
|
2390
|
-
const errorRecord = unknownRecord(envelope.error);
|
|
2391
|
-
const kind = telephonyEventKind(envelope);
|
|
2392
|
-
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
2393
|
-
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
2394
|
-
carrier,
|
|
2395
|
-
envelope,
|
|
2396
|
-
format: input.format,
|
|
2397
|
-
sessionId: input.sessionId
|
|
2398
|
-
}) : undefined;
|
|
2399
|
-
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
2400
|
-
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
2401
|
-
const track = firstString([media, envelope], ["track", "direction"]);
|
|
2402
|
-
return {
|
|
2403
|
-
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
2404
|
-
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
2405
|
-
carrier,
|
|
2406
|
-
direction: telephonyDirection(track),
|
|
2407
|
-
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
2408
|
-
kind,
|
|
2409
|
-
sequenceNumber,
|
|
2410
|
-
streamId
|
|
2411
|
-
};
|
|
2412
|
-
};
|
|
2413
|
-
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
2414
|
-
const envelopes = input.envelopes ?? [];
|
|
2415
|
-
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
2416
|
-
carrier: input.carrier,
|
|
2417
|
-
envelope
|
|
2418
|
-
}));
|
|
2419
|
-
const issues = [];
|
|
2420
|
-
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
2421
|
-
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
2422
|
-
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
2423
|
-
const started = startedIndex >= 0;
|
|
2424
|
-
const stopped = stoppedIndex >= 0;
|
|
2425
|
-
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
2426
|
-
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
2427
|
-
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
2428
|
-
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
2429
|
-
if ((input.requireStart ?? true) && !started) {
|
|
2430
|
-
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
2431
|
-
}
|
|
2432
|
-
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
2433
|
-
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
2434
|
-
}
|
|
2435
|
-
if ((input.requireStop ?? true) && !stopped) {
|
|
2436
|
-
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
2437
|
-
}
|
|
2438
|
-
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
2439
|
-
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
2440
|
-
}
|
|
2441
|
-
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
2442
|
-
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
2443
|
-
}
|
|
2444
|
-
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
2445
|
-
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
2446
|
-
}
|
|
2447
|
-
for (const event of events) {
|
|
2448
|
-
if (event.kind === "error") {
|
|
2449
|
-
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
return {
|
|
2453
|
-
audioBytes,
|
|
2454
|
-
carrier: input.carrier,
|
|
2455
|
-
checkedAt: Date.now(),
|
|
2456
|
-
events,
|
|
2457
|
-
issues,
|
|
2458
|
-
mediaEvents: mediaEvents.length,
|
|
2459
|
-
started,
|
|
2460
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2461
|
-
stopped,
|
|
2462
|
-
streamIds
|
|
2463
|
-
};
|
|
2464
|
-
};
|
|
2465
|
-
var buildMediaResamplingPlan = (input) => {
|
|
2466
|
-
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
2467
|
-
return {
|
|
2468
|
-
inputFormat: input.inputFormat,
|
|
2469
|
-
outputFormat: input.outputFormat,
|
|
2470
|
-
ratio: input.outputFormat.sampleRateHz / input.inputFormat.sampleRateHz,
|
|
2471
|
-
required,
|
|
2472
|
-
status: input.inputFormat.container === input.outputFormat.container && input.inputFormat.encoding === input.outputFormat.encoding && input.inputFormat.channels === input.outputFormat.channels ? "pass" : "warn"
|
|
2473
|
-
};
|
|
2474
|
-
};
|
|
2475
|
-
var speechProbability = (frame) => {
|
|
2476
|
-
if (frame.metadata?.isSpeech === true) {
|
|
2477
|
-
return 1;
|
|
2478
|
-
}
|
|
2479
|
-
if (frame.metadata?.isSpeech === false) {
|
|
2480
|
-
return 0;
|
|
2481
|
-
}
|
|
2482
|
-
for (const key of ["speechProbability", "voiceProbability", "rms", "energy"]) {
|
|
2483
|
-
const value = numericMetadata(frame, key);
|
|
2484
|
-
if (value !== undefined) {
|
|
2485
|
-
return value;
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
return 0;
|
|
2489
|
-
};
|
|
2490
|
-
var buildMediaVadReport = (input = {}) => {
|
|
2491
|
-
const frames = (input.frames ?? []).filter((frame) => frame.kind === "input-audio");
|
|
2492
|
-
const speechStartThreshold = input.speechStartThreshold ?? 0.6;
|
|
2493
|
-
const speechEndThreshold = input.speechEndThreshold ?? 0.35;
|
|
2494
|
-
const minSpeechFrames = input.minSpeechFrames ?? 1;
|
|
2495
|
-
const maxSilenceFrames = input.maxSilenceFrames ?? 1;
|
|
2496
|
-
const segments = [];
|
|
2497
|
-
let activeFrames = [];
|
|
2498
|
-
let silenceFrames = 0;
|
|
2499
|
-
const closeSegment = () => {
|
|
2500
|
-
if (activeFrames.length < minSpeechFrames) {
|
|
2501
|
-
activeFrames = [];
|
|
2502
|
-
silenceFrames = 0;
|
|
2503
|
-
return;
|
|
2504
|
-
}
|
|
2505
|
-
const first = activeFrames[0];
|
|
2506
|
-
const last = activeFrames.at(-1);
|
|
2507
|
-
if (!first) {
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
segments.push({
|
|
2511
|
-
durationMs: first.at !== undefined && last?.at !== undefined ? last.at - first.at + (last.durationMs ?? 0) : undefined,
|
|
2512
|
-
endAt: last?.at !== undefined ? last.at + (last.durationMs ?? 0) : undefined,
|
|
2513
|
-
frameCount: activeFrames.length,
|
|
2514
|
-
segmentId: `vad:${String(segments.length + 1)}`,
|
|
2515
|
-
sessionId: first.sessionId,
|
|
2516
|
-
startAt: first.at,
|
|
2517
|
-
turnId: first.turnId
|
|
2518
|
-
});
|
|
2519
|
-
activeFrames = [];
|
|
2520
|
-
silenceFrames = 0;
|
|
2521
|
-
};
|
|
2522
|
-
for (const frame of frames) {
|
|
2523
|
-
const probability = speechProbability(frame);
|
|
2524
|
-
if (activeFrames.length === 0) {
|
|
2525
|
-
if (probability >= speechStartThreshold) {
|
|
2526
|
-
activeFrames.push(frame);
|
|
2527
|
-
}
|
|
2528
|
-
continue;
|
|
2529
|
-
}
|
|
2530
|
-
activeFrames.push(frame);
|
|
2531
|
-
if (probability <= speechEndThreshold) {
|
|
2532
|
-
silenceFrames += 1;
|
|
2533
|
-
} else {
|
|
2534
|
-
silenceFrames = 0;
|
|
2535
|
-
}
|
|
2536
|
-
if (silenceFrames > maxSilenceFrames) {
|
|
2537
|
-
closeSegment();
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
closeSegment();
|
|
2541
|
-
return {
|
|
2542
|
-
checkedAt: Date.now(),
|
|
2543
|
-
inputAudioFrames: frames.length,
|
|
2544
|
-
segments,
|
|
2545
|
-
status: frames.length === 0 ? "warn" : "pass"
|
|
2546
|
-
};
|
|
2547
|
-
};
|
|
2548
|
-
var buildMediaInterruptionReport = (input = {}) => {
|
|
2549
|
-
const issues = [];
|
|
2550
|
-
const interruptionFrames = (input.frames ?? []).filter((frame) => frame.kind === "interruption");
|
|
2551
|
-
const latenciesMs = interruptionFrames.map((frame) => frame.latencyMs).filter((latency) => typeof latency === "number");
|
|
2552
|
-
const maxInterruptionLatencyMs = input.maxInterruptionLatencyMs;
|
|
2553
|
-
if (interruptionFrames.length === 0) {
|
|
2554
|
-
pushIssue(issues, "warning", "media.interruption_missing", "No interruption frame was observed.");
|
|
2555
|
-
}
|
|
2556
|
-
if (maxInterruptionLatencyMs !== undefined && latenciesMs.some((latency) => latency > maxInterruptionLatencyMs)) {
|
|
2557
|
-
pushIssue(issues, "error", "media.interruption_latency", `Interruption latency exceeded ${String(maxInterruptionLatencyMs)}ms.`);
|
|
2558
|
-
}
|
|
2559
|
-
return {
|
|
2560
|
-
checkedAt: Date.now(),
|
|
2561
|
-
interruptionFrames: interruptionFrames.length,
|
|
2562
|
-
issues,
|
|
2563
|
-
latenciesMs,
|
|
2564
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass"
|
|
2565
|
-
};
|
|
2566
|
-
};
|
|
2567
|
-
var buildMediaQualityReport = (input = {}) => {
|
|
2568
|
-
const frames = [...input.frames ?? []].sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
2569
|
-
const audioFrames = frames.filter((frame) => frame.kind === "input-audio" || frame.kind === "assistant-audio");
|
|
2570
|
-
const inputAudioFrames = frames.filter((frame) => frame.kind === "input-audio");
|
|
2571
|
-
const assistantAudioFrames = frames.filter((frame) => frame.kind === "assistant-audio");
|
|
2572
|
-
const issues = [];
|
|
2573
|
-
const gapsMs = [];
|
|
2574
|
-
for (const [index, frame] of audioFrames.entries()) {
|
|
2575
|
-
const previous = audioFrames[index - 1];
|
|
2576
|
-
if (previous?.at === undefined || frame.at === undefined || previous.durationMs === undefined) {
|
|
2577
|
-
continue;
|
|
2578
|
-
}
|
|
2579
|
-
const gap = frame.at - (previous.at + previous.durationMs);
|
|
2580
|
-
if (gap > 0) {
|
|
2581
|
-
gapsMs.push(gap);
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
const jitterMs = audioFrames.map((frame) => numericMetadata(frame, "jitterMs")).filter((value) => value !== undefined).at(-1) ?? max(gapsMs);
|
|
2585
|
-
const first = audioFrames.find((frame) => frame.at !== undefined);
|
|
2586
|
-
const last = audioFrames.toReversed().find((frame) => frame.at !== undefined);
|
|
2587
|
-
const durationMs = first?.at !== undefined && last?.at !== undefined ? last.at - first.at + (last.durationMs ?? 0) : undefined;
|
|
2588
|
-
const expectedDurationMs = audioFrames.length > 0 ? audioFrames.reduce((total, frame) => total + (frame.durationMs ?? 0), 0) : undefined;
|
|
2589
|
-
const timestampDriftMs = durationMs !== undefined && expectedDurationMs !== undefined ? Math.max(0, durationMs - expectedDurationMs) : undefined;
|
|
2590
|
-
const speechScores = inputAudioFrames.map(speechProbability);
|
|
2591
|
-
const speechFrames = speechScores.filter((score) => score >= 0.6).length;
|
|
2592
|
-
const silenceFrames = speechScores.filter((score) => score <= 0.35).length;
|
|
2593
|
-
const unknownSpeechFrames = Math.max(0, inputAudioFrames.length - speechFrames - silenceFrames);
|
|
2594
|
-
const speechRatio = inputAudioFrames.length === 0 ? 0 : speechFrames / inputAudioFrames.length;
|
|
2595
|
-
const silenceRatio = inputAudioFrames.length === 0 ? 0 : silenceFrames / inputAudioFrames.length;
|
|
2596
|
-
const levels = audioFrames.map((frame) => numericMetadata(frame, "level") ?? numericMetadata(frame, "rms") ?? numericMetadata(frame, "energy")).filter((value) => value !== undefined);
|
|
2597
|
-
const backpressureEvents = input.transport?.backpressureEvents ?? 0;
|
|
2598
|
-
const maxGapMs = input.maxGapMs;
|
|
2599
|
-
if (maxGapMs !== undefined && gapsMs.some((gap) => gap > maxGapMs)) {
|
|
2600
|
-
pushIssue(issues, "warning", "media.quality_gap", `Observed media gap above ${String(maxGapMs)}ms.`);
|
|
2601
|
-
}
|
|
2602
|
-
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2603
|
-
pushIssue(issues, "warning", "media.quality_jitter", `Observed jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
2604
|
-
}
|
|
2605
|
-
if (input.maxTimestampDriftMs !== undefined && timestampDriftMs !== undefined && timestampDriftMs > input.maxTimestampDriftMs) {
|
|
2606
|
-
pushIssue(issues, "warning", "media.quality_timestamp_drift", `Observed timestamp drift ${String(timestampDriftMs)}ms above ${String(input.maxTimestampDriftMs)}ms.`);
|
|
2607
|
-
}
|
|
2608
|
-
if (input.minSpeechRatio !== undefined && inputAudioFrames.length > 0 && speechRatio < input.minSpeechRatio) {
|
|
2609
|
-
pushIssue(issues, "warning", "media.quality_speech_ratio", `Observed speech ratio ${String(speechRatio)} below ${String(input.minSpeechRatio)}.`);
|
|
2610
|
-
}
|
|
2611
|
-
if (input.maxBackpressureEvents !== undefined && backpressureEvents > input.maxBackpressureEvents) {
|
|
2612
|
-
pushIssue(issues, "warning", "media.quality_backpressure", `Observed ${String(backpressureEvents)} backpressure event(s), above ${String(input.maxBackpressureEvents)}.`);
|
|
2613
|
-
}
|
|
2614
|
-
return {
|
|
2615
|
-
assistantAudioFrames: assistantAudioFrames.length,
|
|
2616
|
-
backpressureEvents,
|
|
2617
|
-
checkedAt: Date.now(),
|
|
2618
|
-
durationMs,
|
|
2619
|
-
gapCount: gapsMs.length,
|
|
2620
|
-
gapsMs,
|
|
2621
|
-
inputAudioFrames: inputAudioFrames.length,
|
|
2622
|
-
issues,
|
|
2623
|
-
jitterMs,
|
|
2624
|
-
levelAverage: average3(levels),
|
|
2625
|
-
levelMax: max(levels),
|
|
2626
|
-
levelMin: min(levels),
|
|
2627
|
-
silenceFrames,
|
|
2628
|
-
silenceRatio,
|
|
2629
|
-
speechFrames,
|
|
2630
|
-
speechRatio,
|
|
2631
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2632
|
-
timestampDriftMs,
|
|
2633
|
-
totalFrames: frames.length,
|
|
2634
|
-
unknownSpeechFrames
|
|
2635
|
-
};
|
|
2636
|
-
};
|
|
2637
|
-
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
2638
|
-
const stats = input.stats ?? [];
|
|
2639
|
-
const issues = [];
|
|
2640
|
-
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
2641
|
-
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
2642
|
-
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
2643
|
-
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
2644
|
-
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
2645
|
-
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
2646
|
-
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
2647
|
-
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
2648
|
-
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
2649
|
-
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
2650
|
-
const packetLossDenominator = inboundPackets + packetsLost;
|
|
2651
|
-
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
2652
|
-
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
2653
|
-
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
2654
|
-
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
2655
|
-
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
2656
|
-
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
2657
|
-
const delay = numericStat(stat, "jitterBufferDelay");
|
|
2658
|
-
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
2659
|
-
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
2660
|
-
}).filter((value) => value !== undefined));
|
|
2661
|
-
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
2662
|
-
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
2663
|
-
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
2664
|
-
}
|
|
2665
|
-
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
2666
|
-
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
2667
|
-
}
|
|
2668
|
-
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
2669
|
-
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
2670
|
-
}
|
|
2671
|
-
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
2672
|
-
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
2673
|
-
}
|
|
2674
|
-
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2675
|
-
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
2676
|
-
}
|
|
2677
|
-
return {
|
|
2678
|
-
activeCandidatePairs,
|
|
2679
|
-
audioLevelAverage: average3(audioLevels),
|
|
2680
|
-
bytesReceived,
|
|
2681
|
-
bytesSent,
|
|
2682
|
-
checkedAt: Date.now(),
|
|
2683
|
-
endedAudioTracks,
|
|
2684
|
-
inboundPackets,
|
|
2685
|
-
issues,
|
|
2686
|
-
jitterBufferDelayMs,
|
|
2687
|
-
jitterMs,
|
|
2688
|
-
liveAudioTracks,
|
|
2689
|
-
outboundPackets,
|
|
2690
|
-
packetLossRatio,
|
|
2691
|
-
packetsLost,
|
|
2692
|
-
roundTripTimeMs,
|
|
2693
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2694
|
-
totalStats: stats.length
|
|
2695
|
-
};
|
|
2696
|
-
};
|
|
2697
|
-
var collectMediaWebRTCStats = async (input) => {
|
|
2698
|
-
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
2699
|
-
return [...report.values()].map(normalizeWebRTCStat);
|
|
2700
|
-
};
|
|
2701
|
-
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
2702
|
-
const stats = input.stats ?? [];
|
|
2703
|
-
const previousStats = input.previousStats ?? [];
|
|
2704
|
-
const issues = [];
|
|
2705
|
-
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
2706
|
-
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
2707
|
-
const streams = audioRtp.map((stat) => {
|
|
2708
|
-
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
2709
|
-
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
2710
|
-
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
2711
|
-
const previous = previousByKey.get(statKey(stat));
|
|
2712
|
-
const currentPackets = numericStat(stat, packetsKey);
|
|
2713
|
-
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
2714
|
-
const currentBytes = numericStat(stat, bytesKey);
|
|
2715
|
-
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
2716
|
-
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
2717
|
-
return {
|
|
2718
|
-
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
2719
|
-
currentPackets,
|
|
2720
|
-
direction,
|
|
2721
|
-
id: statKey(stat),
|
|
2722
|
-
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
2723
|
-
previousPackets,
|
|
2724
|
-
timeDeltaMs
|
|
2725
|
-
};
|
|
2726
|
-
});
|
|
2727
|
-
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
2728
|
-
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
2729
|
-
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
2730
|
-
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
2731
|
-
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
2732
|
-
if (input.requireInboundAudio && inbound.length === 0) {
|
|
2733
|
-
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
2734
|
-
}
|
|
2735
|
-
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
2736
|
-
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
2737
|
-
}
|
|
2738
|
-
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
2739
|
-
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
2740
|
-
}
|
|
2741
|
-
if (stalledInboundStreams > 0) {
|
|
2742
|
-
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
2743
|
-
}
|
|
2744
|
-
if (stalledOutboundStreams > 0) {
|
|
2745
|
-
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
2746
|
-
}
|
|
2747
|
-
return {
|
|
2748
|
-
checkedAt: Date.now(),
|
|
2749
|
-
inboundAudioStreams: inbound.length,
|
|
2750
|
-
issues,
|
|
2751
|
-
maxObservedGapMs,
|
|
2752
|
-
outboundAudioStreams: outbound.length,
|
|
2753
|
-
stalledInboundStreams,
|
|
2754
|
-
stalledOutboundStreams,
|
|
2755
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2756
|
-
streams,
|
|
2757
|
-
totalStats: stats.length
|
|
2758
|
-
};
|
|
2759
|
-
};
|
|
2760
|
-
var buildMediaPipelineCalibrationReport = (input = {}) => {
|
|
2761
|
-
const frames = input.frames ?? [];
|
|
2762
|
-
const issues = [];
|
|
2763
|
-
const inputFrames = frames.filter((frame) => frame.kind === "input-audio");
|
|
2764
|
-
const assistantFrames = frames.filter((frame) => frame.kind === "assistant-audio");
|
|
2765
|
-
const turnCommitFrames = frames.filter((frame) => frame.kind === "turn-commit");
|
|
2766
|
-
const interruptionFrameRecords = frames.filter((frame) => frame.kind === "interruption");
|
|
2767
|
-
const traceLinkedFrames = frames.filter((frame) => frame.traceEventId).length;
|
|
2768
|
-
const backpressureFrames = frames.filter((frame) => Boolean(frame.metadata?.backpressure)).length;
|
|
2769
|
-
const audioLatencies = assistantFrames.map((frame) => frame.latencyMs).filter((latency) => typeof latency === "number");
|
|
2770
|
-
const firstAudioLatencyMs = audioLatencies.length > 0 ? Math.min(...audioLatencies) : undefined;
|
|
2771
|
-
const jitterValues = frames.map((frame) => numericMetadata(frame, "jitterMs")).filter((value) => value !== undefined);
|
|
2772
|
-
const jitterMs = jitterValues.length > 0 ? Math.max(...jitterValues) : undefined;
|
|
2773
|
-
const inputFormat = input.inputFormat ?? inputFrames.find((frame) => frame.format)?.format;
|
|
2774
|
-
const outputFormat = input.outputFormat ?? assistantFrames.find((frame) => frame.format)?.format;
|
|
2775
|
-
const resamplingRequired = Boolean(input.expectedInputFormat && inputFormat && inputFormat.sampleRateHz !== input.expectedInputFormat.sampleRateHz) || Boolean(input.expectedOutputFormat && outputFormat && outputFormat.sampleRateHz !== input.expectedOutputFormat.sampleRateHz);
|
|
2776
|
-
const resamplingTargetHz = resamplingRequired && input.expectedInputFormat ? input.expectedInputFormat.sampleRateHz : resamplingRequired ? input.expectedOutputFormat?.sampleRateHz : undefined;
|
|
2777
|
-
if (inputFrames.length === 0) {
|
|
2778
|
-
pushIssue(issues, "warning", "media.input_audio_missing", "No input audio frames were observed.");
|
|
2779
|
-
}
|
|
2780
|
-
if (assistantFrames.length === 0) {
|
|
2781
|
-
pushIssue(issues, "warning", "media.assistant_audio_missing", "No assistant audio frames were observed.");
|
|
2782
|
-
}
|
|
2783
|
-
if (input.expectedInputFormat && inputFormat && !formatMatches(inputFormat, input.expectedInputFormat)) {
|
|
2784
|
-
pushIssue(issues, inputFormat.sampleRateHz === input.expectedInputFormat.sampleRateHz ? "warning" : "error", "media.input_format_mismatch", `Input format ${formatLabel(inputFormat)} does not match expected ${formatLabel(input.expectedInputFormat)}.`);
|
|
2785
|
-
}
|
|
2786
|
-
if (input.expectedOutputFormat && outputFormat && !formatMatches(outputFormat, input.expectedOutputFormat)) {
|
|
2787
|
-
pushIssue(issues, outputFormat.sampleRateHz === input.expectedOutputFormat.sampleRateHz ? "warning" : "error", "media.output_format_mismatch", `Output format ${formatLabel(outputFormat)} does not match expected ${formatLabel(input.expectedOutputFormat)}.`);
|
|
2788
|
-
}
|
|
2789
|
-
if (firstAudioLatencyMs !== undefined && input.maxFirstAudioLatencyMs !== undefined && firstAudioLatencyMs > input.maxFirstAudioLatencyMs) {
|
|
2790
|
-
pushIssue(issues, "error", "media.first_audio_latency", `First audio latency ${String(firstAudioLatencyMs)}ms exceeds budget ${String(input.maxFirstAudioLatencyMs)}ms.`);
|
|
2791
|
-
}
|
|
2792
|
-
if (jitterMs !== undefined && input.maxJitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2793
|
-
pushIssue(issues, "warning", "media.jitter", `Media jitter ${String(jitterMs)}ms exceeds budget ${String(input.maxJitterMs)}ms.`);
|
|
2794
|
-
}
|
|
2795
|
-
if (input.maxBackpressureFrames !== undefined && backpressureFrames > input.maxBackpressureFrames) {
|
|
2796
|
-
pushIssue(issues, "warning", "media.backpressure", `Backpressure frame count ${String(backpressureFrames)} exceeds budget ${String(input.maxBackpressureFrames)}.`);
|
|
2797
|
-
}
|
|
2798
|
-
if (input.requireInterruptionFrame && interruptionFrameRecords.length === 0) {
|
|
2799
|
-
pushIssue(issues, "warning", "media.interruption_missing", "No interruption frame was observed.");
|
|
2800
|
-
}
|
|
2801
|
-
if (input.requireTraceEvidence && traceLinkedFrames === 0) {
|
|
2802
|
-
pushIssue(issues, "warning", "media.trace_evidence_missing", "No media frames were linked to trace evidence.");
|
|
2803
|
-
}
|
|
2804
|
-
return {
|
|
2805
|
-
assistantAudioFrames: assistantFrames.length,
|
|
2806
|
-
backpressureFrames,
|
|
2807
|
-
checkedAt: Date.now(),
|
|
2808
|
-
firstAudioLatencyMs,
|
|
2809
|
-
inputAudioFrames: inputFrames.length,
|
|
2810
|
-
inputFormat,
|
|
2811
|
-
interruptionFrames: interruptionFrameRecords.length,
|
|
2812
|
-
issues,
|
|
2813
|
-
jitterMs,
|
|
2814
|
-
outputFormat,
|
|
2815
|
-
resamplingRequired,
|
|
2816
|
-
resamplingTargetHz,
|
|
2817
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2818
|
-
surface: input.surface ?? "media-pipeline",
|
|
2819
|
-
traceLinkedFrames,
|
|
2820
|
-
turnCommitFrames: turnCommitFrames.length
|
|
2821
|
-
};
|
|
2822
|
-
};
|
|
2823
|
-
|
|
2824
2162
|
// src/client/browserMedia.ts
|
|
2163
|
+
import {
|
|
2164
|
+
buildMediaWebRTCStatsReport,
|
|
2165
|
+
buildMediaWebRTCStreamContinuityReport,
|
|
2166
|
+
collectMediaWebRTCStats
|
|
2167
|
+
} from "@absolutejs/media";
|
|
2825
2168
|
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
2826
2169
|
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
2827
2170
|
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
@@ -8589,16 +7932,16 @@ var renderVoiceCallReviewHTML = (artifact) => {
|
|
|
8589
7932
|
</html>`;
|
|
8590
7933
|
};
|
|
8591
7934
|
// src/testing/sessionBenchmark.ts
|
|
8592
|
-
var
|
|
7935
|
+
var average3 = (values) => values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
8593
7936
|
var normalizeTurnText = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu, " ").replace(/\s+/g, " ").trim();
|
|
8594
7937
|
var countPassedTurns = (turnResults) => turnResults.reduce((count, result) => count + (result.passes ? 1 : 0), 0);
|
|
8595
7938
|
var calculateTurnPassRate = (turnResults) => turnResults.length > 0 ? countPassedTurns(turnResults) / turnResults.length : 0;
|
|
8596
7939
|
var summarizeScenarioCosts = (turnResults) => {
|
|
8597
7940
|
const costEstimates = turnResults.map((turn) => turn.quality?.cost).filter((value) => value !== undefined);
|
|
8598
7941
|
return {
|
|
8599
|
-
averageRelativeCostUnits: roundMetric5(
|
|
8600
|
-
fallbackReplayAudioMs: roundMetric5(
|
|
8601
|
-
primaryAudioMs: roundMetric5(
|
|
7942
|
+
averageRelativeCostUnits: roundMetric5(average3(costEstimates.map((estimate) => estimate.estimatedRelativeCostUnits))),
|
|
7943
|
+
fallbackReplayAudioMs: roundMetric5(average3(costEstimates.map((estimate) => estimate.fallbackReplayAudioMs)), 2),
|
|
7944
|
+
primaryAudioMs: roundMetric5(average3(costEstimates.map((estimate) => estimate.primaryAudioMs)), 2)
|
|
8602
7945
|
};
|
|
8603
7946
|
};
|
|
8604
7947
|
var roundMetric5 = (value, digits = 4) => {
|
|
@@ -8917,13 +8260,13 @@ var summarizeVoiceSessionBenchmark = (adapterId, scenarios) => {
|
|
|
8917
8260
|
const turnAccuracies = scenarios.flatMap((scenario) => scenario.turnResults.map((turn) => turn.accuracy?.wordErrorRate).filter((value) => typeof value === "number"));
|
|
8918
8261
|
return {
|
|
8919
8262
|
adapterId,
|
|
8920
|
-
averageElapsedMs: roundMetric5(
|
|
8921
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
8922
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
8923
|
-
averageReconnectCount: roundMetric5(
|
|
8924
|
-
averageRelativeCostUnits: roundMetric5(
|
|
8925
|
-
averageTurnPassRate: roundMetric5(
|
|
8926
|
-
averageWordErrorRate: roundMetric5(
|
|
8263
|
+
averageElapsedMs: roundMetric5(average3(scenarios.map((scenario) => scenario.elapsedMs)), 2),
|
|
8264
|
+
averageFallbackReplayAudioMs: roundMetric5(average3(scenarios.map((scenario) => scenario.fallbackReplayAudioMs)), 2),
|
|
8265
|
+
averagePrimaryAudioMs: roundMetric5(average3(scenarios.map((scenario) => scenario.primaryAudioMs)), 2),
|
|
8266
|
+
averageReconnectCount: roundMetric5(average3(scenarios.map((scenario) => scenario.reconnectCount))),
|
|
8267
|
+
averageRelativeCostUnits: roundMetric5(average3(scenarios.map((scenario) => scenario.averageRelativeCostUnits))),
|
|
8268
|
+
averageTurnPassRate: roundMetric5(average3(scenarios.map((scenario) => scenario.turnPassRate))),
|
|
8269
|
+
averageWordErrorRate: roundMetric5(average3(turnAccuracies)),
|
|
8927
8270
|
duplicateTurnRate: roundMetric5(scenarios.length > 0 ? scenarios.filter((scenario) => scenario.duplicateTurnCount > 0).length / scenarios.length : 0),
|
|
8928
8271
|
passCount,
|
|
8929
8272
|
passRate: roundMetric5(scenarios.length > 0 ? passCount / scenarios.length : 0),
|
|
@@ -8949,13 +8292,13 @@ var summarizeVoiceSessionBenchmarkSeries = (input) => {
|
|
|
8949
8292
|
const passCount = results.filter((scenario) => scenario.passes).length;
|
|
8950
8293
|
const sample = results[0];
|
|
8951
8294
|
return {
|
|
8952
|
-
averageElapsedMs: roundMetric5(
|
|
8953
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
8954
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
8955
|
-
averageReconnectCount: roundMetric5(
|
|
8956
|
-
averageRelativeCostUnits: roundMetric5(
|
|
8957
|
-
averageTurnPassRate: roundMetric5(
|
|
8958
|
-
averageWordErrorRate: roundMetric5(
|
|
8295
|
+
averageElapsedMs: roundMetric5(average3(results.map((scenario) => scenario.elapsedMs)), 2),
|
|
8296
|
+
averageFallbackReplayAudioMs: roundMetric5(average3(results.map((scenario) => scenario.fallbackReplayAudioMs)), 2),
|
|
8297
|
+
averagePrimaryAudioMs: roundMetric5(average3(results.map((scenario) => scenario.primaryAudioMs)), 2),
|
|
8298
|
+
averageReconnectCount: roundMetric5(average3(results.map((scenario) => scenario.reconnectCount))),
|
|
8299
|
+
averageRelativeCostUnits: roundMetric5(average3(results.map((scenario) => scenario.averageRelativeCostUnits))),
|
|
8300
|
+
averageTurnPassRate: roundMetric5(average3(results.map((scenario) => scenario.turnPassRate))),
|
|
8301
|
+
averageWordErrorRate: roundMetric5(average3(wordErrorRates)),
|
|
8959
8302
|
bestWordErrorRate: roundMetric5(wordErrorRates.length > 0 ? Math.min(...wordErrorRates) : 0),
|
|
8960
8303
|
fixtureId,
|
|
8961
8304
|
passCount,
|
|
@@ -8978,18 +8321,18 @@ var summarizeVoiceSessionBenchmarkSeries = (input) => {
|
|
|
8978
8321
|
scenarios: scenarioAggregates,
|
|
8979
8322
|
summary: {
|
|
8980
8323
|
adapterId: input.adapterId,
|
|
8981
|
-
averageElapsedMs: roundMetric5(
|
|
8982
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
8983
|
-
averagePassRate: roundMetric5(
|
|
8984
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
8985
|
-
averageReconnectCount: roundMetric5(
|
|
8986
|
-
averageRelativeCostUnits: roundMetric5(
|
|
8987
|
-
averageTurnPassRate: roundMetric5(
|
|
8988
|
-
averageWordErrorRate: roundMetric5(
|
|
8324
|
+
averageElapsedMs: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageElapsedMs)), 2),
|
|
8325
|
+
averageFallbackReplayAudioMs: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageFallbackReplayAudioMs)), 2),
|
|
8326
|
+
averagePassRate: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.passRate))),
|
|
8327
|
+
averagePrimaryAudioMs: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averagePrimaryAudioMs)), 2),
|
|
8328
|
+
averageReconnectCount: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageReconnectCount))),
|
|
8329
|
+
averageRelativeCostUnits: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageRelativeCostUnits))),
|
|
8330
|
+
averageTurnPassRate: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageTurnPassRate))),
|
|
8331
|
+
averageWordErrorRate: roundMetric5(average3(scenarioAggregates.map((scenario) => scenario.averageWordErrorRate))),
|
|
8989
8332
|
flakyScenarioCount: scenarioAggregates.filter((scenario) => scenario.passRate > 0 && scenario.passRate < 1).length,
|
|
8990
8333
|
generatedRunCount: input.reports.length,
|
|
8991
|
-
reconnectCoverageRate: roundMetric5(
|
|
8992
|
-
reconnectSuccessRate: roundMetric5(
|
|
8334
|
+
reconnectCoverageRate: roundMetric5(average3(reconnectCoverageRates)),
|
|
8335
|
+
reconnectSuccessRate: roundMetric5(average3(reconnectRates)),
|
|
8993
8336
|
scenarioCount: scenarioAggregates.length,
|
|
8994
8337
|
stableScenarioCount: scenarioAggregates.filter((scenario) => scenario.passRate === 1).length,
|
|
8995
8338
|
totalPassCount,
|
|
@@ -10663,7 +10006,7 @@ import { Elysia as Elysia3 } from "elysia";
|
|
|
10663
10006
|
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10664
10007
|
var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
10665
10008
|
var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10666
|
-
var
|
|
10009
|
+
var firstString = (payload, keys) => {
|
|
10667
10010
|
for (const key of keys) {
|
|
10668
10011
|
const value = getString3(payload[key]);
|
|
10669
10012
|
if (value) {
|
|
@@ -10672,7 +10015,7 @@ var firstString2 = (payload, keys) => {
|
|
|
10672
10015
|
}
|
|
10673
10016
|
return;
|
|
10674
10017
|
};
|
|
10675
|
-
var
|
|
10018
|
+
var firstNumber = (payload, keys) => {
|
|
10676
10019
|
for (const key of keys) {
|
|
10677
10020
|
const value = getNumber2(payload[key]);
|
|
10678
10021
|
if (value !== undefined) {
|
|
@@ -10681,20 +10024,20 @@ var firstNumber2 = (payload, keys) => {
|
|
|
10681
10024
|
}
|
|
10682
10025
|
return;
|
|
10683
10026
|
};
|
|
10684
|
-
var eventProvider = (event) =>
|
|
10027
|
+
var eventProvider = (event) => firstString(event.payload, [
|
|
10685
10028
|
"provider",
|
|
10686
10029
|
"selectedProvider",
|
|
10687
10030
|
"fallbackProvider",
|
|
10688
10031
|
"variantId"
|
|
10689
10032
|
]);
|
|
10690
|
-
var eventStatus = (event) =>
|
|
10033
|
+
var eventStatus = (event) => firstString(event.payload, [
|
|
10691
10034
|
"providerStatus",
|
|
10692
10035
|
"status",
|
|
10693
10036
|
"disposition",
|
|
10694
10037
|
"type",
|
|
10695
10038
|
"reason"
|
|
10696
10039
|
]);
|
|
10697
|
-
var eventElapsedMs = (event) =>
|
|
10040
|
+
var eventElapsedMs = (event) => firstNumber(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
10698
10041
|
var resolveSessionHref2 = (value, sessionId) => {
|
|
10699
10042
|
if (value === false) {
|
|
10700
10043
|
return;
|
|
@@ -11903,7 +11246,7 @@ var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
|
11903
11246
|
return assertion;
|
|
11904
11247
|
};
|
|
11905
11248
|
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
11906
|
-
var
|
|
11249
|
+
var firstString2 = (source, keys) => {
|
|
11907
11250
|
for (const key of keys) {
|
|
11908
11251
|
const value = source[key];
|
|
11909
11252
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -11914,7 +11257,7 @@ var firstString3 = (source, keys) => {
|
|
|
11914
11257
|
}
|
|
11915
11258
|
}
|
|
11916
11259
|
};
|
|
11917
|
-
var
|
|
11260
|
+
var firstNumber2 = (source, keys) => {
|
|
11918
11261
|
for (const key of keys) {
|
|
11919
11262
|
const value = source[key];
|
|
11920
11263
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -12279,8 +11622,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
|
|
|
12279
11622
|
var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
|
|
12280
11623
|
var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
12281
11624
|
const payload = flattenPayload(input.body);
|
|
12282
|
-
const provider =
|
|
12283
|
-
const status =
|
|
11625
|
+
const provider = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
|
|
11626
|
+
const status = firstString2(payload, [
|
|
12284
11627
|
"CallStatus",
|
|
12285
11628
|
"call_status",
|
|
12286
11629
|
"callStatus",
|
|
@@ -12290,7 +11633,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12290
11633
|
"event_type",
|
|
12291
11634
|
"type"
|
|
12292
11635
|
]);
|
|
12293
|
-
const durationMs =
|
|
11636
|
+
const durationMs = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
|
|
12294
11637
|
"CallDuration",
|
|
12295
11638
|
"call_duration",
|
|
12296
11639
|
"callDuration",
|
|
@@ -12298,21 +11641,21 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12298
11641
|
"dial_call_duration",
|
|
12299
11642
|
"duration"
|
|
12300
11643
|
]));
|
|
12301
|
-
const sipCode =
|
|
11644
|
+
const sipCode = firstNumber2(payload, [
|
|
12302
11645
|
"SipResponseCode",
|
|
12303
11646
|
"sip_response_code",
|
|
12304
11647
|
"sipCode",
|
|
12305
11648
|
"sip_code",
|
|
12306
11649
|
"hangupCauseCode"
|
|
12307
11650
|
]);
|
|
12308
|
-
const from =
|
|
12309
|
-
const to =
|
|
11651
|
+
const from = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
|
|
11652
|
+
const to = firstString2(payload, [
|
|
12310
11653
|
"To",
|
|
12311
11654
|
"to",
|
|
12312
11655
|
"called_number",
|
|
12313
11656
|
"calledNumber"
|
|
12314
11657
|
]);
|
|
12315
|
-
const target =
|
|
11658
|
+
const target = firstString2(payload, [
|
|
12316
11659
|
"transferTarget",
|
|
12317
11660
|
"TransferTarget",
|
|
12318
11661
|
"target",
|
|
@@ -12320,7 +11663,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12320
11663
|
"department"
|
|
12321
11664
|
]);
|
|
12322
11665
|
return {
|
|
12323
|
-
answeredBy:
|
|
11666
|
+
answeredBy: firstString2(payload, [
|
|
12324
11667
|
"AnsweredBy",
|
|
12325
11668
|
"answered_by",
|
|
12326
11669
|
"answeredBy",
|
|
@@ -12334,7 +11677,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12334
11677
|
...payload
|
|
12335
11678
|
},
|
|
12336
11679
|
provider,
|
|
12337
|
-
reason:
|
|
11680
|
+
reason: firstString2(payload, [
|
|
12338
11681
|
"Reason",
|
|
12339
11682
|
"reason",
|
|
12340
11683
|
"HangupCause",
|
|
@@ -12350,7 +11693,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12350
11693
|
var defaultSessionId = (input) => {
|
|
12351
11694
|
const payload = flattenPayload(input.body);
|
|
12352
11695
|
const metadataSessionId = input.event.metadata?.sessionId;
|
|
12353
|
-
return
|
|
11696
|
+
return firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
|
|
12354
11697
|
"sessionId",
|
|
12355
11698
|
"session_id",
|
|
12356
11699
|
"SessionId",
|
|
@@ -12365,7 +11708,7 @@ var defaultSessionId = (input) => {
|
|
|
12365
11708
|
};
|
|
12366
11709
|
var defaultIdempotencyKey = (input) => {
|
|
12367
11710
|
const payload = flattenPayload(input.body);
|
|
12368
|
-
const eventId =
|
|
11711
|
+
const eventId = firstString2(payload, [
|
|
12369
11712
|
"id",
|
|
12370
11713
|
"event_id",
|
|
12371
11714
|
"eventId",
|