@pyai/sdk 0.2.2 → 0.2.3
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/index.d.ts +13 -0
- package/dist/index.js +96 -9
- package/package.json +1 -1
- package/src/index.ts +100 -9
package/dist/index.d.ts
CHANGED
|
@@ -420,6 +420,17 @@ export interface OmniServerFrame {
|
|
|
420
420
|
event: string;
|
|
421
421
|
[k: string]: unknown;
|
|
422
422
|
}
|
|
423
|
+
/** Canonical, sanitized transcript delivered by the native Omni demux. */
|
|
424
|
+
export interface OmniTranscriptFrame extends OmniServerFrame {
|
|
425
|
+
event: "transcript";
|
|
426
|
+
role: "user" | "assistant";
|
|
427
|
+
text: string;
|
|
428
|
+
final: boolean;
|
|
429
|
+
mode: "delta" | "replace";
|
|
430
|
+
sequence?: number;
|
|
431
|
+
}
|
|
432
|
+
/** Normalize the live UTF-8 0x02 body and the documented direct-object legacy shape. */
|
|
433
|
+
export declare function normalizeOmniTranscriptBody(bytes: Uint8Array): OmniTranscriptFrame | null;
|
|
423
434
|
/** A binary agent-audio chunk delivered to {@link OmniConnectOptions.onAudio}. */
|
|
424
435
|
export type OmniAudioChunk = ArrayBuffer | ArrayBufferView | Blob;
|
|
425
436
|
export interface OmniToolDef {
|
|
@@ -539,6 +550,8 @@ export declare class OmniConnection {
|
|
|
539
550
|
private closed;
|
|
540
551
|
/** Serializes async Blob reads in {@link sendAudio} so frames stay ordered. */
|
|
541
552
|
private blobTail;
|
|
553
|
+
/** Serializes inbound Blob decoding so browser frames stay ordered. */
|
|
554
|
+
private inboundTail;
|
|
542
555
|
constructor(url: string, subprotocol: string, opts: OmniConnectOptions);
|
|
543
556
|
private handleMessage;
|
|
544
557
|
private dispatchFrame;
|
package/dist/index.js
CHANGED
|
@@ -272,6 +272,71 @@ export const OmniEvent = {
|
|
|
272
272
|
/** Server fault frame. */
|
|
273
273
|
Error: "error",
|
|
274
274
|
};
|
|
275
|
+
const OMNI_TRANSCRIPT_MAX_BYTES = 16_384;
|
|
276
|
+
const OMNI_TRANSCRIPT_MAX_CHARS = 4_000;
|
|
277
|
+
function omniTranscriptRole(value) {
|
|
278
|
+
if (value === "user" || value === "caller" || value === "human")
|
|
279
|
+
return "user";
|
|
280
|
+
if (value === "assistant" || value === "agent")
|
|
281
|
+
return "assistant";
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function omniTranscriptText(value) {
|
|
285
|
+
return typeof value === "string"
|
|
286
|
+
&& value.length > 0
|
|
287
|
+
&& value.length <= OMNI_TRANSCRIPT_MAX_CHARS
|
|
288
|
+
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)
|
|
289
|
+
? value
|
|
290
|
+
: null;
|
|
291
|
+
}
|
|
292
|
+
/** Normalize the live UTF-8 0x02 body and the documented direct-object legacy shape. */
|
|
293
|
+
export function normalizeOmniTranscriptBody(bytes) {
|
|
294
|
+
if (bytes.byteLength === 0 || bytes.byteLength > OMNI_TRANSCRIPT_MAX_BYTES)
|
|
295
|
+
return null;
|
|
296
|
+
let decoded;
|
|
297
|
+
try {
|
|
298
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (!decoded.trimStart().startsWith("{")) {
|
|
304
|
+
const text = omniTranscriptText(decoded);
|
|
305
|
+
return text
|
|
306
|
+
? { event: "transcript", role: "user", text, final: false, mode: "delta" }
|
|
307
|
+
: null;
|
|
308
|
+
}
|
|
309
|
+
let value;
|
|
310
|
+
try {
|
|
311
|
+
value = JSON.parse(decoded);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
317
|
+
return null;
|
|
318
|
+
const payload = value;
|
|
319
|
+
const role = omniTranscriptRole(payload.role ?? payload.speaker);
|
|
320
|
+
const mode = typeof payload.delta === "string" ? "delta" : "replace";
|
|
321
|
+
const text = omniTranscriptText(mode === "delta" ? payload.delta : typeof payload.text === "string" ? payload.text : payload.transcript);
|
|
322
|
+
if (!role || !text)
|
|
323
|
+
return null;
|
|
324
|
+
if (payload.final !== undefined && typeof payload.final !== "boolean")
|
|
325
|
+
return null;
|
|
326
|
+
if (payload.sequence !== undefined
|
|
327
|
+
&& (typeof payload.sequence !== "number"
|
|
328
|
+
|| !Number.isSafeInteger(payload.sequence)
|
|
329
|
+
|| payload.sequence < 0))
|
|
330
|
+
return null;
|
|
331
|
+
return {
|
|
332
|
+
event: "transcript",
|
|
333
|
+
role,
|
|
334
|
+
text,
|
|
335
|
+
final: payload.final === true,
|
|
336
|
+
mode,
|
|
337
|
+
...(payload.sequence === undefined ? {} : { sequence: payload.sequence }),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
275
340
|
/** Extract a byte view from a binary WS frame (Buffer / ArrayBuffer / typed
|
|
276
341
|
* array). Returns null for a Blob or unknown (can't be read synchronously). */
|
|
277
342
|
function omniToBytes(data) {
|
|
@@ -324,6 +389,8 @@ export class OmniConnection {
|
|
|
324
389
|
closed = false;
|
|
325
390
|
/** Serializes async Blob reads in {@link sendAudio} so frames stay ordered. */
|
|
326
391
|
blobTail = Promise.resolve();
|
|
392
|
+
/** Serializes inbound Blob decoding so browser frames stay ordered. */
|
|
393
|
+
inboundTail = Promise.resolve();
|
|
327
394
|
constructor(url, subprotocol, opts) {
|
|
328
395
|
this.opts = opts;
|
|
329
396
|
const WS = opts.webSocket ?? globalThis.WebSocket;
|
|
@@ -342,22 +409,34 @@ export class OmniConnection {
|
|
|
342
409
|
}
|
|
343
410
|
opts.onOpen?.();
|
|
344
411
|
};
|
|
345
|
-
this.ws.onmessage = (ev) =>
|
|
412
|
+
this.ws.onmessage = (ev) => {
|
|
413
|
+
const reportDecodeError = (error) => {
|
|
414
|
+
opts.onError?.(error instanceof Error ? error : new Error("Could not decode Omni frame"));
|
|
415
|
+
};
|
|
416
|
+
if (typeof Blob !== "undefined" && ev.data instanceof Blob) {
|
|
417
|
+
this.inboundTail = this.inboundTail.then(() => this.handleMessage(ev.data)).catch(reportDecodeError);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
void this.handleMessage(ev.data).catch(reportDecodeError);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
346
423
|
this.ws.onerror = (ev) => opts.onError?.(ev instanceof Error ? ev : new Error("WebSocket error"));
|
|
347
424
|
this.ws.onclose = (ev) => {
|
|
348
425
|
this.closed = true;
|
|
349
426
|
opts.onClose?.(ev.code, ev.reason);
|
|
350
427
|
};
|
|
351
428
|
}
|
|
352
|
-
handleMessage(data) {
|
|
429
|
+
async handleMessage(data) {
|
|
353
430
|
// Server → client binary frames are TYPE-TAGGED by their first byte:
|
|
354
|
-
// 0x01 = agent audio (PCM16) · 0x02 = transcript
|
|
431
|
+
// 0x01 = agent audio (PCM16) · 0x02 = transcript UTF-8 · 0x03 = control JSON.
|
|
355
432
|
// (Treating every binary frame as audio, the old behavior, plays the
|
|
356
433
|
// 0x03/0x02 frames as a glitch and drops every event/transcript.)
|
|
357
434
|
if (typeof data !== "string") {
|
|
358
|
-
const bytes =
|
|
435
|
+
const bytes = typeof Blob !== "undefined" && data instanceof Blob
|
|
436
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
437
|
+
: omniToBytes(data);
|
|
359
438
|
if (!bytes) {
|
|
360
|
-
this.opts.
|
|
439
|
+
this.opts.onError?.(new Error("Unsupported Omni binary frame"));
|
|
361
440
|
return;
|
|
362
441
|
}
|
|
363
442
|
const tag = bytes[0];
|
|
@@ -365,18 +444,26 @@ export class OmniConnection {
|
|
|
365
444
|
this.opts.onAudio?.(bytes.slice(1)); // copy → aligned PCM16
|
|
366
445
|
return;
|
|
367
446
|
}
|
|
368
|
-
if (tag === 0x02
|
|
447
|
+
if (tag === 0x02) {
|
|
448
|
+
const transcript = normalizeOmniTranscriptBody(bytes.subarray(1));
|
|
449
|
+
if (transcript)
|
|
450
|
+
this.dispatchFrame(transcript);
|
|
451
|
+
else
|
|
452
|
+
this.opts.onError?.(new Error("Unparseable Omni transcript frame"));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (tag === 0x03) {
|
|
369
456
|
try {
|
|
370
457
|
const parsed = JSON.parse(new TextDecoder().decode(bytes.subarray(1)));
|
|
371
|
-
|
|
372
|
-
this.dispatchFrame(tag === 0x02 ? { ...parsed, event: "transcript" } : parsed);
|
|
458
|
+
this.dispatchFrame(parsed);
|
|
373
459
|
}
|
|
374
460
|
catch {
|
|
375
461
|
this.opts.onError?.(new Error("Unparseable Omni binary frame"));
|
|
376
462
|
}
|
|
377
463
|
return;
|
|
378
464
|
}
|
|
379
|
-
|
|
465
|
+
const tagName = tag === undefined ? "empty" : `0x${tag.toString(16).padStart(2, "0")}`;
|
|
466
|
+
this.opts.onError?.(new Error(`Ignored unknown Omni binary frame tag ${tagName}`));
|
|
380
467
|
return;
|
|
381
468
|
}
|
|
382
469
|
// Text frame (e.g. a server-side broker relay).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyai/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Official TypeScript/JavaScript SDK for PyAI, speech-to-text (Hear), text-to-speech (Speak), realtime voice agents (Omni), and call compliance (Trace).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/index.ts
CHANGED
|
@@ -586,6 +586,78 @@ export interface OmniServerFrame {
|
|
|
586
586
|
[k: string]: unknown;
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
/** Canonical, sanitized transcript delivered by the native Omni demux. */
|
|
590
|
+
export interface OmniTranscriptFrame extends OmniServerFrame {
|
|
591
|
+
event: "transcript";
|
|
592
|
+
role: "user" | "assistant";
|
|
593
|
+
text: string;
|
|
594
|
+
final: boolean;
|
|
595
|
+
mode: "delta" | "replace";
|
|
596
|
+
sequence?: number;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const OMNI_TRANSCRIPT_MAX_BYTES = 16_384;
|
|
600
|
+
const OMNI_TRANSCRIPT_MAX_CHARS = 4_000;
|
|
601
|
+
|
|
602
|
+
function omniTranscriptRole(value: unknown): "user" | "assistant" | null {
|
|
603
|
+
if (value === "user" || value === "caller" || value === "human") return "user";
|
|
604
|
+
if (value === "assistant" || value === "agent") return "assistant";
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function omniTranscriptText(value: unknown): string | null {
|
|
609
|
+
return typeof value === "string"
|
|
610
|
+
&& value.length > 0
|
|
611
|
+
&& value.length <= OMNI_TRANSCRIPT_MAX_CHARS
|
|
612
|
+
&& !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)
|
|
613
|
+
? value
|
|
614
|
+
: null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Normalize the live UTF-8 0x02 body and the documented direct-object legacy shape. */
|
|
618
|
+
export function normalizeOmniTranscriptBody(bytes: Uint8Array): OmniTranscriptFrame | null {
|
|
619
|
+
if (bytes.byteLength === 0 || bytes.byteLength > OMNI_TRANSCRIPT_MAX_BYTES) return null;
|
|
620
|
+
let decoded: string;
|
|
621
|
+
try {
|
|
622
|
+
decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
623
|
+
} catch {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
if (!decoded.trimStart().startsWith("{")) {
|
|
627
|
+
const text = omniTranscriptText(decoded);
|
|
628
|
+
return text
|
|
629
|
+
? { event: "transcript", role: "user", text, final: false, mode: "delta" }
|
|
630
|
+
: null;
|
|
631
|
+
}
|
|
632
|
+
let value: unknown;
|
|
633
|
+
try {
|
|
634
|
+
value = JSON.parse(decoded);
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
639
|
+
const payload = value as Record<string, unknown>;
|
|
640
|
+
const role = omniTranscriptRole(payload.role ?? payload.speaker);
|
|
641
|
+
const mode = typeof payload.delta === "string" ? "delta" : "replace";
|
|
642
|
+
const text = omniTranscriptText(
|
|
643
|
+
mode === "delta" ? payload.delta : typeof payload.text === "string" ? payload.text : payload.transcript,
|
|
644
|
+
);
|
|
645
|
+
if (!role || !text) return null;
|
|
646
|
+
if (payload.final !== undefined && typeof payload.final !== "boolean") return null;
|
|
647
|
+
if (payload.sequence !== undefined
|
|
648
|
+
&& (typeof payload.sequence !== "number"
|
|
649
|
+
|| !Number.isSafeInteger(payload.sequence)
|
|
650
|
+
|| payload.sequence < 0)) return null;
|
|
651
|
+
return {
|
|
652
|
+
event: "transcript",
|
|
653
|
+
role,
|
|
654
|
+
text,
|
|
655
|
+
final: payload.final === true,
|
|
656
|
+
mode,
|
|
657
|
+
...(payload.sequence === undefined ? {} : { sequence: payload.sequence as number }),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
589
661
|
/** A binary agent-audio chunk delivered to {@link OmniConnectOptions.onAudio}. */
|
|
590
662
|
export type OmniAudioChunk = ArrayBuffer | ArrayBufferView | Blob;
|
|
591
663
|
|
|
@@ -741,6 +813,8 @@ export class OmniConnection {
|
|
|
741
813
|
private closed = false;
|
|
742
814
|
/** Serializes async Blob reads in {@link sendAudio} so frames stay ordered. */
|
|
743
815
|
private blobTail: Promise<void> = Promise.resolve();
|
|
816
|
+
/** Serializes inbound Blob decoding so browser frames stay ordered. */
|
|
817
|
+
private inboundTail: Promise<void> = Promise.resolve();
|
|
744
818
|
|
|
745
819
|
constructor(url: string, subprotocol: string, opts: OmniConnectOptions) {
|
|
746
820
|
this.opts = opts;
|
|
@@ -761,7 +835,16 @@ export class OmniConnection {
|
|
|
761
835
|
}
|
|
762
836
|
opts.onOpen?.();
|
|
763
837
|
};
|
|
764
|
-
this.ws.onmessage = (ev) =>
|
|
838
|
+
this.ws.onmessage = (ev) => {
|
|
839
|
+
const reportDecodeError = (error: unknown) => {
|
|
840
|
+
opts.onError?.(error instanceof Error ? error : new Error("Could not decode Omni frame"));
|
|
841
|
+
};
|
|
842
|
+
if (typeof Blob !== "undefined" && ev.data instanceof Blob) {
|
|
843
|
+
this.inboundTail = this.inboundTail.then(() => this.handleMessage(ev.data)).catch(reportDecodeError);
|
|
844
|
+
} else {
|
|
845
|
+
void this.handleMessage(ev.data).catch(reportDecodeError);
|
|
846
|
+
}
|
|
847
|
+
};
|
|
765
848
|
this.ws.onerror = (ev) => opts.onError?.(ev instanceof Error ? ev : new Error("WebSocket error"));
|
|
766
849
|
this.ws.onclose = (ev) => {
|
|
767
850
|
this.closed = true;
|
|
@@ -769,15 +852,17 @@ export class OmniConnection {
|
|
|
769
852
|
};
|
|
770
853
|
}
|
|
771
854
|
|
|
772
|
-
private handleMessage(data: unknown): void {
|
|
855
|
+
private async handleMessage(data: unknown): Promise<void> {
|
|
773
856
|
// Server → client binary frames are TYPE-TAGGED by their first byte:
|
|
774
|
-
// 0x01 = agent audio (PCM16) · 0x02 = transcript
|
|
857
|
+
// 0x01 = agent audio (PCM16) · 0x02 = transcript UTF-8 · 0x03 = control JSON.
|
|
775
858
|
// (Treating every binary frame as audio, the old behavior, plays the
|
|
776
859
|
// 0x03/0x02 frames as a glitch and drops every event/transcript.)
|
|
777
860
|
if (typeof data !== "string") {
|
|
778
|
-
const bytes =
|
|
861
|
+
const bytes = typeof Blob !== "undefined" && data instanceof Blob
|
|
862
|
+
? new Uint8Array(await data.arrayBuffer())
|
|
863
|
+
: omniToBytes(data);
|
|
779
864
|
if (!bytes) {
|
|
780
|
-
this.opts.
|
|
865
|
+
this.opts.onError?.(new Error("Unsupported Omni binary frame"));
|
|
781
866
|
return;
|
|
782
867
|
}
|
|
783
868
|
const tag = bytes[0];
|
|
@@ -785,17 +870,23 @@ export class OmniConnection {
|
|
|
785
870
|
this.opts.onAudio?.(bytes.slice(1) as OmniAudioChunk); // copy → aligned PCM16
|
|
786
871
|
return;
|
|
787
872
|
}
|
|
788
|
-
if (tag === 0x02
|
|
873
|
+
if (tag === 0x02) {
|
|
874
|
+
const transcript = normalizeOmniTranscriptBody(bytes.subarray(1));
|
|
875
|
+
if (transcript) this.dispatchFrame(transcript);
|
|
876
|
+
else this.opts.onError?.(new Error("Unparseable Omni transcript frame"));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (tag === 0x03) {
|
|
789
880
|
try {
|
|
790
881
|
const parsed = JSON.parse(new TextDecoder().decode(bytes.subarray(1))) as OmniServerFrame;
|
|
791
|
-
|
|
792
|
-
this.dispatchFrame(tag === 0x02 ? { ...parsed, event: "transcript" } : parsed);
|
|
882
|
+
this.dispatchFrame(parsed);
|
|
793
883
|
} catch {
|
|
794
884
|
this.opts.onError?.(new Error("Unparseable Omni binary frame"));
|
|
795
885
|
}
|
|
796
886
|
return;
|
|
797
887
|
}
|
|
798
|
-
|
|
888
|
+
const tagName = tag === undefined ? "empty" : `0x${tag.toString(16).padStart(2, "0")}`;
|
|
889
|
+
this.opts.onError?.(new Error(`Ignored unknown Omni binary frame tag ${tagName}`));
|
|
799
890
|
return;
|
|
800
891
|
}
|
|
801
892
|
// Text frame (e.g. a server-side broker relay).
|