@kognitivedev/voice-media-bridge 0.2.29
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/.turbo/turbo-build.log +2 -0
- package/CHANGELOG.md +10 -0
- package/dist/audio.d.ts +34 -0
- package/dist/audio.js +163 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/openai.d.ts +62 -0
- package/dist/openai.js +130 -0
- package/dist/profiles.d.ts +38 -0
- package/dist/profiles.js +97 -0
- package/dist/routes.d.ts +51 -0
- package/dist/routes.js +98 -0
- package/package.json +39 -0
- package/src/__tests__/media.test.ts +229 -0
- package/src/audio.ts +182 -0
- package/src/index.ts +4 -0
- package/src/openai.ts +127 -0
- package/src/profiles.ts +135 -0
- package/src/routes.ts +139 -0
- package/tsconfig.json +13 -0
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export type PhoneMediaProvider = "twilio" | "telnyx" | "sip";
|
|
2
|
+
export type PhoneMediaProfileId = "twilio-pcmu-8k" | "telnyx-pcmu-8k" | "telnyx-l16-16k" | "sip-pcmu-8k";
|
|
3
|
+
export type PhoneMediaCodecPreference = "auto" | "l16_16k" | "pcmu_8k";
|
|
4
|
+
export type PhoneMediaL16ByteOrder = "be" | "le";
|
|
5
|
+
|
|
6
|
+
export interface PhoneMediaProfile {
|
|
7
|
+
id: PhoneMediaProfileId;
|
|
8
|
+
provider: PhoneMediaProvider;
|
|
9
|
+
codec: "pcmu" | "l16";
|
|
10
|
+
carrierCodec: "PCMU" | "L16";
|
|
11
|
+
carrierSampleRate: 8000 | 16000;
|
|
12
|
+
carrierByteOrder?: PhoneMediaL16ByteOrder;
|
|
13
|
+
openAIFormat: "audio/pcmu" | "audio/pcm";
|
|
14
|
+
openAISampleRate: 8000 | 24000;
|
|
15
|
+
source: "auto" | "forced" | "fallback";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PhoneMediaFallbackState {
|
|
19
|
+
forcePcmu?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolvePhoneMediaProfileInput {
|
|
23
|
+
provider: PhoneMediaProvider;
|
|
24
|
+
requestedCodec?: PhoneMediaCodecPreference | string | null;
|
|
25
|
+
l16ByteOrder?: PhoneMediaL16ByteOrder | string | null;
|
|
26
|
+
fallbackState?: PhoneMediaFallbackState | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const TWILIO_PCMU_8K: PhoneMediaProfile = {
|
|
30
|
+
id: "twilio-pcmu-8k",
|
|
31
|
+
provider: "twilio",
|
|
32
|
+
codec: "pcmu",
|
|
33
|
+
carrierCodec: "PCMU",
|
|
34
|
+
carrierSampleRate: 8000,
|
|
35
|
+
openAIFormat: "audio/pcmu",
|
|
36
|
+
openAISampleRate: 8000,
|
|
37
|
+
source: "forced",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TELNYX_PCMU_8K: PhoneMediaProfile = {
|
|
41
|
+
id: "telnyx-pcmu-8k",
|
|
42
|
+
provider: "telnyx",
|
|
43
|
+
codec: "pcmu",
|
|
44
|
+
carrierCodec: "PCMU",
|
|
45
|
+
carrierSampleRate: 8000,
|
|
46
|
+
openAIFormat: "audio/pcmu",
|
|
47
|
+
openAISampleRate: 8000,
|
|
48
|
+
source: "forced",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const TELNYX_L16_16K: PhoneMediaProfile = {
|
|
52
|
+
id: "telnyx-l16-16k",
|
|
53
|
+
provider: "telnyx",
|
|
54
|
+
codec: "l16",
|
|
55
|
+
carrierCodec: "L16",
|
|
56
|
+
carrierSampleRate: 16000,
|
|
57
|
+
carrierByteOrder: "le",
|
|
58
|
+
openAIFormat: "audio/pcm",
|
|
59
|
+
openAISampleRate: 24000,
|
|
60
|
+
source: "auto",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const SIP_PCMU_8K: PhoneMediaProfile = {
|
|
64
|
+
id: "sip-pcmu-8k",
|
|
65
|
+
provider: "sip",
|
|
66
|
+
codec: "pcmu",
|
|
67
|
+
carrierCodec: "PCMU",
|
|
68
|
+
carrierSampleRate: 8000,
|
|
69
|
+
openAIFormat: "audio/pcmu",
|
|
70
|
+
openAISampleRate: 8000,
|
|
71
|
+
source: "forced",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function getPhoneMediaProfile(id: PhoneMediaProfileId): PhoneMediaProfile {
|
|
75
|
+
switch (id) {
|
|
76
|
+
case "twilio-pcmu-8k":
|
|
77
|
+
return { ...TWILIO_PCMU_8K };
|
|
78
|
+
case "telnyx-pcmu-8k":
|
|
79
|
+
return { ...TELNYX_PCMU_8K };
|
|
80
|
+
case "telnyx-l16-16k":
|
|
81
|
+
return { ...TELNYX_L16_16K };
|
|
82
|
+
case "sip-pcmu-8k":
|
|
83
|
+
return { ...SIP_PCMU_8K };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizePhoneMediaCodecPreference(value: unknown): PhoneMediaCodecPreference {
|
|
88
|
+
if (value === "l16_16k" || value === "pcmu_8k" || value === "auto") return value;
|
|
89
|
+
if (value === "L16" || value === "l16" || value === "telnyx-l16-16k") return "l16_16k";
|
|
90
|
+
if (value === "PCMU" || value === "pcmu" || value === "telnyx-pcmu-8k" || value === "twilio-pcmu-8k") return "pcmu_8k";
|
|
91
|
+
return "auto";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function normalizePhoneMediaL16ByteOrder(value: unknown): PhoneMediaL16ByteOrder {
|
|
95
|
+
return value === "le" || value === "little" || value === "little-endian" ? "le" : "be";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolveL16ByteOrder(value: unknown, fallback: PhoneMediaL16ByteOrder): PhoneMediaL16ByteOrder {
|
|
99
|
+
return value === undefined || value === null || value === "" ? fallback : normalizePhoneMediaL16ByteOrder(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resolvePhoneMediaProfile(input: ResolvePhoneMediaProfileInput): PhoneMediaProfile {
|
|
103
|
+
if (input.provider === "twilio") return getPhoneMediaProfile("twilio-pcmu-8k");
|
|
104
|
+
if (input.provider === "sip") return getPhoneMediaProfile("sip-pcmu-8k");
|
|
105
|
+
|
|
106
|
+
const requested = normalizePhoneMediaCodecPreference(input.requestedCodec);
|
|
107
|
+
if (input.fallbackState?.forcePcmu) {
|
|
108
|
+
return { ...getPhoneMediaProfile("telnyx-pcmu-8k"), source: "fallback" };
|
|
109
|
+
}
|
|
110
|
+
if (requested === "pcmu_8k") {
|
|
111
|
+
return { ...getPhoneMediaProfile("telnyx-pcmu-8k"), source: "forced" };
|
|
112
|
+
}
|
|
113
|
+
if (requested === "l16_16k") {
|
|
114
|
+
const profile = getPhoneMediaProfile("telnyx-l16-16k");
|
|
115
|
+
return {
|
|
116
|
+
...profile,
|
|
117
|
+
carrierByteOrder: resolveL16ByteOrder(input.l16ByteOrder, profile.carrierByteOrder ?? "le"),
|
|
118
|
+
source: "forced",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { ...getPhoneMediaProfile("telnyx-pcmu-8k"), source: "auto" };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function phoneMediaProfileMetadata(profile: PhoneMediaProfile) {
|
|
125
|
+
return {
|
|
126
|
+
id: profile.id,
|
|
127
|
+
provider: profile.provider,
|
|
128
|
+
codec: profile.carrierCodec,
|
|
129
|
+
sampleRate: profile.carrierSampleRate,
|
|
130
|
+
...(profile.carrierByteOrder ? { byteOrder: profile.carrierByteOrder } : {}),
|
|
131
|
+
openAIFormat: profile.openAIFormat,
|
|
132
|
+
openAISampleRate: profile.openAISampleRate,
|
|
133
|
+
source: profile.source,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPhoneMediaProfile,
|
|
3
|
+
phoneMediaProfileMetadata,
|
|
4
|
+
resolvePhoneMediaProfile,
|
|
5
|
+
type PhoneMediaFallbackState,
|
|
6
|
+
type PhoneMediaL16ByteOrder,
|
|
7
|
+
type PhoneMediaProfile,
|
|
8
|
+
type PhoneMediaProvider,
|
|
9
|
+
} from "./profiles";
|
|
10
|
+
|
|
11
|
+
export type PhoneAiProvider = "openai-realtime" | "xai-realtime" | "gemini-live";
|
|
12
|
+
export type PhoneAudioCodec = "pcmu" | "pcm";
|
|
13
|
+
export type PhoneAudioFormat = "audio/pcmu" | "audio/pcm";
|
|
14
|
+
|
|
15
|
+
export interface PhoneAiAudioLeg {
|
|
16
|
+
codec: PhoneAudioCodec;
|
|
17
|
+
format: PhoneAudioFormat;
|
|
18
|
+
sampleRate: 8000 | 16000 | 24000;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PhoneAudioRoute {
|
|
23
|
+
id: string;
|
|
24
|
+
carrierProvider: PhoneMediaProvider;
|
|
25
|
+
aiProvider: PhoneAiProvider;
|
|
26
|
+
carrier: PhoneMediaProfile;
|
|
27
|
+
aiInput: PhoneAiAudioLeg;
|
|
28
|
+
aiOutput: PhoneAiAudioLeg;
|
|
29
|
+
inputTranscoding: "none" | "decode-resample";
|
|
30
|
+
outputTranscoding: "none" | "resample-encode";
|
|
31
|
+
stable: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ResolvePhoneAudioRouteInput {
|
|
35
|
+
carrierProvider: PhoneMediaProvider;
|
|
36
|
+
aiProvider: PhoneAiProvider | string | null | undefined;
|
|
37
|
+
requestedCodec?: string | null;
|
|
38
|
+
l16ByteOrder?: PhoneMediaL16ByteOrder | string | null;
|
|
39
|
+
fallbackState?: PhoneMediaFallbackState | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeAiProvider(value: unknown): PhoneAiProvider {
|
|
43
|
+
return value === "xai-realtime" || value === "gemini-live" ? value : "openai-realtime";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pcmLeg(sampleRate: 16000 | 24000): PhoneAiAudioLeg {
|
|
47
|
+
return {
|
|
48
|
+
codec: "pcm",
|
|
49
|
+
format: "audio/pcm",
|
|
50
|
+
sampleRate,
|
|
51
|
+
mimeType: `audio/pcm;rate=${sampleRate}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PCMU_LEG: PhoneAiAudioLeg = {
|
|
56
|
+
codec: "pcmu",
|
|
57
|
+
format: "audio/pcmu",
|
|
58
|
+
sampleRate: 8000,
|
|
59
|
+
mimeType: "audio/pcmu",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function resolveAiLegs(aiProvider: PhoneAiProvider, carrier: PhoneMediaProfile) {
|
|
63
|
+
if (aiProvider === "gemini-live") {
|
|
64
|
+
return {
|
|
65
|
+
aiInput: pcmLeg(16000),
|
|
66
|
+
aiOutput: pcmLeg(24000),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (carrier.codec === "pcmu") {
|
|
70
|
+
return {
|
|
71
|
+
aiInput: PCMU_LEG,
|
|
72
|
+
aiOutput: PCMU_LEG,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (aiProvider === "xai-realtime") {
|
|
76
|
+
const leg = pcmLeg(carrier.carrierSampleRate === 16000 ? 16000 : 24000);
|
|
77
|
+
return {
|
|
78
|
+
aiInput: leg,
|
|
79
|
+
aiOutput: leg,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const leg = pcmLeg(24000);
|
|
83
|
+
return {
|
|
84
|
+
aiInput: leg,
|
|
85
|
+
aiOutput: leg,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createPhoneAudioRoute(carrier: PhoneMediaProfile, aiProviderInput: PhoneAiProvider | string | null | undefined): PhoneAudioRoute {
|
|
90
|
+
const aiProvider = normalizeAiProvider(aiProviderInput);
|
|
91
|
+
const { aiInput, aiOutput } = resolveAiLegs(aiProvider, carrier);
|
|
92
|
+
const inputTranscoding = carrier.codec === "pcmu" && aiInput.codec === "pcmu" && carrier.carrierSampleRate === aiInput.sampleRate
|
|
93
|
+
? "none"
|
|
94
|
+
: "decode-resample";
|
|
95
|
+
const outputTranscoding = carrier.codec === "pcmu" && aiOutput.codec === "pcmu" && carrier.carrierSampleRate === aiOutput.sampleRate
|
|
96
|
+
? "none"
|
|
97
|
+
: "resample-encode";
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id: `${carrier.id}__${aiProvider}__in-${aiInput.codec}-${aiInput.sampleRate}__out-${aiOutput.codec}-${aiOutput.sampleRate}`,
|
|
101
|
+
carrierProvider: carrier.provider,
|
|
102
|
+
aiProvider,
|
|
103
|
+
carrier,
|
|
104
|
+
aiInput,
|
|
105
|
+
aiOutput,
|
|
106
|
+
inputTranscoding,
|
|
107
|
+
outputTranscoding,
|
|
108
|
+
stable: carrier.id !== "telnyx-l16-16k",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolvePhoneAudioRoute(input: ResolvePhoneAudioRouteInput): PhoneAudioRoute {
|
|
113
|
+
const carrier = resolvePhoneMediaProfile({
|
|
114
|
+
provider: input.carrierProvider,
|
|
115
|
+
requestedCodec: input.requestedCodec,
|
|
116
|
+
l16ByteOrder: input.l16ByteOrder,
|
|
117
|
+
fallbackState: input.fallbackState,
|
|
118
|
+
});
|
|
119
|
+
return createPhoneAudioRoute(carrier, input.aiProvider);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function phoneAudioRouteMetadata(route: PhoneAudioRoute) {
|
|
123
|
+
return {
|
|
124
|
+
id: route.id,
|
|
125
|
+
carrierProvider: route.carrierProvider,
|
|
126
|
+
aiProvider: route.aiProvider,
|
|
127
|
+
carrier: phoneMediaProfileMetadata(route.carrier),
|
|
128
|
+
aiInput: route.aiInput,
|
|
129
|
+
aiOutput: route.aiOutput,
|
|
130
|
+
inputTranscoding: route.inputTranscoding,
|
|
131
|
+
outputTranscoding: route.outputTranscoding,
|
|
132
|
+
stable: route.stable,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getStableCarrierProfile(provider: PhoneMediaProvider): PhoneMediaProfile {
|
|
137
|
+
if (provider === "telnyx") return { ...getPhoneMediaProfile("telnyx-pcmu-8k"), source: "auto" };
|
|
138
|
+
return resolvePhoneMediaProfile({ provider, requestedCodec: "auto" });
|
|
139
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"incremental": false
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": ["src/__tests__"]
|
|
13
|
+
}
|