Compare commits

...

11 Commits

Author SHA1 Message Date
Hoan Luu Huu
0a7bbaac52 Merge branch 'main' into feat/elevenlabs_stt 2025-12-01 15:53:33 +07:00
Hoan Luu Huu
c33eb46ce0 soundhound speech credential support audio endpoint (#582)
* soubound speech credential support audio endpoint

* wip
2025-11-28 21:48:13 -05:00
Hoan Luu Huu
83fe40227b Merge branch 'main' into feat/elevenlabs_stt 2025-11-19 11:32:50 +07:00
Hoan Luu Huu
f003c158dc fix outbound call routing race condition show default lcr route set that user cannot delete (#577) 2025-11-18 07:52:28 -05:00
Hoan HL
51f50aa261 wip 2025-11-18 17:21:34 +07:00
Hoan HL
50a31a0644 wip 2025-11-18 16:08:53 +07:00
Hoan HL
12104df9b1 support elevenlabs stt 2025-11-18 16:07:22 +07:00
Hoan HL
51bb871350 Merge branch 'main' of https://github.com/jambonz/jambonz-webapp 2025-11-18 13:48:46 +07:00
Sam Machin
b1ddaf230d require IP auth trunk to have either inbound or outbound carrier (#579)
also cleaned up wordig to be consistent `IP Trunk` not `Static IP Whitelist`
2025-11-10 10:29:45 -05:00
Hoan HL
00923ea26b fix outbound call routing race condition show default lcr route set that user cannot delete 2025-11-10 10:39:28 +07:00
Hoan Luu Huu
0260b1ec8b Inbound and outbound sipgateway can be duplicated (#576) 2025-11-08 08:49:00 -05:00
5 changed files with 58 additions and 68 deletions

View File

@@ -450,6 +450,7 @@ export interface SpeechCredential {
resemble_tts_uri: null | string;
resemble_tts_use_tls: number;
api_uri: null | string;
houndify_server_uri: null | string;
}
export interface Alert {

View File

@@ -588,7 +588,6 @@ export const SpeechProviderSelection = ({
options={sttVendorOptions.filter(
(vendor) =>
vendor.value != VENDOR_WELLSAID &&
vendor.value != VENDOR_ELEVENLABS &&
vendor.value != VENDOR_WHISPER &&
vendor.value !== VENDOR_RESEMBLE &&
vendor.value !== VENDOR_CUSTOM,

View File

@@ -463,24 +463,18 @@ export const CarrierForm = ({
};
const getSipValidation = () => {
if (sipInboundGateways.length === 0 && sipOutboundGateways.length === 0) {
if (trunkType === "static_ip") {
setActiveTab("inbound");
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
} else if (trunkType === "reg") {
setActiveTab("outbound");
return "Registration trunk type requires at least one outbound gateway.";
}
}
if (trunkType === "static_ip" && sipInboundGateways.length < 1) {
setActiveTab("inbound");
return "Static IP Whitelist trunk type requires at least one inbound gateway.";
if (
trunkType === "static_ip" &&
sipInboundGateways.length === 0 &&
sipOutboundGateways.length === 0
) {
setActiveTab("general");
return "IP Trunk type requires at least one inbound or outbound gateway.";
}
if (trunkType === "reg" && sipOutboundGateways.length < 1) {
setActiveTab("outbound");
return "Registration trunk type requires at least one outbound gateway.";
return "Registration Trunk type requires at least one outbound gateway.";
}
// Validate Auth Trunk credentials
@@ -577,26 +571,6 @@ export const CarrierForm = ({
return "Each SIP gateway must have a unique IP address.";
}
}
// Check for duplicates between inbound and outbound gateways
for (let i = 0; i < sipInboundGateways.length; i++) {
const inboundGateway = sipInboundGateways[i];
const dupeInOutbound = sipOutboundGateways.find((g) => {
return (
inboundGateway.ipv4 &&
g.ipv4 === inboundGateway.ipv4 &&
g.port === inboundGateway.port
);
});
if (dupeInOutbound) {
if (refSipInboundIp.current[i]) {
refSipInboundIp.current[i].focus();
}
setActiveTab("inbound");
return "Each SIP gateway must have a unique IP address.";
}
}
};
const getSmppValidation = () => {
@@ -721,9 +695,11 @@ export const CarrierForm = ({
if (sipGatewayValidation) {
if (
sipGatewayValidation ===
"Static IP Whitelist trunk type requires at least one inbound gateway."
"IP Trunk type requires at least one inbound or outbound gateway."
) {
setSipInboundMessage(sipGatewayValidation);
setSipOutboundMessage(sipGatewayValidation);
toastError(sipGatewayValidation);
} else if (
sipGatewayValidation ===
"Auth Trunk requires both username and password credentials."
@@ -731,7 +707,7 @@ export const CarrierForm = ({
setSipInboundMessage(sipGatewayValidation);
} else if (
sipGatewayValidation ===
"Registration trunk type requires at least one outbound gateway."
"Registration Trunk type requires at least one outbound gateway."
) {
setSipOutboundMessage(sipGatewayValidation);
} else {

View File

@@ -67,9 +67,6 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
const [accountSid, setAccountSid] = useState("");
const [isActive, setIsActive] = useState(true);
const [lcrRoutes, setLcrRoutes] = useState<LcrRoute[]>([LCR_ROUTE_TEMPLATE]);
const [previousLcrRoutes, setPreviousLcrRoutes] = useState<LcrRoute[]>([
LCR_ROUTE_TEMPLATE,
]);
const [previouseLcr, setPreviousLcr] = useState<Lcr | null>();
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const [lcrForDelete, setLcrForDelete] = useState<Lcr | null>();
@@ -127,38 +124,35 @@ export const LcrForm = ({ lcrDataMap, lcrRouteDataMap }: LcrFormProps) => {
}, [lcrDataMap?.data, previouseLcr]);
useMemo(() => {
let default_lcr_route_sid = "";
if (
lcrRouteDataMap &&
lcrRouteDataMap.data &&
lcrRouteDataMap.data !== previousLcrRoutes
) {
setPreviousLcrRoutes(lcrRouteDataMap.data);
// Find default carrier
lcrRouteDataMap.data.forEach((lr) => {
lr.lcr_carrier_set_entries?.forEach((entry) => {
if (
entry.lcr_carrier_set_entry_sid ===
lcrDataMap?.data?.default_carrier_set_entry_sid
) {
// Only process when both lcrDataMap and lcrRouteDataMap are available
if (lcrRouteDataMap && lcrRouteDataMap.data && lcrDataMap?.data) {
const defaultCarrierSetEntrySid =
lcrDataMap.data.default_carrier_set_entry_sid;
// Find and store default route information
lcrRouteDataMap.data.forEach((route) => {
route.lcr_carrier_set_entries?.forEach((entry) => {
if (entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid) {
setDefaultLcrCarrier(entry.voip_carrier_sid || defaultCarrier);
setDefaultLcrCarrierSetEntrySid(
entry.lcr_carrier_set_entry_sid || null,
);
default_lcr_route_sid = entry.lcr_route_sid || "";
setDefaultLcrRoute(lr);
setDefaultLcrRoute(route);
}
});
});
}
if (lcrRouteDataMap && lcrRouteDataMap.data)
setLcrRoutes(
lcrRouteDataMap.data.filter(
(route) => route.lcr_route_sid !== default_lcr_route_sid,
),
);
}, [lcrRouteDataMap?.data]);
// Filter out routes that contain the default carrier set entry
const filteredRoutes = lcrRouteDataMap.data.filter((route) => {
return !route.lcr_carrier_set_entries?.some(
(entry) =>
entry.lcr_carrier_set_entry_sid === defaultCarrierSetEntrySid,
);
});
setLcrRoutes(filteredRoutes);
}
}, [lcrRouteDataMap?.data, lcrDataMap?.data]);
const addLcrRoutes = () => {
const newLcrRoute = LCR_ROUTE_TEMPLATE;

View File

@@ -225,6 +225,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
useState(false);
const [resembleTtsUseTls, setResembleTtsUseTls] = useState(false);
const [tmpResembleTtsUseTls, setTmpResembleTtsUseTls] = useState(false);
const [houndifyServerUri, setHoundifyServerUri] = useState("");
const handleFile = (file: File) => {
const handleError = () => {
setGoogleServiceKey(null);
@@ -294,6 +295,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
case VENDOR_DEEPGRAM:
return "Model ID";
case VENDOR_CARTESIA:
case VENDOR_ELEVENLABS:
return "TTS Model ID";
default:
return "Model";
@@ -303,6 +305,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const getSTTModelLabelByVendor = (vendor: Lowercase<Vendor>) => {
switch (vendor) {
case VENDOR_CARTESIA:
case VENDOR_ELEVENLABS:
return " STT Model ID";
case VENDOR_DEEPGRAM:
return "Model ID";
@@ -462,16 +465,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
client_id: clientId || null,
client_key: clientKey || null,
user_id: userId || null,
houndify_server_uri: houndifyServerUri || null,
}),
...(vendor === VENDOR_COBALT && {
cobalt_server_uri: cobaltServerUri || null,
}),
...((vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_WHISPER ||
...((vendor === VENDOR_WHISPER ||
vendor === VENDOR_INWORLD ||
vendor === VENDOR_RIMELABS) && {
model_id: ttsModelId || null,
}),
...(vendor === VENDOR_ELEVENLABS && {
model_id: ttsModelId || null,
stt_model_id: sttModelId || null,
}),
...(vendor === VENDOR_ELEVENLABS && {
api_uri: apiUri || null,
}),
@@ -878,6 +885,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
setUserId(credential.data.user_id);
}
if (credential?.data?.houndify_server_uri) {
setHoundifyServerUri(credential.data.houndify_server_uri);
}
if (credential?.data?.voice_engine) {
setTtsModelId(credential.data.voice_engine);
}
@@ -1065,8 +1076,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor !== VENDOR_PLAYHT &&
vendor !== VENDOR_RIMELABS &&
vendor !== VENDOR_INWORLD &&
vendor !== VENDOR_RESEMBLE &&
vendor !== VENDOR_ELEVENLABS && (
vendor !== VENDOR_RESEMBLE && (
<label htmlFor="use_for_stt" className="chk">
<input
id="use_for_stt"
@@ -1484,6 +1494,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
onChange={(e) => setUserId(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="houndify_server_uri">Audio Endpoint</label>
<input
id="houndify_server_uri"
type="text"
name="houndify_server_uri"
placeholder="Audio Endpoint (optional)"
value={houndifyServerUri}
onChange={(e) => setHoundifyServerUri(e.target.value)}
/>
</fieldset>
)}
{vendor === VENDOR_NUANCE && (
@@ -1977,6 +1996,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
)}
{(vendor == VENDOR_OPENAI ||
vendor === VENDOR_DEEPGRAM ||
vendor === VENDOR_ELEVENLABS ||
(sttCheck && vendor === VENDOR_CARTESIA)) &&
sttModels.length > 0 && (
<fieldset>