@absolutejs/voice 0.0.22-beta.464 → 0.0.22-beta.466
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 -927
- package/dist/client/htmxBootstrap.js +7 -330
- package/dist/client/index.js +5 -927
- package/dist/generated/htmxBootstrapBundle.d.ts +1 -1
- package/dist/incidentTimeline.d.ts +1 -0
- package/dist/index.js +146 -1009
- package/dist/operationsRecord.d.ts +16 -0
- package/dist/react/index.js +5 -927
- package/dist/svelte/index.js +5 -927
- package/dist/testing/index.js +92 -975
- package/dist/vue/index.js +5 -927
- package/package.json +3 -3
package/dist/testing/index.js
CHANGED
|
@@ -2159,934 +2159,12 @@ var serverMessageToAction = (message) => {
|
|
|
2159
2159
|
}
|
|
2160
2160
|
};
|
|
2161
2161
|
|
|
2162
|
-
// node_modules/@absolutejs/media/dist/index.js
|
|
2163
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
2164
|
-
import { join } from "path";
|
|
2165
|
-
var formatLabel = (format) => `${format.container}/${format.encoding}/${String(format.sampleRateHz)}hz/${String(format.channels)}ch`;
|
|
2166
|
-
var formatMatches = (actual, expected) => actual.container === expected.container && actual.encoding === expected.encoding && actual.sampleRateHz === expected.sampleRateHz && actual.channels === expected.channels;
|
|
2167
|
-
var pushIssue = (issues, severity, code, message) => {
|
|
2168
|
-
issues.push({ code, message, severity });
|
|
2169
|
-
};
|
|
2170
|
-
var numericMetadata = (frame, key) => {
|
|
2171
|
-
const value = frame.metadata?.[key];
|
|
2172
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2173
|
-
};
|
|
2174
|
-
var average3 = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
|
|
2175
|
-
var max = (values) => values.length === 0 ? undefined : Math.max(...values);
|
|
2176
|
-
var min = (values) => values.length === 0 ? undefined : Math.min(...values);
|
|
2177
|
-
var numericStat = (stat, key) => {
|
|
2178
|
-
const value = stat[key];
|
|
2179
|
-
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
2180
|
-
};
|
|
2181
|
-
var booleanStat = (stat, key) => {
|
|
2182
|
-
const value = stat[key];
|
|
2183
|
-
return typeof value === "boolean" ? value : undefined;
|
|
2184
|
-
};
|
|
2185
|
-
var stringStat = (stat, key) => {
|
|
2186
|
-
const value = stat[key];
|
|
2187
|
-
return typeof value === "string" ? value : undefined;
|
|
2188
|
-
};
|
|
2189
|
-
var statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
|
|
2190
|
-
var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
|
|
2191
|
-
var DEFAULT_TELEPHONY_FORMAT = {
|
|
2192
|
-
channels: 1,
|
|
2193
|
-
container: "raw",
|
|
2194
|
-
encoding: "mulaw",
|
|
2195
|
-
sampleRateHz: 8000
|
|
2196
|
-
};
|
|
2197
|
-
var bytesToBase64 = (audio) => {
|
|
2198
|
-
const bytes = audio instanceof ArrayBuffer ? new Uint8Array(audio) : new Uint8Array(audio.buffer, audio.byteOffset, audio.byteLength);
|
|
2199
|
-
return Buffer.from(bytes).toString("base64");
|
|
2200
|
-
};
|
|
2201
|
-
var base64ToBytes = (value) => new Uint8Array(Buffer.from(value, "base64"));
|
|
2202
|
-
var unknownRecord = (value) => value && typeof value === "object" ? value : {};
|
|
2203
|
-
var firstString = (records, keys) => {
|
|
2204
|
-
for (const record of records) {
|
|
2205
|
-
for (const key of keys) {
|
|
2206
|
-
const value = record[key];
|
|
2207
|
-
if (typeof value === "string" && value.length > 0) {
|
|
2208
|
-
return value;
|
|
2209
|
-
}
|
|
2210
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2211
|
-
return String(value);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2215
|
-
return;
|
|
2216
|
-
};
|
|
2217
|
-
var firstNumber = (records, keys) => {
|
|
2218
|
-
for (const record of records) {
|
|
2219
|
-
for (const key of keys) {
|
|
2220
|
-
const value = record[key];
|
|
2221
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2222
|
-
return value;
|
|
2223
|
-
}
|
|
2224
|
-
if (typeof value === "string") {
|
|
2225
|
-
const parsed = Number(value);
|
|
2226
|
-
if (Number.isFinite(parsed)) {
|
|
2227
|
-
return parsed;
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
return;
|
|
2233
|
-
};
|
|
2234
|
-
var telephonyDirection = (track) => {
|
|
2235
|
-
const normalized = track?.toLowerCase();
|
|
2236
|
-
if (!normalized) {
|
|
2237
|
-
return "unknown";
|
|
2238
|
-
}
|
|
2239
|
-
if (normalized.includes("inbound") || normalized.includes("caller") || normalized.includes("in")) {
|
|
2240
|
-
return "inbound";
|
|
2241
|
-
}
|
|
2242
|
-
if (normalized.includes("outbound") || normalized.includes("assistant") || normalized.includes("out")) {
|
|
2243
|
-
return "outbound";
|
|
2244
|
-
}
|
|
2245
|
-
return "unknown";
|
|
2246
|
-
};
|
|
2247
|
-
var telephonyFrameKind = (direction) => direction === "outbound" ? "assistant-audio" : "input-audio";
|
|
2248
|
-
var telephonyEventKind = (envelope) => {
|
|
2249
|
-
const raw = firstString([envelope], ["event", "type", "eventType"]) ?? firstString([unknownRecord(envelope.message)], ["event", "type"]);
|
|
2250
|
-
const normalized = raw?.toLowerCase().replace(/[_\s-]+/g, "-");
|
|
2251
|
-
if (!normalized) {
|
|
2252
|
-
return "unknown";
|
|
2253
|
-
}
|
|
2254
|
-
if (normalized.includes("connected")) {
|
|
2255
|
-
return "connected";
|
|
2256
|
-
}
|
|
2257
|
-
if (normalized.includes("start")) {
|
|
2258
|
-
return "start";
|
|
2259
|
-
}
|
|
2260
|
-
if (normalized.includes("media")) {
|
|
2261
|
-
return "media";
|
|
2262
|
-
}
|
|
2263
|
-
if (normalized.includes("stop") || normalized.includes("closed")) {
|
|
2264
|
-
return "stop";
|
|
2265
|
-
}
|
|
2266
|
-
if (normalized.includes("error") || normalized.includes("failed")) {
|
|
2267
|
-
return "error";
|
|
2268
|
-
}
|
|
2269
|
-
return "unknown";
|
|
2270
|
-
};
|
|
2271
|
-
var normalizeWebRTCStat = (stat) => {
|
|
2272
|
-
const sample = {};
|
|
2273
|
-
for (const [key, value] of Object.entries(stat)) {
|
|
2274
|
-
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
2275
|
-
sample[key] = value;
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
return sample;
|
|
2279
|
-
};
|
|
2280
|
-
var parseTelephonyMediaFrame = (input) => {
|
|
2281
|
-
const envelope = input.envelope;
|
|
2282
|
-
const media = unknownRecord(envelope.media);
|
|
2283
|
-
const payload = firstString([media, envelope], ["payload", "audio", "data"]) ?? firstString([unknownRecord(envelope.message)], ["payload"]);
|
|
2284
|
-
if (!payload) {
|
|
2285
|
-
return;
|
|
2286
|
-
}
|
|
2287
|
-
const carrier = input.carrier ?? firstString([envelope], ["provider"]) ?? "telephony";
|
|
2288
|
-
const streamId = firstString([media, envelope], ["streamSid", "stream_id", "streamId", "streamId", "callSid", "call_id"]);
|
|
2289
|
-
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
2290
|
-
const track = firstString([media, envelope], ["track", "direction"]);
|
|
2291
|
-
const direction = telephonyDirection(track);
|
|
2292
|
-
const timestamp = firstNumber([media, envelope], ["timestamp", "time", "startedAt"]);
|
|
2293
|
-
return {
|
|
2294
|
-
at: timestamp,
|
|
2295
|
-
audio: base64ToBytes(payload),
|
|
2296
|
-
format: input.format ?? DEFAULT_TELEPHONY_FORMAT,
|
|
2297
|
-
id: [
|
|
2298
|
-
carrier,
|
|
2299
|
-
streamId ?? input.sessionId ?? "stream",
|
|
2300
|
-
sequenceNumber ?? timestamp ?? Date.now()
|
|
2301
|
-
].join(":"),
|
|
2302
|
-
kind: telephonyFrameKind(direction),
|
|
2303
|
-
metadata: {
|
|
2304
|
-
carrier,
|
|
2305
|
-
direction,
|
|
2306
|
-
event: firstString([envelope], ["event", "type"]),
|
|
2307
|
-
sequenceNumber,
|
|
2308
|
-
streamId,
|
|
2309
|
-
track
|
|
2310
|
-
},
|
|
2311
|
-
sessionId: input.sessionId ?? streamId,
|
|
2312
|
-
source: "telephony"
|
|
2313
|
-
};
|
|
2314
|
-
};
|
|
2315
|
-
var serializeTelephonyMediaFrame = (input) => {
|
|
2316
|
-
const carrier = input.carrier ?? input.frame.metadata?.carrier ?? "telephony";
|
|
2317
|
-
const streamId = input.streamId ?? (typeof input.frame.metadata?.streamId === "string" ? input.frame.metadata.streamId : input.frame.sessionId);
|
|
2318
|
-
const sequenceNumber = input.sequenceNumber ?? (typeof input.frame.metadata?.sequenceNumber === "string" || typeof input.frame.metadata?.sequenceNumber === "number" ? input.frame.metadata.sequenceNumber : undefined);
|
|
2319
|
-
const direction = input.frame.kind === "assistant-audio" ? "outbound" : "inbound";
|
|
2320
|
-
const payload = input.frame.audio ? bytesToBase64(input.frame.audio) : "";
|
|
2321
|
-
if (carrier === "twilio") {
|
|
2322
|
-
return {
|
|
2323
|
-
event: "media",
|
|
2324
|
-
sequenceNumber,
|
|
2325
|
-
streamSid: streamId,
|
|
2326
|
-
media: {
|
|
2327
|
-
payload,
|
|
2328
|
-
timestamp: input.frame.at,
|
|
2329
|
-
track: direction
|
|
2330
|
-
}
|
|
2331
|
-
};
|
|
2332
|
-
}
|
|
2333
|
-
if (carrier === "telnyx") {
|
|
2334
|
-
return {
|
|
2335
|
-
event: "media",
|
|
2336
|
-
stream_id: streamId,
|
|
2337
|
-
sequence_number: sequenceNumber,
|
|
2338
|
-
media: {
|
|
2339
|
-
payload,
|
|
2340
|
-
timestamp: input.frame.at,
|
|
2341
|
-
track: direction
|
|
2342
|
-
}
|
|
2343
|
-
};
|
|
2344
|
-
}
|
|
2345
|
-
if (carrier === "plivo") {
|
|
2346
|
-
return {
|
|
2347
|
-
event: "media",
|
|
2348
|
-
streamId,
|
|
2349
|
-
sequenceNumber,
|
|
2350
|
-
media: {
|
|
2351
|
-
payload,
|
|
2352
|
-
timestamp: input.frame.at,
|
|
2353
|
-
track: direction
|
|
2354
|
-
}
|
|
2355
|
-
};
|
|
2356
|
-
}
|
|
2357
|
-
return {
|
|
2358
|
-
event: "media",
|
|
2359
|
-
provider: carrier,
|
|
2360
|
-
sequenceNumber,
|
|
2361
|
-
streamId,
|
|
2362
|
-
media: {
|
|
2363
|
-
payload,
|
|
2364
|
-
timestamp: input.frame.at,
|
|
2365
|
-
track: direction
|
|
2366
|
-
}
|
|
2367
|
-
};
|
|
2368
|
-
};
|
|
2369
|
-
var createTelephonyMediaSerializer = (input) => {
|
|
2370
|
-
const format = input.format ?? DEFAULT_TELEPHONY_FORMAT;
|
|
2371
|
-
return {
|
|
2372
|
-
carrier: input.carrier,
|
|
2373
|
-
format,
|
|
2374
|
-
parse: (envelope) => parseTelephonyMediaFrame({
|
|
2375
|
-
carrier: input.carrier,
|
|
2376
|
-
envelope,
|
|
2377
|
-
format,
|
|
2378
|
-
sessionId: input.sessionId ?? input.streamId
|
|
2379
|
-
}),
|
|
2380
|
-
serialize: (frame) => serializeTelephonyMediaFrame({
|
|
2381
|
-
carrier: input.carrier,
|
|
2382
|
-
frame,
|
|
2383
|
-
streamId: input.streamId
|
|
2384
|
-
})
|
|
2385
|
-
};
|
|
2386
|
-
};
|
|
2387
|
-
var parseTelephonyStreamEvent = (input) => {
|
|
2388
|
-
const envelope = input.envelope;
|
|
2389
|
-
const media = unknownRecord(envelope.media);
|
|
2390
|
-
const start = unknownRecord(envelope.start);
|
|
2391
|
-
const stop = unknownRecord(envelope.stop);
|
|
2392
|
-
const errorRecord = unknownRecord(envelope.error);
|
|
2393
|
-
const kind = telephonyEventKind(envelope);
|
|
2394
|
-
const carrier = input.carrier ?? firstString([envelope], ["provider", "carrier"]) ?? "telephony";
|
|
2395
|
-
const frame = kind === "media" ? parseTelephonyMediaFrame({
|
|
2396
|
-
carrier,
|
|
2397
|
-
envelope,
|
|
2398
|
-
format: input.format,
|
|
2399
|
-
sessionId: input.sessionId
|
|
2400
|
-
}) : undefined;
|
|
2401
|
-
const streamId = firstString([media, start, stop, envelope], ["streamSid", "stream_id", "streamId", "callSid", "call_id"]) ?? input.sessionId;
|
|
2402
|
-
const sequenceNumber = firstString([media, envelope], ["sequenceNumber", "sequence_number", "chunk"]);
|
|
2403
|
-
const track = firstString([media, envelope], ["track", "direction"]);
|
|
2404
|
-
return {
|
|
2405
|
-
audioBytes: frame?.audio ? frame.audio instanceof ArrayBuffer ? frame.audio.byteLength : frame.audio.byteLength : 0,
|
|
2406
|
-
at: frame?.at ?? firstNumber([media, start, stop, envelope], ["timestamp", "time", "startedAt"]),
|
|
2407
|
-
carrier,
|
|
2408
|
-
direction: telephonyDirection(track),
|
|
2409
|
-
error: firstString([errorRecord, envelope], ["message", "error", "reason"]),
|
|
2410
|
-
kind,
|
|
2411
|
-
sequenceNumber,
|
|
2412
|
-
streamId
|
|
2413
|
-
};
|
|
2414
|
-
};
|
|
2415
|
-
var buildMediaTelephonyStreamLifecycleReport = (input = {}) => {
|
|
2416
|
-
const envelopes = input.envelopes ?? [];
|
|
2417
|
-
const events = envelopes.map((envelope) => parseTelephonyStreamEvent({
|
|
2418
|
-
carrier: input.carrier,
|
|
2419
|
-
envelope
|
|
2420
|
-
}));
|
|
2421
|
-
const issues = [];
|
|
2422
|
-
const startedIndex = events.findIndex((event) => event.kind === "start");
|
|
2423
|
-
const firstMediaIndex = events.findIndex((event) => event.kind === "media");
|
|
2424
|
-
const stoppedIndex = events.findIndex((event) => event.kind === "stop");
|
|
2425
|
-
const started = startedIndex >= 0;
|
|
2426
|
-
const stopped = stoppedIndex >= 0;
|
|
2427
|
-
const mediaEvents = events.filter((event) => event.kind === "media");
|
|
2428
|
-
const audioBytes = events.reduce((total, event) => total + event.audioBytes, 0);
|
|
2429
|
-
const minAudioBytes = input.minAudioBytes ?? 1;
|
|
2430
|
-
const streamIds = Array.from(new Set(events.map((event) => event.streamId).filter(Boolean)));
|
|
2431
|
-
if ((input.requireStart ?? true) && !started) {
|
|
2432
|
-
pushIssue(issues, "error", "media.telephony_missing_start", "Telephony media stream did not include a start event.");
|
|
2433
|
-
}
|
|
2434
|
-
if ((input.requireMedia ?? true) && mediaEvents.length === 0) {
|
|
2435
|
-
pushIssue(issues, "error", "media.telephony_missing_media", "Telephony media stream did not include media payload events.");
|
|
2436
|
-
}
|
|
2437
|
-
if ((input.requireStop ?? true) && !stopped) {
|
|
2438
|
-
pushIssue(issues, input.maxMissingStop === false ? "warning" : "error", "media.telephony_missing_stop", "Telephony media stream did not include a stop event.");
|
|
2439
|
-
}
|
|
2440
|
-
if (started && firstMediaIndex >= 0 && firstMediaIndex < startedIndex) {
|
|
2441
|
-
pushIssue(issues, "error", "media.telephony_media_before_start", "Telephony media payload arrived before the stream start event.");
|
|
2442
|
-
}
|
|
2443
|
-
if (stopped && firstMediaIndex >= 0 && stoppedIndex < firstMediaIndex) {
|
|
2444
|
-
pushIssue(issues, "error", "media.telephony_stop_before_media", "Telephony media stream stopped before any media payload arrived.");
|
|
2445
|
-
}
|
|
2446
|
-
if (mediaEvents.length > 0 && audioBytes < minAudioBytes) {
|
|
2447
|
-
pushIssue(issues, "error", "media.telephony_no_audio_bytes", `Telephony media stream parsed ${String(audioBytes)} audio byte(s), below required ${String(minAudioBytes)}.`);
|
|
2448
|
-
}
|
|
2449
|
-
for (const event of events) {
|
|
2450
|
-
if (event.kind === "error") {
|
|
2451
|
-
pushIssue(issues, "error", "media.telephony_stream_error", event.error ?? "Telephony media stream emitted an error event.");
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
return {
|
|
2455
|
-
audioBytes,
|
|
2456
|
-
carrier: input.carrier,
|
|
2457
|
-
checkedAt: Date.now(),
|
|
2458
|
-
events,
|
|
2459
|
-
issues,
|
|
2460
|
-
mediaEvents: mediaEvents.length,
|
|
2461
|
-
started,
|
|
2462
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2463
|
-
stopped,
|
|
2464
|
-
streamIds
|
|
2465
|
-
};
|
|
2466
|
-
};
|
|
2467
|
-
var buildMediaResamplingPlan = (input) => {
|
|
2468
|
-
const required = !formatMatches(input.inputFormat, input.outputFormat);
|
|
2469
|
-
return {
|
|
2470
|
-
inputFormat: input.inputFormat,
|
|
2471
|
-
outputFormat: input.outputFormat,
|
|
2472
|
-
ratio: input.outputFormat.sampleRateHz / input.inputFormat.sampleRateHz,
|
|
2473
|
-
required,
|
|
2474
|
-
status: input.inputFormat.container === input.outputFormat.container && input.inputFormat.encoding === input.outputFormat.encoding && input.inputFormat.channels === input.outputFormat.channels ? "pass" : "warn"
|
|
2475
|
-
};
|
|
2476
|
-
};
|
|
2477
|
-
var speechProbability = (frame) => {
|
|
2478
|
-
if (frame.metadata?.isSpeech === true) {
|
|
2479
|
-
return 1;
|
|
2480
|
-
}
|
|
2481
|
-
if (frame.metadata?.isSpeech === false) {
|
|
2482
|
-
return 0;
|
|
2483
|
-
}
|
|
2484
|
-
for (const key of ["speechProbability", "voiceProbability", "rms", "energy"]) {
|
|
2485
|
-
const value = numericMetadata(frame, key);
|
|
2486
|
-
if (value !== undefined) {
|
|
2487
|
-
return value;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
return 0;
|
|
2491
|
-
};
|
|
2492
|
-
var buildMediaVadReport = (input = {}) => {
|
|
2493
|
-
const frames = (input.frames ?? []).filter((frame) => frame.kind === "input-audio");
|
|
2494
|
-
const speechStartThreshold = input.speechStartThreshold ?? 0.6;
|
|
2495
|
-
const speechEndThreshold = input.speechEndThreshold ?? 0.35;
|
|
2496
|
-
const minSpeechFrames = input.minSpeechFrames ?? 1;
|
|
2497
|
-
const maxSilenceFrames = input.maxSilenceFrames ?? 1;
|
|
2498
|
-
const segments = [];
|
|
2499
|
-
let activeFrames = [];
|
|
2500
|
-
let silenceFrames = 0;
|
|
2501
|
-
const closeSegment = () => {
|
|
2502
|
-
if (activeFrames.length < minSpeechFrames) {
|
|
2503
|
-
activeFrames = [];
|
|
2504
|
-
silenceFrames = 0;
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
const first = activeFrames[0];
|
|
2508
|
-
const last = activeFrames.at(-1);
|
|
2509
|
-
if (!first) {
|
|
2510
|
-
return;
|
|
2511
|
-
}
|
|
2512
|
-
segments.push({
|
|
2513
|
-
durationMs: first.at !== undefined && last?.at !== undefined ? last.at - first.at + (last.durationMs ?? 0) : undefined,
|
|
2514
|
-
endAt: last?.at !== undefined ? last.at + (last.durationMs ?? 0) : undefined,
|
|
2515
|
-
frameCount: activeFrames.length,
|
|
2516
|
-
segmentId: `vad:${String(segments.length + 1)}`,
|
|
2517
|
-
sessionId: first.sessionId,
|
|
2518
|
-
startAt: first.at,
|
|
2519
|
-
turnId: first.turnId
|
|
2520
|
-
});
|
|
2521
|
-
activeFrames = [];
|
|
2522
|
-
silenceFrames = 0;
|
|
2523
|
-
};
|
|
2524
|
-
for (const frame of frames) {
|
|
2525
|
-
const probability = speechProbability(frame);
|
|
2526
|
-
if (activeFrames.length === 0) {
|
|
2527
|
-
if (probability >= speechStartThreshold) {
|
|
2528
|
-
activeFrames.push(frame);
|
|
2529
|
-
}
|
|
2530
|
-
continue;
|
|
2531
|
-
}
|
|
2532
|
-
activeFrames.push(frame);
|
|
2533
|
-
if (probability <= speechEndThreshold) {
|
|
2534
|
-
silenceFrames += 1;
|
|
2535
|
-
} else {
|
|
2536
|
-
silenceFrames = 0;
|
|
2537
|
-
}
|
|
2538
|
-
if (silenceFrames > maxSilenceFrames) {
|
|
2539
|
-
closeSegment();
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
closeSegment();
|
|
2543
|
-
return {
|
|
2544
|
-
checkedAt: Date.now(),
|
|
2545
|
-
inputAudioFrames: frames.length,
|
|
2546
|
-
segments,
|
|
2547
|
-
status: frames.length === 0 ? "warn" : "pass"
|
|
2548
|
-
};
|
|
2549
|
-
};
|
|
2550
|
-
var buildMediaInterruptionReport = (input = {}) => {
|
|
2551
|
-
const issues = [];
|
|
2552
|
-
const interruptionFrames = (input.frames ?? []).filter((frame) => frame.kind === "interruption");
|
|
2553
|
-
const latenciesMs = interruptionFrames.map((frame) => frame.latencyMs).filter((latency) => typeof latency === "number");
|
|
2554
|
-
const maxInterruptionLatencyMs = input.maxInterruptionLatencyMs;
|
|
2555
|
-
if (interruptionFrames.length === 0) {
|
|
2556
|
-
pushIssue(issues, "warning", "media.interruption_missing", "No interruption frame was observed.");
|
|
2557
|
-
}
|
|
2558
|
-
if (maxInterruptionLatencyMs !== undefined && latenciesMs.some((latency) => latency > maxInterruptionLatencyMs)) {
|
|
2559
|
-
pushIssue(issues, "error", "media.interruption_latency", `Interruption latency exceeded ${String(maxInterruptionLatencyMs)}ms.`);
|
|
2560
|
-
}
|
|
2561
|
-
return {
|
|
2562
|
-
checkedAt: Date.now(),
|
|
2563
|
-
interruptionFrames: interruptionFrames.length,
|
|
2564
|
-
issues,
|
|
2565
|
-
latenciesMs,
|
|
2566
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass"
|
|
2567
|
-
};
|
|
2568
|
-
};
|
|
2569
|
-
var buildMediaQualityReport = (input = {}) => {
|
|
2570
|
-
const frames = [...input.frames ?? []].sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
2571
|
-
const audioFrames = frames.filter((frame) => frame.kind === "input-audio" || frame.kind === "assistant-audio");
|
|
2572
|
-
const inputAudioFrames = frames.filter((frame) => frame.kind === "input-audio");
|
|
2573
|
-
const assistantAudioFrames = frames.filter((frame) => frame.kind === "assistant-audio");
|
|
2574
|
-
const issues = [];
|
|
2575
|
-
const gapsMs = [];
|
|
2576
|
-
for (const [index, frame] of audioFrames.entries()) {
|
|
2577
|
-
const previous = audioFrames[index - 1];
|
|
2578
|
-
if (previous?.at === undefined || frame.at === undefined || previous.durationMs === undefined) {
|
|
2579
|
-
continue;
|
|
2580
|
-
}
|
|
2581
|
-
const gap = frame.at - (previous.at + previous.durationMs);
|
|
2582
|
-
if (gap > 0) {
|
|
2583
|
-
gapsMs.push(gap);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
const jitterMs = audioFrames.map((frame) => numericMetadata(frame, "jitterMs")).filter((value) => value !== undefined).at(-1) ?? max(gapsMs);
|
|
2587
|
-
const first = audioFrames.find((frame) => frame.at !== undefined);
|
|
2588
|
-
const last = audioFrames.toReversed().find((frame) => frame.at !== undefined);
|
|
2589
|
-
const durationMs = first?.at !== undefined && last?.at !== undefined ? last.at - first.at + (last.durationMs ?? 0) : undefined;
|
|
2590
|
-
const expectedDurationMs = audioFrames.length > 0 ? audioFrames.reduce((total, frame) => total + (frame.durationMs ?? 0), 0) : undefined;
|
|
2591
|
-
const timestampDriftMs = durationMs !== undefined && expectedDurationMs !== undefined ? Math.max(0, durationMs - expectedDurationMs) : undefined;
|
|
2592
|
-
const speechScores = inputAudioFrames.map(speechProbability);
|
|
2593
|
-
const speechFrames = speechScores.filter((score) => score >= 0.6).length;
|
|
2594
|
-
const silenceFrames = speechScores.filter((score) => score <= 0.35).length;
|
|
2595
|
-
const unknownSpeechFrames = Math.max(0, inputAudioFrames.length - speechFrames - silenceFrames);
|
|
2596
|
-
const speechRatio = inputAudioFrames.length === 0 ? 0 : speechFrames / inputAudioFrames.length;
|
|
2597
|
-
const silenceRatio = inputAudioFrames.length === 0 ? 0 : silenceFrames / inputAudioFrames.length;
|
|
2598
|
-
const levels = audioFrames.map((frame) => numericMetadata(frame, "level") ?? numericMetadata(frame, "rms") ?? numericMetadata(frame, "energy")).filter((value) => value !== undefined);
|
|
2599
|
-
const backpressureEvents = input.transport?.backpressureEvents ?? 0;
|
|
2600
|
-
const maxGapMs = input.maxGapMs;
|
|
2601
|
-
if (maxGapMs !== undefined && gapsMs.some((gap) => gap > maxGapMs)) {
|
|
2602
|
-
pushIssue(issues, "warning", "media.quality_gap", `Observed media gap above ${String(maxGapMs)}ms.`);
|
|
2603
|
-
}
|
|
2604
|
-
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2605
|
-
pushIssue(issues, "warning", "media.quality_jitter", `Observed jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
2606
|
-
}
|
|
2607
|
-
if (input.maxTimestampDriftMs !== undefined && timestampDriftMs !== undefined && timestampDriftMs > input.maxTimestampDriftMs) {
|
|
2608
|
-
pushIssue(issues, "warning", "media.quality_timestamp_drift", `Observed timestamp drift ${String(timestampDriftMs)}ms above ${String(input.maxTimestampDriftMs)}ms.`);
|
|
2609
|
-
}
|
|
2610
|
-
if (input.minSpeechRatio !== undefined && inputAudioFrames.length > 0 && speechRatio < input.minSpeechRatio) {
|
|
2611
|
-
pushIssue(issues, "warning", "media.quality_speech_ratio", `Observed speech ratio ${String(speechRatio)} below ${String(input.minSpeechRatio)}.`);
|
|
2612
|
-
}
|
|
2613
|
-
if (input.maxBackpressureEvents !== undefined && backpressureEvents > input.maxBackpressureEvents) {
|
|
2614
|
-
pushIssue(issues, "warning", "media.quality_backpressure", `Observed ${String(backpressureEvents)} backpressure event(s), above ${String(input.maxBackpressureEvents)}.`);
|
|
2615
|
-
}
|
|
2616
|
-
return {
|
|
2617
|
-
assistantAudioFrames: assistantAudioFrames.length,
|
|
2618
|
-
backpressureEvents,
|
|
2619
|
-
checkedAt: Date.now(),
|
|
2620
|
-
durationMs,
|
|
2621
|
-
gapCount: gapsMs.length,
|
|
2622
|
-
gapsMs,
|
|
2623
|
-
inputAudioFrames: inputAudioFrames.length,
|
|
2624
|
-
issues,
|
|
2625
|
-
jitterMs,
|
|
2626
|
-
levelAverage: average3(levels),
|
|
2627
|
-
levelMax: max(levels),
|
|
2628
|
-
levelMin: min(levels),
|
|
2629
|
-
silenceFrames,
|
|
2630
|
-
silenceRatio,
|
|
2631
|
-
speechFrames,
|
|
2632
|
-
speechRatio,
|
|
2633
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2634
|
-
timestampDriftMs,
|
|
2635
|
-
totalFrames: frames.length,
|
|
2636
|
-
unknownSpeechFrames
|
|
2637
|
-
};
|
|
2638
|
-
};
|
|
2639
|
-
var buildMediaWebRTCStatsReport = (input = {}) => {
|
|
2640
|
-
const stats = input.stats ?? [];
|
|
2641
|
-
const issues = [];
|
|
2642
|
-
const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
2643
|
-
const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
|
|
2644
|
-
const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
|
|
2645
|
-
const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
|
|
2646
|
-
const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
|
|
2647
|
-
const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
|
|
2648
|
-
const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
|
|
2649
|
-
const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
|
|
2650
|
-
const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
|
|
2651
|
-
const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
|
|
2652
|
-
const packetLossDenominator = inboundPackets + packetsLost;
|
|
2653
|
-
const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
|
|
2654
|
-
const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
|
|
2655
|
-
const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
|
|
2656
|
-
const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
|
|
2657
|
-
const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
|
|
2658
|
-
const jitterBufferDelayMs = max(inbound.map((stat) => {
|
|
2659
|
-
const delay = numericStat(stat, "jitterBufferDelay");
|
|
2660
|
-
const emitted = numericStat(stat, "jitterBufferEmittedCount");
|
|
2661
|
-
return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
|
|
2662
|
-
}).filter((value) => value !== undefined));
|
|
2663
|
-
const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
|
|
2664
|
-
if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
|
|
2665
|
-
pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
|
|
2666
|
-
}
|
|
2667
|
-
if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
|
|
2668
|
-
pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
|
|
2669
|
-
}
|
|
2670
|
-
if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
|
|
2671
|
-
pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
|
|
2672
|
-
}
|
|
2673
|
-
if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
|
|
2674
|
-
pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
|
|
2675
|
-
}
|
|
2676
|
-
if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2677
|
-
pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
|
|
2678
|
-
}
|
|
2679
|
-
return {
|
|
2680
|
-
activeCandidatePairs,
|
|
2681
|
-
audioLevelAverage: average3(audioLevels),
|
|
2682
|
-
bytesReceived,
|
|
2683
|
-
bytesSent,
|
|
2684
|
-
checkedAt: Date.now(),
|
|
2685
|
-
endedAudioTracks,
|
|
2686
|
-
inboundPackets,
|
|
2687
|
-
issues,
|
|
2688
|
-
jitterBufferDelayMs,
|
|
2689
|
-
jitterMs,
|
|
2690
|
-
liveAudioTracks,
|
|
2691
|
-
outboundPackets,
|
|
2692
|
-
packetLossRatio,
|
|
2693
|
-
packetsLost,
|
|
2694
|
-
roundTripTimeMs,
|
|
2695
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2696
|
-
totalStats: stats.length
|
|
2697
|
-
};
|
|
2698
|
-
};
|
|
2699
|
-
var collectMediaWebRTCStats = async (input) => {
|
|
2700
|
-
const report = await input.peerConnection.getStats(input.selector ?? null);
|
|
2701
|
-
return [...report.values()].map(normalizeWebRTCStat);
|
|
2702
|
-
};
|
|
2703
|
-
var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
|
|
2704
|
-
const stats = input.stats ?? [];
|
|
2705
|
-
const previousStats = input.previousStats ?? [];
|
|
2706
|
-
const issues = [];
|
|
2707
|
-
const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
|
|
2708
|
-
const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
|
|
2709
|
-
const streams = audioRtp.map((stat) => {
|
|
2710
|
-
const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
|
|
2711
|
-
const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
|
|
2712
|
-
const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
|
|
2713
|
-
const previous = previousByKey.get(statKey(stat));
|
|
2714
|
-
const currentPackets = numericStat(stat, packetsKey);
|
|
2715
|
-
const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
|
|
2716
|
-
const currentBytes = numericStat(stat, bytesKey);
|
|
2717
|
-
const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
|
|
2718
|
-
const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
|
|
2719
|
-
return {
|
|
2720
|
-
bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
|
|
2721
|
-
currentPackets,
|
|
2722
|
-
direction,
|
|
2723
|
-
id: statKey(stat),
|
|
2724
|
-
packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
|
|
2725
|
-
previousPackets,
|
|
2726
|
-
timeDeltaMs
|
|
2727
|
-
};
|
|
2728
|
-
});
|
|
2729
|
-
const inbound = streams.filter((stream) => stream.direction === "inbound");
|
|
2730
|
-
const outbound = streams.filter((stream) => stream.direction === "outbound");
|
|
2731
|
-
const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
|
|
2732
|
-
const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
2733
|
-
const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
|
|
2734
|
-
if (input.requireInboundAudio && inbound.length === 0) {
|
|
2735
|
-
pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
|
|
2736
|
-
}
|
|
2737
|
-
if (input.requireOutboundAudio && outbound.length === 0) {
|
|
2738
|
-
pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
|
|
2739
|
-
}
|
|
2740
|
-
if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
|
|
2741
|
-
pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
|
|
2742
|
-
}
|
|
2743
|
-
if (stalledInboundStreams > 0) {
|
|
2744
|
-
pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
|
|
2745
|
-
}
|
|
2746
|
-
if (stalledOutboundStreams > 0) {
|
|
2747
|
-
pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
|
|
2748
|
-
}
|
|
2749
|
-
return {
|
|
2750
|
-
checkedAt: Date.now(),
|
|
2751
|
-
inboundAudioStreams: inbound.length,
|
|
2752
|
-
issues,
|
|
2753
|
-
maxObservedGapMs,
|
|
2754
|
-
outboundAudioStreams: outbound.length,
|
|
2755
|
-
stalledInboundStreams,
|
|
2756
|
-
stalledOutboundStreams,
|
|
2757
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2758
|
-
streams,
|
|
2759
|
-
totalStats: stats.length
|
|
2760
|
-
};
|
|
2761
|
-
};
|
|
2762
|
-
var buildMediaPipelineCalibrationReport = (input = {}) => {
|
|
2763
|
-
const frames = input.frames ?? [];
|
|
2764
|
-
const issues = [];
|
|
2765
|
-
const inputFrames = frames.filter((frame) => frame.kind === "input-audio");
|
|
2766
|
-
const assistantFrames = frames.filter((frame) => frame.kind === "assistant-audio");
|
|
2767
|
-
const turnCommitFrames = frames.filter((frame) => frame.kind === "turn-commit");
|
|
2768
|
-
const interruptionFrameRecords = frames.filter((frame) => frame.kind === "interruption");
|
|
2769
|
-
const traceLinkedFrames = frames.filter((frame) => frame.traceEventId).length;
|
|
2770
|
-
const backpressureFrames = frames.filter((frame) => Boolean(frame.metadata?.backpressure)).length;
|
|
2771
|
-
const audioLatencies = assistantFrames.map((frame) => frame.latencyMs).filter((latency) => typeof latency === "number");
|
|
2772
|
-
const firstAudioLatencyMs = audioLatencies.length > 0 ? Math.min(...audioLatencies) : undefined;
|
|
2773
|
-
const jitterValues = frames.map((frame) => numericMetadata(frame, "jitterMs")).filter((value) => value !== undefined);
|
|
2774
|
-
const jitterMs = jitterValues.length > 0 ? Math.max(...jitterValues) : undefined;
|
|
2775
|
-
const inputFormat = input.inputFormat ?? inputFrames.find((frame) => frame.format)?.format;
|
|
2776
|
-
const outputFormat = input.outputFormat ?? assistantFrames.find((frame) => frame.format)?.format;
|
|
2777
|
-
const resamplingRequired = Boolean(input.expectedInputFormat && inputFormat && inputFormat.sampleRateHz !== input.expectedInputFormat.sampleRateHz) || Boolean(input.expectedOutputFormat && outputFormat && outputFormat.sampleRateHz !== input.expectedOutputFormat.sampleRateHz);
|
|
2778
|
-
const resamplingTargetHz = resamplingRequired && input.expectedInputFormat ? input.expectedInputFormat.sampleRateHz : resamplingRequired ? input.expectedOutputFormat?.sampleRateHz : undefined;
|
|
2779
|
-
if (inputFrames.length === 0) {
|
|
2780
|
-
pushIssue(issues, "warning", "media.input_audio_missing", "No input audio frames were observed.");
|
|
2781
|
-
}
|
|
2782
|
-
if (assistantFrames.length === 0) {
|
|
2783
|
-
pushIssue(issues, "warning", "media.assistant_audio_missing", "No assistant audio frames were observed.");
|
|
2784
|
-
}
|
|
2785
|
-
if (input.expectedInputFormat && inputFormat && !formatMatches(inputFormat, input.expectedInputFormat)) {
|
|
2786
|
-
pushIssue(issues, inputFormat.sampleRateHz === input.expectedInputFormat.sampleRateHz ? "warning" : "error", "media.input_format_mismatch", `Input format ${formatLabel(inputFormat)} does not match expected ${formatLabel(input.expectedInputFormat)}.`);
|
|
2787
|
-
}
|
|
2788
|
-
if (input.expectedOutputFormat && outputFormat && !formatMatches(outputFormat, input.expectedOutputFormat)) {
|
|
2789
|
-
pushIssue(issues, outputFormat.sampleRateHz === input.expectedOutputFormat.sampleRateHz ? "warning" : "error", "media.output_format_mismatch", `Output format ${formatLabel(outputFormat)} does not match expected ${formatLabel(input.expectedOutputFormat)}.`);
|
|
2790
|
-
}
|
|
2791
|
-
if (firstAudioLatencyMs !== undefined && input.maxFirstAudioLatencyMs !== undefined && firstAudioLatencyMs > input.maxFirstAudioLatencyMs) {
|
|
2792
|
-
pushIssue(issues, "error", "media.first_audio_latency", `First audio latency ${String(firstAudioLatencyMs)}ms exceeds budget ${String(input.maxFirstAudioLatencyMs)}ms.`);
|
|
2793
|
-
}
|
|
2794
|
-
if (jitterMs !== undefined && input.maxJitterMs !== undefined && jitterMs > input.maxJitterMs) {
|
|
2795
|
-
pushIssue(issues, "warning", "media.jitter", `Media jitter ${String(jitterMs)}ms exceeds budget ${String(input.maxJitterMs)}ms.`);
|
|
2796
|
-
}
|
|
2797
|
-
if (input.maxBackpressureFrames !== undefined && backpressureFrames > input.maxBackpressureFrames) {
|
|
2798
|
-
pushIssue(issues, "warning", "media.backpressure", `Backpressure frame count ${String(backpressureFrames)} exceeds budget ${String(input.maxBackpressureFrames)}.`);
|
|
2799
|
-
}
|
|
2800
|
-
if (input.requireInterruptionFrame && interruptionFrameRecords.length === 0) {
|
|
2801
|
-
pushIssue(issues, "warning", "media.interruption_missing", "No interruption frame was observed.");
|
|
2802
|
-
}
|
|
2803
|
-
if (input.requireTraceEvidence && traceLinkedFrames === 0) {
|
|
2804
|
-
pushIssue(issues, "warning", "media.trace_evidence_missing", "No media frames were linked to trace evidence.");
|
|
2805
|
-
}
|
|
2806
|
-
return {
|
|
2807
|
-
assistantAudioFrames: assistantFrames.length,
|
|
2808
|
-
backpressureFrames,
|
|
2809
|
-
checkedAt: Date.now(),
|
|
2810
|
-
firstAudioLatencyMs,
|
|
2811
|
-
inputAudioFrames: inputFrames.length,
|
|
2812
|
-
inputFormat,
|
|
2813
|
-
interruptionFrames: interruptionFrameRecords.length,
|
|
2814
|
-
issues,
|
|
2815
|
-
jitterMs,
|
|
2816
|
-
outputFormat,
|
|
2817
|
-
resamplingRequired,
|
|
2818
|
-
resamplingTargetHz,
|
|
2819
|
-
status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
|
|
2820
|
-
surface: input.surface ?? "media-pipeline",
|
|
2821
|
-
traceLinkedFrames,
|
|
2822
|
-
turnCommitFrames: turnCommitFrames.length
|
|
2823
|
-
};
|
|
2824
|
-
};
|
|
2825
|
-
var DEFAULT_METADATA_DENY = [
|
|
2826
|
-
"audioPayload",
|
|
2827
|
-
"auth",
|
|
2828
|
-
"authorization",
|
|
2829
|
-
"cookie",
|
|
2830
|
-
"email",
|
|
2831
|
-
"phone",
|
|
2832
|
-
"phoneNumber",
|
|
2833
|
-
"rawPayload",
|
|
2834
|
-
"secret",
|
|
2835
|
-
"token",
|
|
2836
|
-
"transcript",
|
|
2837
|
-
"utterance"
|
|
2838
|
-
];
|
|
2839
|
-
var DEFAULT_TRUNCATE = 8;
|
|
2840
|
-
var issueCodes = (issues) => Array.from(new Set(issues.map((issue) => issue.code)));
|
|
2841
|
-
var lastEventKind = (events) => events[events.length - 1]?.kind;
|
|
2842
|
-
var formatOptionalMs = (value) => value === undefined ? "n/a" : `${String(Math.round(value))}ms`;
|
|
2843
|
-
var formatRatio = (value) => `${(value * 100).toFixed(1)}%`;
|
|
2844
|
-
var escapeMarkdownCell = (value) => value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2845
|
-
var renderIssuesTable = (issues) => {
|
|
2846
|
-
if (issues.length === 0) {
|
|
2847
|
-
return `- No issues.
|
|
2848
|
-
`;
|
|
2849
|
-
}
|
|
2850
|
-
const rows = issues.map((issue) => `| ${issue.severity} | ${escapeMarkdownCell(issue.code)} | ${escapeMarkdownCell(issue.message)} |`).join(`
|
|
2851
|
-
`);
|
|
2852
|
-
return `| Severity | Code | Message |
|
|
2853
|
-
| --- | --- | --- |
|
|
2854
|
-
${rows}
|
|
2855
|
-
`;
|
|
2856
|
-
};
|
|
2857
|
-
var summarizeMediaQualityReport = (report) => ({
|
|
2858
|
-
backpressureEvents: report.backpressureEvents,
|
|
2859
|
-
description: `${report.totalFrames} frame(s), ${report.gapCount} gap(s), speech ${formatRatio(report.speechRatio)}, status ${report.status}.`,
|
|
2860
|
-
driftMs: report.timestampDriftMs,
|
|
2861
|
-
frameCount: report.totalFrames,
|
|
2862
|
-
gapCount: report.gapCount,
|
|
2863
|
-
issueCodes: issueCodes(report.issues),
|
|
2864
|
-
issueCount: report.issues.length,
|
|
2865
|
-
jitterMs: report.jitterMs,
|
|
2866
|
-
silenceRatio: report.silenceRatio,
|
|
2867
|
-
speechRatio: report.speechRatio,
|
|
2868
|
-
status: report.status
|
|
2869
|
-
});
|
|
2870
|
-
var summarizeMediaTransportReport = (report) => ({
|
|
2871
|
-
backpressureEvents: report.backpressureEvents,
|
|
2872
|
-
description: `${report.name}: ${report.state}, in ${report.inputFrames}, out ${report.outputFrames}, backpressure ${report.backpressureEvents}.`,
|
|
2873
|
-
errors: report.events.filter((event) => event.kind === "error").length,
|
|
2874
|
-
inputFrames: report.inputFrames,
|
|
2875
|
-
lastEventKind: lastEventKind(report.events),
|
|
2876
|
-
name: report.name,
|
|
2877
|
-
outputFrames: report.outputFrames,
|
|
2878
|
-
state: report.state,
|
|
2879
|
-
status: report.status
|
|
2880
|
-
});
|
|
2881
|
-
var summarizeMediaProcessorGraphReport = (report) => {
|
|
2882
|
-
const errorIssueCodes = Array.from(new Set(report.errors.map((event) => event.kind)));
|
|
2883
|
-
return {
|
|
2884
|
-
backpressureEvents: report.backpressure.events.length,
|
|
2885
|
-
description: `${report.name}: ${report.state}, ${report.nodes.length} node(s), in ${report.inputFrames}, out ${report.emittedFrames}, dropped ${report.droppedFrames}.`,
|
|
2886
|
-
droppedFrames: report.droppedFrames,
|
|
2887
|
-
edgeCount: report.edges.length,
|
|
2888
|
-
edgeEventCount: report.edgeEvents.length,
|
|
2889
|
-
emittedFrames: report.emittedFrames,
|
|
2890
|
-
errorCount: report.errors.length,
|
|
2891
|
-
inputFrames: report.inputFrames,
|
|
2892
|
-
issueCodes: errorIssueCodes,
|
|
2893
|
-
lifecycleEventCount: report.lifecycleEvents.length,
|
|
2894
|
-
name: report.name,
|
|
2895
|
-
nodeCount: report.nodes.length,
|
|
2896
|
-
state: report.state,
|
|
2897
|
-
status: report.status,
|
|
2898
|
-
timingMaxMs: report.timing.maxNodeMs
|
|
2899
|
-
};
|
|
2900
|
-
};
|
|
2901
|
-
var renderMediaQualityMarkdown = (report, options = {}) => {
|
|
2902
|
-
const title = options.title ?? "Media Quality Report";
|
|
2903
|
-
const lines = [
|
|
2904
|
-
`# ${title}`,
|
|
2905
|
-
"",
|
|
2906
|
-
`Status: **${report.status}**`,
|
|
2907
|
-
"",
|
|
2908
|
-
"| Metric | Value |",
|
|
2909
|
-
"| --- | ---: |",
|
|
2910
|
-
`| Total frames | ${report.totalFrames} |`,
|
|
2911
|
-
`| Input audio | ${report.inputAudioFrames} |`,
|
|
2912
|
-
`| Assistant audio | ${report.assistantAudioFrames} |`,
|
|
2913
|
-
`| Gaps | ${report.gapCount} |`,
|
|
2914
|
-
`| Jitter | ${formatOptionalMs(report.jitterMs)} |`,
|
|
2915
|
-
`| Timestamp drift | ${formatOptionalMs(report.timestampDriftMs)} |`,
|
|
2916
|
-
`| Speech ratio | ${formatRatio(report.speechRatio)} |`,
|
|
2917
|
-
`| Silence ratio | ${formatRatio(report.silenceRatio)} |`,
|
|
2918
|
-
`| Backpressure events | ${report.backpressureEvents} |`,
|
|
2919
|
-
"",
|
|
2920
|
-
"## Issues",
|
|
2921
|
-
"",
|
|
2922
|
-
renderIssuesTable(report.issues).trimEnd()
|
|
2923
|
-
];
|
|
2924
|
-
return `${lines.join(`
|
|
2925
|
-
`)}
|
|
2926
|
-
`;
|
|
2927
|
-
};
|
|
2928
|
-
var renderMediaTransportMarkdown = (report, options = {}) => {
|
|
2929
|
-
const title = options.title ?? `Media Transport: ${report.name}`;
|
|
2930
|
-
const limit = options.redact?.truncateArraysAt ?? DEFAULT_TRUNCATE;
|
|
2931
|
-
const events = report.events.slice(-limit);
|
|
2932
|
-
const eventRows = events.length === 0 ? "- No transport events recorded." : ["| At | Kind | State | Buffered | Error |", "| --- | --- | --- | ---: | --- |"].concat(events.map((event) => `| ${event.at} | ${event.kind} | ${event.state} | ${event.bufferedFrames ?? ""} | ${escapeMarkdownCell(event.error ?? "")} |`)).join(`
|
|
2933
|
-
`);
|
|
2934
|
-
const lines = [
|
|
2935
|
-
`# ${title}`,
|
|
2936
|
-
"",
|
|
2937
|
-
`Status: **${report.status}** \xB7 State: **${report.state}**`,
|
|
2938
|
-
"",
|
|
2939
|
-
"| Metric | Value |",
|
|
2940
|
-
"| --- | ---: |",
|
|
2941
|
-
`| Input frames | ${report.inputFrames} |`,
|
|
2942
|
-
`| Output frames | ${report.outputFrames} |`,
|
|
2943
|
-
`| Backpressure events | ${report.backpressureEvents} |`,
|
|
2944
|
-
`| Connected | ${report.connected ? "yes" : "no"} |`,
|
|
2945
|
-
`| Closed | ${report.closed ? "yes" : "no"} |`,
|
|
2946
|
-
`| Failed | ${report.failed ? "yes" : "no"} |`,
|
|
2947
|
-
"",
|
|
2948
|
-
`## Last ${events.length} event(s)`,
|
|
2949
|
-
"",
|
|
2950
|
-
eventRows
|
|
2951
|
-
];
|
|
2952
|
-
return `${lines.join(`
|
|
2953
|
-
`)}
|
|
2954
|
-
`;
|
|
2955
|
-
};
|
|
2956
|
-
var renderMediaProcessorGraphMarkdown = (report, options = {}) => {
|
|
2957
|
-
const title = options.title ?? `Media Processor Graph: ${report.name}`;
|
|
2958
|
-
const limit = options.redact?.truncateArraysAt ?? DEFAULT_TRUNCATE;
|
|
2959
|
-
const nodeRows = report.nodes.map((node) => `| ${escapeMarkdownCell(node.name)} | ${node.kind} | ${node.status} | ${node.inputFrames} | ${node.emittedFrames} | ${node.droppedFrames} | ${node.errors.length} |`).join(`
|
|
2960
|
-
`);
|
|
2961
|
-
const edgeRows = report.edges.slice(0, limit).map((edge) => `| ${escapeMarkdownCell(edge.from)} | ${escapeMarkdownCell(edge.to)} | ${edge.status} | ${edge.emittedFrames} |`).join(`
|
|
2962
|
-
`);
|
|
2963
|
-
const issues = report.errors.map((event) => ({
|
|
2964
|
-
code: event.kind,
|
|
2965
|
-
message: event.error ?? `Processor graph ${event.kind} (state ${event.state}).`,
|
|
2966
|
-
severity: "error"
|
|
2967
|
-
}));
|
|
2968
|
-
const lines = [
|
|
2969
|
-
`# ${title}`,
|
|
2970
|
-
"",
|
|
2971
|
-
`Status: **${report.status}** \xB7 State: **${report.state}**`,
|
|
2972
|
-
"",
|
|
2973
|
-
"| Metric | Value |",
|
|
2974
|
-
"| --- | ---: |",
|
|
2975
|
-
`| Nodes | ${report.nodes.length} |`,
|
|
2976
|
-
`| Input frames | ${report.inputFrames} |`,
|
|
2977
|
-
`| Emitted frames | ${report.emittedFrames} |`,
|
|
2978
|
-
`| Dropped frames | ${report.droppedFrames} |`,
|
|
2979
|
-
`| Lifecycle events | ${report.lifecycleEvents.length} |`,
|
|
2980
|
-
`| Edge events | ${report.edgeEvents.length} |`,
|
|
2981
|
-
`| Backpressure events | ${report.backpressure.events.length} |`,
|
|
2982
|
-
`| Timing max | ${formatOptionalMs(report.timing.maxNodeMs)} |`,
|
|
2983
|
-
`| Timing average | ${formatOptionalMs(report.timing.averageNodeMs)} |`,
|
|
2984
|
-
"",
|
|
2985
|
-
"## Nodes",
|
|
2986
|
-
"",
|
|
2987
|
-
nodeRows ? `| Node | Kind | Status | In | Out | Dropped | Errors |
|
|
2988
|
-
| --- | --- | --- | ---: | ---: | ---: | ---: |
|
|
2989
|
-
${nodeRows}` : "- No nodes.",
|
|
2990
|
-
"",
|
|
2991
|
-
`## Edges (showing up to ${limit})`,
|
|
2992
|
-
"",
|
|
2993
|
-
edgeRows ? `| From | To | Status | Frames |
|
|
2994
|
-
| --- | --- | --- | ---: |
|
|
2995
|
-
${edgeRows}` : "- No edges.",
|
|
2996
|
-
"",
|
|
2997
|
-
"## Errors",
|
|
2998
|
-
"",
|
|
2999
|
-
renderIssuesTable(issues).trimEnd()
|
|
3000
|
-
];
|
|
3001
|
-
return `${lines.join(`
|
|
3002
|
-
`)}
|
|
3003
|
-
`;
|
|
3004
|
-
};
|
|
3005
|
-
var truncateArrays = (value, limit, seen) => {
|
|
3006
|
-
if (Array.isArray(value)) {
|
|
3007
|
-
const head = value.slice(0, limit).map((entry) => truncateArrays(entry, limit, seen));
|
|
3008
|
-
if (value.length > limit) {
|
|
3009
|
-
return [...head, { truncated: value.length - limit }];
|
|
3010
|
-
}
|
|
3011
|
-
return head;
|
|
3012
|
-
}
|
|
3013
|
-
if (value && typeof value === "object") {
|
|
3014
|
-
if (seen.has(value))
|
|
3015
|
-
return value;
|
|
3016
|
-
seen.add(value);
|
|
3017
|
-
const next = {};
|
|
3018
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
3019
|
-
next[key] = truncateArrays(entry, limit, seen);
|
|
3020
|
-
}
|
|
3021
|
-
return next;
|
|
3022
|
-
}
|
|
3023
|
-
return value;
|
|
3024
|
-
};
|
|
3025
|
-
var applyRedaction = (value, options, seen) => {
|
|
3026
|
-
const mode = options.mode ?? "omit";
|
|
3027
|
-
const maskValue = options.maskValue ?? "[redacted]";
|
|
3028
|
-
const allow = new Set(options.metadataAllow ?? []);
|
|
3029
|
-
const deny = new Set(options.metadataDeny ?? DEFAULT_METADATA_DENY);
|
|
3030
|
-
const walk = (input) => {
|
|
3031
|
-
if (Array.isArray(input)) {
|
|
3032
|
-
return input.map((entry) => walk(entry));
|
|
3033
|
-
}
|
|
3034
|
-
if (input && typeof input === "object") {
|
|
3035
|
-
if (seen.has(input))
|
|
3036
|
-
return input;
|
|
3037
|
-
seen.add(input);
|
|
3038
|
-
const next = {};
|
|
3039
|
-
for (const [key, entry] of Object.entries(input)) {
|
|
3040
|
-
if (allow.has(key)) {
|
|
3041
|
-
next[key] = entry;
|
|
3042
|
-
continue;
|
|
3043
|
-
}
|
|
3044
|
-
if (deny.has(key)) {
|
|
3045
|
-
if (mode === "mask")
|
|
3046
|
-
next[key] = maskValue;
|
|
3047
|
-
continue;
|
|
3048
|
-
}
|
|
3049
|
-
next[key] = walk(entry);
|
|
3050
|
-
}
|
|
3051
|
-
return next;
|
|
3052
|
-
}
|
|
3053
|
-
return input;
|
|
3054
|
-
};
|
|
3055
|
-
return walk(value);
|
|
3056
|
-
};
|
|
3057
|
-
var redactMediaReport = (report, options = {}) => {
|
|
3058
|
-
const limit = options.truncateArraysAt ?? DEFAULT_TRUNCATE;
|
|
3059
|
-
const truncated = truncateArrays(report, limit, new WeakSet);
|
|
3060
|
-
return applyRedaction(truncated, options, new WeakSet);
|
|
3061
|
-
};
|
|
3062
|
-
var buildArtifactPair = (report, summary, markdown, options) => {
|
|
3063
|
-
const jsonValue = options.redact ? redactMediaReport(report, options.redact) : report;
|
|
3064
|
-
return {
|
|
3065
|
-
json: JSON.stringify(jsonValue, null, 2),
|
|
3066
|
-
jsonValue,
|
|
3067
|
-
markdown,
|
|
3068
|
-
summary
|
|
3069
|
-
};
|
|
3070
|
-
};
|
|
3071
|
-
var buildMediaQualityArtifact = (report, options = {}) => buildArtifactPair(report, summarizeMediaQualityReport(report), renderMediaQualityMarkdown(report, options), options);
|
|
3072
|
-
var buildMediaTransportArtifact = (report, options = {}) => buildArtifactPair(report, summarizeMediaTransportReport(report), renderMediaTransportMarkdown(report, options), options);
|
|
3073
|
-
var buildMediaProcessorGraphArtifact = (report, options = {}) => buildArtifactPair(report, summarizeMediaProcessorGraphReport(report), renderMediaProcessorGraphMarkdown(report, options), options);
|
|
3074
|
-
var writeMediaArtifact = async (input) => {
|
|
3075
|
-
await mkdir(input.dir, { recursive: true });
|
|
3076
|
-
const jsonPath = join(input.dir, `${input.slug}.json`);
|
|
3077
|
-
const markdownPath = join(input.dir, `${input.slug}.md`);
|
|
3078
|
-
await Promise.all([
|
|
3079
|
-
writeFile(jsonPath, input.json, "utf8"),
|
|
3080
|
-
writeFile(markdownPath, input.markdown, "utf8")
|
|
3081
|
-
]);
|
|
3082
|
-
return {
|
|
3083
|
-
jsonPath,
|
|
3084
|
-
markdownPath,
|
|
3085
|
-
summary: input.summary
|
|
3086
|
-
};
|
|
3087
|
-
};
|
|
3088
|
-
|
|
3089
2162
|
// src/client/browserMedia.ts
|
|
2163
|
+
import {
|
|
2164
|
+
buildMediaWebRTCStatsReport,
|
|
2165
|
+
buildMediaWebRTCStreamContinuityReport,
|
|
2166
|
+
collectMediaWebRTCStats
|
|
2167
|
+
} from "@absolutejs/media";
|
|
3090
2168
|
var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
|
|
3091
2169
|
var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
|
|
3092
2170
|
var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
|
|
@@ -8854,16 +7932,16 @@ var renderVoiceCallReviewHTML = (artifact) => {
|
|
|
8854
7932
|
</html>`;
|
|
8855
7933
|
};
|
|
8856
7934
|
// src/testing/sessionBenchmark.ts
|
|
8857
|
-
var
|
|
7935
|
+
var average3 = (values) => values.length > 0 ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
|
|
8858
7936
|
var normalizeTurnText = (value) => value.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu, " ").replace(/\s+/g, " ").trim();
|
|
8859
7937
|
var countPassedTurns = (turnResults) => turnResults.reduce((count, result) => count + (result.passes ? 1 : 0), 0);
|
|
8860
7938
|
var calculateTurnPassRate = (turnResults) => turnResults.length > 0 ? countPassedTurns(turnResults) / turnResults.length : 0;
|
|
8861
7939
|
var summarizeScenarioCosts = (turnResults) => {
|
|
8862
7940
|
const costEstimates = turnResults.map((turn) => turn.quality?.cost).filter((value) => value !== undefined);
|
|
8863
7941
|
return {
|
|
8864
|
-
averageRelativeCostUnits: roundMetric5(
|
|
8865
|
-
fallbackReplayAudioMs: roundMetric5(
|
|
8866
|
-
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)
|
|
8867
7945
|
};
|
|
8868
7946
|
};
|
|
8869
7947
|
var roundMetric5 = (value, digits = 4) => {
|
|
@@ -9182,13 +8260,13 @@ var summarizeVoiceSessionBenchmark = (adapterId, scenarios) => {
|
|
|
9182
8260
|
const turnAccuracies = scenarios.flatMap((scenario) => scenario.turnResults.map((turn) => turn.accuracy?.wordErrorRate).filter((value) => typeof value === "number"));
|
|
9183
8261
|
return {
|
|
9184
8262
|
adapterId,
|
|
9185
|
-
averageElapsedMs: roundMetric5(
|
|
9186
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
9187
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
9188
|
-
averageReconnectCount: roundMetric5(
|
|
9189
|
-
averageRelativeCostUnits: roundMetric5(
|
|
9190
|
-
averageTurnPassRate: roundMetric5(
|
|
9191
|
-
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)),
|
|
9192
8270
|
duplicateTurnRate: roundMetric5(scenarios.length > 0 ? scenarios.filter((scenario) => scenario.duplicateTurnCount > 0).length / scenarios.length : 0),
|
|
9193
8271
|
passCount,
|
|
9194
8272
|
passRate: roundMetric5(scenarios.length > 0 ? passCount / scenarios.length : 0),
|
|
@@ -9214,13 +8292,13 @@ var summarizeVoiceSessionBenchmarkSeries = (input) => {
|
|
|
9214
8292
|
const passCount = results.filter((scenario) => scenario.passes).length;
|
|
9215
8293
|
const sample = results[0];
|
|
9216
8294
|
return {
|
|
9217
|
-
averageElapsedMs: roundMetric5(
|
|
9218
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
9219
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
9220
|
-
averageReconnectCount: roundMetric5(
|
|
9221
|
-
averageRelativeCostUnits: roundMetric5(
|
|
9222
|
-
averageTurnPassRate: roundMetric5(
|
|
9223
|
-
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)),
|
|
9224
8302
|
bestWordErrorRate: roundMetric5(wordErrorRates.length > 0 ? Math.min(...wordErrorRates) : 0),
|
|
9225
8303
|
fixtureId,
|
|
9226
8304
|
passCount,
|
|
@@ -9243,18 +8321,18 @@ var summarizeVoiceSessionBenchmarkSeries = (input) => {
|
|
|
9243
8321
|
scenarios: scenarioAggregates,
|
|
9244
8322
|
summary: {
|
|
9245
8323
|
adapterId: input.adapterId,
|
|
9246
|
-
averageElapsedMs: roundMetric5(
|
|
9247
|
-
averageFallbackReplayAudioMs: roundMetric5(
|
|
9248
|
-
averagePassRate: roundMetric5(
|
|
9249
|
-
averagePrimaryAudioMs: roundMetric5(
|
|
9250
|
-
averageReconnectCount: roundMetric5(
|
|
9251
|
-
averageRelativeCostUnits: roundMetric5(
|
|
9252
|
-
averageTurnPassRate: roundMetric5(
|
|
9253
|
-
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))),
|
|
9254
8332
|
flakyScenarioCount: scenarioAggregates.filter((scenario) => scenario.passRate > 0 && scenario.passRate < 1).length,
|
|
9255
8333
|
generatedRunCount: input.reports.length,
|
|
9256
|
-
reconnectCoverageRate: roundMetric5(
|
|
9257
|
-
reconnectSuccessRate: roundMetric5(
|
|
8334
|
+
reconnectCoverageRate: roundMetric5(average3(reconnectCoverageRates)),
|
|
8335
|
+
reconnectSuccessRate: roundMetric5(average3(reconnectRates)),
|
|
9258
8336
|
scenarioCount: scenarioAggregates.length,
|
|
9259
8337
|
stableScenarioCount: scenarioAggregates.filter((scenario) => scenario.passRate === 1).length,
|
|
9260
8338
|
totalPassCount,
|
|
@@ -9298,6 +8376,11 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
|
|
|
9298
8376
|
};
|
|
9299
8377
|
// src/operationsRecord.ts
|
|
9300
8378
|
import { Elysia as Elysia4 } from "elysia";
|
|
8379
|
+
import {
|
|
8380
|
+
summarizeMediaProcessorGraphReport,
|
|
8381
|
+
summarizeMediaQualityReport,
|
|
8382
|
+
summarizeMediaTransportReport
|
|
8383
|
+
} from "@absolutejs/media";
|
|
9301
8384
|
|
|
9302
8385
|
// src/audit.ts
|
|
9303
8386
|
var includes = (filter, value) => {
|
|
@@ -10928,7 +10011,7 @@ import { Elysia as Elysia3 } from "elysia";
|
|
|
10928
10011
|
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
10929
10012
|
var getString3 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
10930
10013
|
var getNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
10931
|
-
var
|
|
10014
|
+
var firstString = (payload, keys) => {
|
|
10932
10015
|
for (const key of keys) {
|
|
10933
10016
|
const value = getString3(payload[key]);
|
|
10934
10017
|
if (value) {
|
|
@@ -10937,7 +10020,7 @@ var firstString2 = (payload, keys) => {
|
|
|
10937
10020
|
}
|
|
10938
10021
|
return;
|
|
10939
10022
|
};
|
|
10940
|
-
var
|
|
10023
|
+
var firstNumber = (payload, keys) => {
|
|
10941
10024
|
for (const key of keys) {
|
|
10942
10025
|
const value = getNumber2(payload[key]);
|
|
10943
10026
|
if (value !== undefined) {
|
|
@@ -10946,20 +10029,20 @@ var firstNumber2 = (payload, keys) => {
|
|
|
10946
10029
|
}
|
|
10947
10030
|
return;
|
|
10948
10031
|
};
|
|
10949
|
-
var eventProvider = (event) =>
|
|
10032
|
+
var eventProvider = (event) => firstString(event.payload, [
|
|
10950
10033
|
"provider",
|
|
10951
10034
|
"selectedProvider",
|
|
10952
10035
|
"fallbackProvider",
|
|
10953
10036
|
"variantId"
|
|
10954
10037
|
]);
|
|
10955
|
-
var eventStatus = (event) =>
|
|
10038
|
+
var eventStatus = (event) => firstString(event.payload, [
|
|
10956
10039
|
"providerStatus",
|
|
10957
10040
|
"status",
|
|
10958
10041
|
"disposition",
|
|
10959
10042
|
"type",
|
|
10960
10043
|
"reason"
|
|
10961
10044
|
]);
|
|
10962
|
-
var eventElapsedMs = (event) =>
|
|
10045
|
+
var eventElapsedMs = (event) => firstNumber(event.payload, ["elapsedMs", "latencyMs", "durationMs"]);
|
|
10963
10046
|
var resolveSessionHref2 = (value, sessionId) => {
|
|
10964
10047
|
if (value === false) {
|
|
10965
10048
|
return;
|
|
@@ -11301,6 +10384,29 @@ var toTelephonyMediaEvent = (event) => {
|
|
|
11301
10384
|
streamId
|
|
11302
10385
|
};
|
|
11303
10386
|
};
|
|
10387
|
+
var summarizeMediaPipelineForOperationsRecord = (report) => {
|
|
10388
|
+
const quality = summarizeMediaQualityReport(report.quality);
|
|
10389
|
+
const transport = report.transport ? summarizeMediaTransportReport(report.transport) : undefined;
|
|
10390
|
+
const processorGraph = report.processorGraph ? summarizeMediaProcessorGraphReport(report.processorGraph) : undefined;
|
|
10391
|
+
const calibrationCodes = report.calibration.issues.map((issue) => issue.code);
|
|
10392
|
+
const interruptionCodes = report.interruption.issues.map((issue) => issue.code);
|
|
10393
|
+
const issueCodes = Array.from(new Set([
|
|
10394
|
+
...calibrationCodes,
|
|
10395
|
+
...quality.issueCodes,
|
|
10396
|
+
...interruptionCodes,
|
|
10397
|
+
...processorGraph?.issueCodes ?? []
|
|
10398
|
+
]));
|
|
10399
|
+
return {
|
|
10400
|
+
frames: report.frames,
|
|
10401
|
+
issueCodes,
|
|
10402
|
+
jitterMs: report.quality.jitterMs,
|
|
10403
|
+
processorGraphStatus: processorGraph?.status,
|
|
10404
|
+
qualityStatus: quality.status,
|
|
10405
|
+
status: report.status,
|
|
10406
|
+
surface: report.surface,
|
|
10407
|
+
transportStatus: transport?.status
|
|
10408
|
+
};
|
|
10409
|
+
};
|
|
11304
10410
|
var summarizeTelephonyMedia = (events) => {
|
|
11305
10411
|
const mediaEvents = events.map(toTelephonyMediaEvent).filter((event) => event !== undefined);
|
|
11306
10412
|
const eventNames = mediaEvents.map((event) => event.event.toLowerCase());
|
|
@@ -11467,6 +10573,7 @@ var buildVoiceOperationsRecord = async (options) => {
|
|
|
11467
10573
|
tasks,
|
|
11468
10574
|
total: tasks.length
|
|
11469
10575
|
} : undefined,
|
|
10576
|
+
mediaPipeline: options.mediaPipeline ? summarizeMediaPipelineForOperationsRecord(options.mediaPipeline) : undefined,
|
|
11470
10577
|
telephonyMedia: summarizeTelephonyMedia(traceEvents),
|
|
11471
10578
|
timeline: timelineSession?.events ?? [],
|
|
11472
10579
|
tools: traceEvents.filter((event) => event.type === "agent.tool").map(toTool),
|
|
@@ -11684,15 +10791,18 @@ var buildVoiceFailureReplay = (record, options = {}) => {
|
|
|
11684
10791
|
const recovery = step.status === "fallback" ? `recovered through ${step.fallbackProvider ?? step.selectedProvider ?? "fallback"}` : step.status === "degraded" ? `degraded to ${step.fallbackProvider ?? step.selectedProvider ?? "fallback"}` : "failed before recovery";
|
|
11685
10792
|
return `${provider} ${recovery}${step.reason ? `: ${step.reason}` : ""}`;
|
|
11686
10793
|
});
|
|
10794
|
+
const pipelineIssueCodes = record.mediaPipeline?.issueCodes ?? [];
|
|
11687
10795
|
const mediaIssues = [
|
|
11688
10796
|
...mediaSteps.map((step) => step.issue).filter((issue) => typeof issue === "string"),
|
|
11689
10797
|
record.telephonyMedia.total > 0 && record.telephonyMedia.inbound === 0 ? "Carrier stream has no inbound media evidence." : undefined,
|
|
11690
|
-
record.telephonyMedia.total > 0 && record.telephonyMedia.outbound === 0 ? "Carrier stream has no outbound assistant media/control evidence." : undefined
|
|
10798
|
+
record.telephonyMedia.total > 0 && record.telephonyMedia.outbound === 0 ? "Carrier stream has no outbound assistant media/control evidence." : undefined,
|
|
10799
|
+
...pipelineIssueCodes.map((code) => `Media pipeline issue: ${code}.`)
|
|
11691
10800
|
].filter((issue) => typeof issue === "string");
|
|
11692
10801
|
const userHeard = [
|
|
11693
10802
|
...new Set(record.transcript.flatMap((turn) => turn.assistantReplies))
|
|
11694
10803
|
];
|
|
11695
|
-
const
|
|
10804
|
+
const mediaPipelineStatus = record.mediaPipeline?.status;
|
|
10805
|
+
const status = record.providerDecisionSummary.errors > 0 || record.telephonyMedia.errors > 0 || mediaPipelineStatus === "fail" ? "failed" : record.providerDecisionSummary.degraded > 0 || mediaPipelineStatus === "warn" ? "degraded" : record.providerDecisionSummary.fallbacks > 0 || mediaIssues.length > 0 ? "recovered" : "healthy";
|
|
11696
10806
|
const turns = record.transcript.map((turn) => ({
|
|
11697
10807
|
...turn,
|
|
11698
10808
|
media: mediaSteps.filter((step) => record.traceEvents.some((event) => event.turnId === turn.id && event.type === "client.telephony_media" && event.at === step.at)),
|
|
@@ -11705,6 +10815,8 @@ var buildVoiceFailureReplay = (record, options = {}) => {
|
|
|
11705
10815
|
clears: record.telephonyMedia.clears,
|
|
11706
10816
|
errors: record.telephonyMedia.errors,
|
|
11707
10817
|
issues: mediaIssues,
|
|
10818
|
+
pipelineIssueCodes,
|
|
10819
|
+
pipelineStatus: record.mediaPipeline?.status,
|
|
11708
10820
|
steps: mediaSteps,
|
|
11709
10821
|
total: record.telephonyMedia.total
|
|
11710
10822
|
},
|
|
@@ -11761,6 +10873,8 @@ var renderVoiceFailureReplayMarkdown = (report) => {
|
|
|
11761
10873
|
}).join(`
|
|
11762
10874
|
`) : "- none recorded";
|
|
11763
10875
|
const issues = report.summary.issues.length ? report.summary.issues.map((issue) => `- ${issue}`).join(`
|
|
10876
|
+
`) : "- none";
|
|
10877
|
+
const pipelineCodes = report.media.pipelineIssueCodes.length ? report.media.pipelineIssueCodes.map((code) => `- ${code}`).join(`
|
|
11764
10878
|
`) : "- none";
|
|
11765
10879
|
return [
|
|
11766
10880
|
`# Voice Failure Replay: ${report.sessionId}`,
|
|
@@ -11778,6 +10892,9 @@ var renderVoiceFailureReplayMarkdown = (report) => {
|
|
|
11778
10892
|
"## Media Path",
|
|
11779
10893
|
mediaSteps,
|
|
11780
10894
|
"",
|
|
10895
|
+
`## Media Pipeline (status: ${report.media.pipelineStatus ?? "n/a"})`,
|
|
10896
|
+
pipelineCodes,
|
|
10897
|
+
"",
|
|
11781
10898
|
"## What The User Heard",
|
|
11782
10899
|
heard
|
|
11783
10900
|
].join(`
|
|
@@ -12168,7 +11285,7 @@ var assertVoiceTelephonyWebhookNormalizationEvidence = (input = {}) => {
|
|
|
12168
11285
|
return assertion;
|
|
12169
11286
|
};
|
|
12170
11287
|
var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
|
|
12171
|
-
var
|
|
11288
|
+
var firstString2 = (source, keys) => {
|
|
12172
11289
|
for (const key of keys) {
|
|
12173
11290
|
const value = source[key];
|
|
12174
11291
|
if (typeof value === "string" && value.trim()) {
|
|
@@ -12179,7 +11296,7 @@ var firstString3 = (source, keys) => {
|
|
|
12179
11296
|
}
|
|
12180
11297
|
}
|
|
12181
11298
|
};
|
|
12182
|
-
var
|
|
11299
|
+
var firstNumber2 = (source, keys) => {
|
|
12183
11300
|
for (const key of keys) {
|
|
12184
11301
|
const value = source[key];
|
|
12185
11302
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -12544,8 +11661,8 @@ var verifyVoiceTelephonyWebhook = async (input) => {
|
|
|
12544
11661
|
var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
|
|
12545
11662
|
var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
12546
11663
|
const payload = flattenPayload(input.body);
|
|
12547
|
-
const provider =
|
|
12548
|
-
const status =
|
|
11664
|
+
const provider = firstString2(payload, ["provider", "Provider"]) ?? input.provider;
|
|
11665
|
+
const status = firstString2(payload, [
|
|
12549
11666
|
"CallStatus",
|
|
12550
11667
|
"call_status",
|
|
12551
11668
|
"callStatus",
|
|
@@ -12555,7 +11672,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12555
11672
|
"event_type",
|
|
12556
11673
|
"type"
|
|
12557
11674
|
]);
|
|
12558
|
-
const durationMs =
|
|
11675
|
+
const durationMs = firstNumber2(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber2(payload, [
|
|
12559
11676
|
"CallDuration",
|
|
12560
11677
|
"call_duration",
|
|
12561
11678
|
"callDuration",
|
|
@@ -12563,21 +11680,21 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12563
11680
|
"dial_call_duration",
|
|
12564
11681
|
"duration"
|
|
12565
11682
|
]));
|
|
12566
|
-
const sipCode =
|
|
11683
|
+
const sipCode = firstNumber2(payload, [
|
|
12567
11684
|
"SipResponseCode",
|
|
12568
11685
|
"sip_response_code",
|
|
12569
11686
|
"sipCode",
|
|
12570
11687
|
"sip_code",
|
|
12571
11688
|
"hangupCauseCode"
|
|
12572
11689
|
]);
|
|
12573
|
-
const from =
|
|
12574
|
-
const to =
|
|
11690
|
+
const from = firstString2(payload, ["From", "from", "caller_id", "callerId"]);
|
|
11691
|
+
const to = firstString2(payload, [
|
|
12575
11692
|
"To",
|
|
12576
11693
|
"to",
|
|
12577
11694
|
"called_number",
|
|
12578
11695
|
"calledNumber"
|
|
12579
11696
|
]);
|
|
12580
|
-
const target =
|
|
11697
|
+
const target = firstString2(payload, [
|
|
12581
11698
|
"transferTarget",
|
|
12582
11699
|
"TransferTarget",
|
|
12583
11700
|
"target",
|
|
@@ -12585,7 +11702,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12585
11702
|
"department"
|
|
12586
11703
|
]);
|
|
12587
11704
|
return {
|
|
12588
|
-
answeredBy:
|
|
11705
|
+
answeredBy: firstString2(payload, [
|
|
12589
11706
|
"AnsweredBy",
|
|
12590
11707
|
"answered_by",
|
|
12591
11708
|
"answeredBy",
|
|
@@ -12599,7 +11716,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12599
11716
|
...payload
|
|
12600
11717
|
},
|
|
12601
11718
|
provider,
|
|
12602
|
-
reason:
|
|
11719
|
+
reason: firstString2(payload, [
|
|
12603
11720
|
"Reason",
|
|
12604
11721
|
"reason",
|
|
12605
11722
|
"HangupCause",
|
|
@@ -12615,7 +11732,7 @@ var parseVoiceTelephonyWebhookEvent = (input) => {
|
|
|
12615
11732
|
var defaultSessionId = (input) => {
|
|
12616
11733
|
const payload = flattenPayload(input.body);
|
|
12617
11734
|
const metadataSessionId = input.event.metadata?.sessionId;
|
|
12618
|
-
return
|
|
11735
|
+
return firstString2(input.query, ["sessionId", "session_id"]) ?? firstString2(payload, [
|
|
12619
11736
|
"sessionId",
|
|
12620
11737
|
"session_id",
|
|
12621
11738
|
"SessionId",
|
|
@@ -12630,7 +11747,7 @@ var defaultSessionId = (input) => {
|
|
|
12630
11747
|
};
|
|
12631
11748
|
var defaultIdempotencyKey = (input) => {
|
|
12632
11749
|
const payload = flattenPayload(input.body);
|
|
12633
|
-
const eventId =
|
|
11750
|
+
const eventId = firstString2(payload, [
|
|
12634
11751
|
"id",
|
|
12635
11752
|
"event_id",
|
|
12636
11753
|
"eventId",
|