Compare commits

..

94 Commits

Author SHA1 Message Date
Dave Horton
c4a6057fc6 bump version 2024-09-04 13:31:05 +01:00
rammohan-y
174438bb01 Feat/882: default model setting for en-IN language (#888)
* feat/882: default model setting for en-IN language

* feat/882: refactored if into ||
2024-09-03 13:22:38 +01:00
Antony Jukes
4348615b75 Create Call Rest is missing target headers on outdial (#874)
* add target headers for rest create-call

* rebased

---------

Co-authored-by: ajukes <ajukes@callable.io>
2024-09-02 21:48:09 +01:00
Hoan Luu Huu
d365883bfe fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event (#886)
* fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event

* fix #883 that after kicked from conference, no long receive freeswitch CUSTOM event

* reset Esl Custom event after conference.

* update drachtio fsmrf version
2024-08-31 14:47:39 +01:00
Dave Horton
c0ab936b76 wip (#830)
* wip

* wip

* wip

* wip

* update deps

* update test to use latest freeswitch image
2024-08-29 15:23:49 -04:00
Dave Horton
600ff763fa fix #840 (#880) 2024-08-26 10:14:59 -04:00
Hoan Luu Huu
4d077e990f Fix/audio issue kick conference (#878)
* rest call session does not handle for RE-INVITE

* fixed audio is bad after kicked from conference

* fix review comment
2024-08-23 09:28:39 -04:00
RJ Burnham
eccef54b04 Add support for configuring the IP address that is advertised to the API server. (#875)
This is needed when running in fargate as ip.address() will return the wrong ip address.
2024-08-23 08:33:16 -04:00
Dave Horton
2790e6d9ad fix linting error from PR 2024-08-20 08:36:24 -04:00
rammohan-y
f95d8639be Feat/868: Use global synthesizer config properties for say verb (#869)
* feat/868: Use the properties from global config in verb for TTS

* feat/868: setting this.options to combination of cs.synthesizer.options and this.options

* feat/868: Move the logic of copying cs properties to parent class tts-task.js

* feat/868: add empty line that was removed, say.js restored to original version

* feat/868: moved _synthesizeWithSpecificVendor to tts-task.js

---------

Co-authored-by: Rammohan Yadavalli <rammohan.yadavalli@kore.com>
2024-08-20 08:31:44 -04:00
Hoan Luu Huu
fc838512b6 Fixed long amd hints make freeswitch module cannot connect the vendor (#872)
* rest call session does not handle for RE-INVITE

* fixed long amd hints make freeswitch module cannot connect the vendor
2024-08-20 07:30:32 -04:00
Dave Horton
68992bccf6 fix #866 (#867) 2024-08-16 14:54:22 -04:00
Anton Voylenko
c131fceea7 fix: misleading log on call creation (#865) 2024-08-15 09:15:08 -04:00
Hoan Luu Huu
12174359f2 fix support precache audio with tts stream (#855)
* fix support precache audio with tts stream

* update speech util
2024-08-15 08:22:00 -04:00
Hoan Luu Huu
020c84d2df rest call session does not handle for RE-INVITE (#863) 2024-08-14 07:19:00 -04:00
Hoan Luu Huu
62d71d2504 fix conference in cluster have correct direction in callInfo (#842)
* fix conference in cluster have correct direction

* update github action
2024-08-13 20:02:50 -04:00
Anton Voylenko
c594797cb0 fix: support _lccMuteStatus for conference (#853) 2024-08-13 09:57:26 -04:00
Anton Voylenko
bae96a6752 fix: do not run snake case for customer data (#861) 2024-08-13 09:45:58 -04:00
rammohan-kore
ee68575ea4 Feat/844 Sending callSid in the custom-stt start message (#848)
* https://github.com/jambonz/jambonz-feature-server/issues/844

sending callSid in options, so that the callSid is sent to stt websocket in case of custom websocket

* feat/844: checking for existance of task.cs.callSid

* feat/844: changed the condition to task.cs?.callSid
2024-08-13 09:28:19 -04:00
rammohan-kore
6d0aeff6e2 feat/859: updated verb-specifications to 0.0.76 (#860) 2024-08-13 08:56:56 -04:00
rammohan-kore
d2a5d483d0 feat/856: sending DEEPGRAM_SPEECH_MODEL_VERSION to deepgram (#858) 2024-08-12 09:34:23 -04:00
Hoan Luu Huu
d3eb106d5d clear gather timeout if imterim result received (#800)
* clear gather timeout if imterim result received

* fix to reset timeout timer if receive interrim result

* fix to reset timeout timer if receive interrim result
2024-08-08 07:50:46 -04:00
Hoan Luu Huu
689e55bdf0 support wait hook for conf:participant-action hold (#851) 2024-08-08 07:42:11 -04:00
Hoan Luu Huu
ed7e036890 support jambonz transcribe sampling rate (#847)
* support jambonz transcribe sampling rate

* fix review comment

* update verb specification version
2024-08-07 10:39:58 -04:00
Hoan Luu Huu
f90fcdf57b Feat/deepgrap tts onprem (#846)
* support deepgram tts onprem

* upodate speech utils version
2024-08-07 07:25:28 -04:00
rammohan-kore
c2a1819cbb feat/813 - notify speech-bargein-detected and dtmf-bargein-detected events (#823)
* feat/813 - notify speech-bargein-detected and dtmf-bargein-detected events

* fix for #826 race condition in say (#827)

* fix for #826 race condition in say

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update transcription-utils.js (#802)

* Check the confidence levels of a transcript  with minConfidence (#808)

* https://github.com/jambonz/jambonz-feature-server/issues/807

* feat/807: Using minConfidence from recognizer settings

* feat/807: new reason stt-min-confidence-error

* feat/807: sending stt-min-confidence instead of  stt-min-confidence-error

* feat/807: sending stt-low-confidence instead of  stt-min-confidence-error

* feat/807 - removed ? for this.data

* fix conference end is not sent when moderator leave conference (#825)

* fix conference end is not sent when moderator leave conference

* wip

* fix review comment

* feat/813: checking for playComplete before sending dtmf-bargein-detected event

* feat/813: added this.playComplete=true at the end of _killAudio method

* feat/813: removed empty line

* feat/813: removed nested if and added condition to main if

* feat/813: notifyStatus called when not playComplete

* feat/813: referring to time-series 0.2.9 version

* feat/813: generated package-lock.json

---------

Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: Vinod Dharashive <vdharashive@gmail.com>
Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2024-08-06 09:34:15 -04:00
rammohan-kore
4259a24fa0 feat/758: for google getting the language_code from evt (#843) 2024-08-06 07:45:08 -04:00
rammohan-kore
e4e37d5697 feat/836: capturing callSid for STT and TTS alerts (#838)
* feat/836: capturing callSid for STT and TTS alerts

* feat/836: corrected assignment of callSid and added target_sid at few more alerts

* update github action

---------

Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
2024-08-05 12:14:08 -04:00
Markus Frindt
b7a3c2970a Bug/fix missing arg reconnect alert (#835)
* Add url as argument to a webhook connection failure alert after reconnect error

* npm audit fix to remove 15 high vulnerabilities

---------

Co-authored-by: Markus Frindt <m.frindt@cognigy.com>
2024-07-31 09:25:31 -04:00
Hoan Luu Huu
cc33ac1d51 fix conference end is not sent when moderator leave conference (#825)
* fix conference end is not sent when moderator leave conference

* wip

* fix review comment
2024-07-30 07:32:07 -04:00
rammohan-kore
4b4807e4cf Check the confidence levels of a transcript with minConfidence (#808)
* https://github.com/jambonz/jambonz-feature-server/issues/807

* feat/807: Using minConfidence from recognizer settings

* feat/807: new reason stt-min-confidence-error

* feat/807: sending stt-min-confidence instead of  stt-min-confidence-error

* feat/807: sending stt-low-confidence instead of  stt-min-confidence-error

* feat/807 - removed ? for this.data
2024-07-25 12:22:42 -04:00
Vinod Dharashive
9a3c731389 Update transcription-utils.js (#802) 2024-07-24 15:20:26 -04:00
Dave Horton
edd8f20642 fix for #826 race condition in say (#827)
* fix for #826 race condition in say

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2024-07-24 12:56:03 -04:00
Hoan Luu Huu
ee24041cba Allow joining conference as muted (#821)
* allow entering conference as muted

* allow entering conference as muted
2024-07-20 12:31:25 -04:00
Hoan Luu Huu
83f7abcd89 Kick member out conference (#820) 2024-07-20 12:11:36 -04:00
Hoan Luu Huu
c9194168d2 support restDial.referhook (#812)
* support restDial.referhook

* support restDial.referhook

* wip
2024-07-19 10:22:29 -04:00
Hoan Luu Huu
83191487cf fix config.transcribe should not override config.transcribe.recognizer (#817) 2024-07-19 07:26:49 -04:00
Hoan Luu Huu
65ef4e6d64 fix conference in feature server cluster join, leave, end events are … (#803)
* fix conference in feature server cluster join, leave, end events are missing original data

* wip
2024-07-12 08:36:43 -06:00
Hoan Luu Huu
ddb4719220 Merge pull request #806 from jambonz/feat/fd_269
support disable/enable listen DTMF in prompt
2024-07-11 20:09:40 +07:00
Quan HL
f514a65f63 support disable/enable listen DTMF in prompt 2024-07-10 08:37:02 -06:00
Hoan Luu Huu
5ccea65b7f stt/tts label can be empty, should not assign application level label… (#804)
* stt/tts label can be empty, should not assign application level label as default value

* wip
2024-07-10 08:36:00 -06:00
Dave Horton
8672152873 fix for #765 (#785) 2024-06-28 09:05:05 -04:00
Dave Horton
425b88f930 fix: package.json & package-lock.json to reduce vulnerabilities (#792)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-UNDICI-7361667

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-06-28 09:04:25 -04:00
Dave Horton
111976bea5 bug: clear asr timer when gather resolves with timeout (#788) 2024-06-28 08:54:36 -04:00
Dave Horton
ec6d7b3f42 persistent connection for custom stt vendors in transcribe (#794) 2024-06-28 08:33:10 -04:00
Vinod Dharashive
5e1b826da4 Aws polly engine fix (#789)
* Aws polly engine fix  

engine parameter was  not able to change using synthesizer

* WIP

code correction and set default engine to Neural

* WIP

* WIP

Updated  tts-task.js

* WIP
2024-06-25 13:29:28 -04:00
Dave Horton
be9c3406c1 fix bug where play incorrectly plays again after response received (#786)
* fix bug where play incorrectly plays again after response received

* wip

* fix race condition where bot delay audio kcks off same instant we receive commands
2024-06-25 12:25:55 -04:00
Dave Horton
2f3ef1654a fix: package.json & package-lock.json to reduce vulnerabilities (#787)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-WS-7266574

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-06-24 15:07:55 -04:00
Hoan Luu Huu
0baa080a1e update getAwsAuthToken use parameters in an object (#784)
* update getAwsAuthToken use parameters in an object

* wip

* update speech utils
2024-06-15 08:11:31 -04:00
Dave Horton
f5cbd26c9f update to speech-utils with support for JAMBONES_DISABLE_AZURE_TTS_STREAMING (#776) 2024-06-14 09:31:28 -04:00
Dave Horton
d9fd82fa60 major refactor and simplification of actionHookDelay feature (#771)
* major refactor and simplification of actionHookDelay feature

* wip for #765

* wip

* testing

* wip

* added validity checks for actionHookDelay properties

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix bug where config happens before endpoint is established

* wip

* hangup and clear ws connection if nogiveuptimer expires

* wip

* wip

* wip
2024-06-14 09:24:26 -04:00
Dave Horton
76a3aa7f42 send end of utterance events if using deepgram, interim events are enabled, and utterance_end_ms option is set (#772) (#782) 2024-06-13 13:18:32 -04:00
Hoan Luu Huu
cafe149bdf fix wrong vad notification to background bargin (#781)
* fix wrong vad notification to background bargin

* wip

* wip

* reset bargeinHandled every on reset
2024-06-12 10:52:53 -04:00
Anton Voylenko
9969e39e7e set valid terminatedBy for rest call (#779) 2024-06-08 17:39:40 -04:00
Hoan Luu Huu
8eea212df2 Fix/verbio stt (#770)
* fix verbio stt normalization

* wip
2024-06-01 07:38:36 -04:00
Hoan Luu Huu
e8e356ea3a update speech util version to fix verbio cache (#766) 2024-05-31 06:42:22 -04:00
Hoan Luu Huu
c5e19bf775 support verbio speech (#757)
* support verbio speech

* wip

* wip

* wip

* update speech utils

* update verb specification
2024-05-29 07:55:46 -04:00
Hoan Luu Huu
498dd64025 support mod_vad_detect (#762)
* support mod_vad_detect

* wip

* update verb spec and drachtio fsmrf

* Update example-voicemail-greetings.json (#761)

Update voicemail english greetings

* wip

* stopvad if playdone

---------

Co-authored-by: Vinod Dharashive <vdharashive@gmail.com>
2024-05-29 07:31:59 -04:00
Dave Horton
24b6d2464b update speech-utils and fsmrf (#764) 2024-05-28 18:24:51 -04:00
Dave Horton
cd5421120f fix race condition with filler noise and also play filler noise when idle and waiting for commands (#763) 2024-05-28 12:45:29 -04:00
Hoan Luu Huu
d7c3a4a632 support mod_custom_tts (#731) 2024-05-28 12:30:25 -04:00
Hoan Luu Huu
c53ad89154 support direct call to conference (#746)
* support direct call to conference

* wip

* wip

* wip
2024-05-28 10:30:52 -04:00
Vinod Dharashive
10b98630d3 Update example-voicemail-greetings.json (#761)
Update voicemail english greetings
2024-05-27 21:13:48 -04:00
Dave Horton
d132bdb92b fix gather race condition (#759) 2024-05-22 14:03:15 -04:00
Hoan Luu Huu
6be3fd9b64 say verb should not print speech credentials in log when tts stream API is used (#756) 2024-05-21 08:38:18 -04:00
Dave Horton
844b0cb05d log endpoint uuid for cross referencing with freeswitch logs 2024-05-20 11:04:15 -04:00
Dave Horton
c0b56d4fc6 per email from microsoft, do not restart STT connection when we get a no audio event (#754) 2024-05-17 11:19:01 -04:00
Dave Horton
d27de284e7 update to drachtio-srf@4.5.35 (#750) 2024-05-09 08:32:52 -04:00
Hoan Luu Huu
5e97847a2f fix fs keep looping forever if there is no fallback TTS (#749) 2024-05-09 06:15:57 -04:00
Hoan Luu Huu
17c379df47 update stats colector version (#744) 2024-05-06 20:06:04 -04:00
Hoan Luu Huu
e7bc0b0737 fix dead lock in say verb while waiting playback-stop and say verb is killed (#742) 2024-05-05 08:12:29 -04:00
Dave Horton
dfe623e78a Fix/google race condition gather (#743)
* lint

* logging

* wip
2024-05-03 12:53:26 -04:00
Dave Horton
56b8f0623b limit utterance_end_ms to (1000,5000) per discussion with Deepgram (#740) 2024-05-02 13:19:14 -04:00
Hoan Luu Huu
7bcbab5b74 feat tts stream fallback (#736)
* feat tts stream fallback

* wip

* wip

* wip

* wip

* wip

* wip

* fix review comment
2024-05-02 08:43:41 -04:00
Hoan Luu Huu
44e6a3513d support speech aws polly by role_arn (#729)
* support speech aws polly by role_arn

* support aws stt assume role

* wip

* update speech utils version
2024-05-02 07:59:21 -04:00
Dave Horton
fad16144b9 update undici and ws (#739) 2024-05-01 14:20:43 -04:00
Dave Horton
6523a861c0 fix asr error notify wrong vendor name (#728) (#738)
Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2024-05-01 13:48:23 -04:00
Dave Horton
cff67f5e4c dial race where caller hangs up while dial is starting (#737) 2024-05-01 13:38:59 -04:00
Dave Horton
c77bd84e0e we should restart asr timer after a partial transcript (#735) 2024-04-30 14:53:08 -04:00
Dave Horton
3cd7a619ad ignore transcriptions from previous turns of conversation (#734) 2024-04-30 08:21:27 -04:00
Dave Horton
59cf02bd04 wait for session:reconnect ack to send queued msgs (#723) (#732)
Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2024-04-25 11:22:15 -04:00
Dave Horton
a18d55e9ab minor fix for leaving coach mode in conferencing 2024-04-22 12:46:34 -04:00
Dave Horton
d474b9d604 Feat/advanced conferencing features (#730)
* update drachtio-fsmrf and fixes to setCoachMode

* wip

* wip

* wip

* wip

* wip

* update gh actions
2024-04-22 11:00:05 -04:00
Dave Horton
8d2b60c284 minor 2024-04-21 09:51:05 -04:00
Dave Horton
9cf9d4f587 Fix/0.8.5 cherries (#724)
* kill play task if bot responds verbs while actionHook delay is enabled (#712)

* kill play task if bot responds verbs while actionHook delay is enabled

* fix actionHook delay continues even the bot already responded verbs

* wip

* wip

* wip

* gather is hang if listenDuringPrompt = false and say/play task throw exception (#717)

* merge fix for Support ASR TTS fallback (#713)

---------

Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
2024-04-17 11:01:21 -04:00
Dave Horton
bd002ede48 ignore google errors with error_code 0 2024-04-16 20:06:26 -04:00
Dave Horton
1a2aa91973 proper fix for precache (#721)
* proper fix for precache

* wip
2024-04-15 16:25:12 -04:00
Dave Horton
e322b7d8d3 be more cautious about pre-caching prompts; in particular, a Config verb will not give us time to precache so avoid in that scenario (#720) 2024-04-15 15:38:10 -04:00
Hoan Luu Huu
7da11df88e default DEEPGRAM_SPEECH_UTTERANCE_END_MS is 1000 (#719) 2024-04-14 19:39:07 -04:00
Hoan Luu Huu
09cf1345f6 tts span for whisper (#718)
* tts span for whisper

* support deepgram tts span

* support playht tts span

* support rimelabs tts span

* wip
2024-04-14 09:14:49 -04:00
Dave Horton
2595f527ff gather: fix bug where empty deepgram transcript saved incorrectly 2024-04-13 09:59:02 -04:00
Dave Horton
1d77c0cd20 bugfx: bargein after first when config bargein with sticky=true fails 2024-04-12 20:08:21 -04:00
Hoan Luu Huu
9eab81268b support mod_rimelabs_tts (#716)
* support mod_rimelabs_tts

* update speech utils
2024-04-12 07:28:45 -04:00
Dave Horton
ecf3d140d6 fix #714 (#715) 2024-04-10 16:23:22 -04:00
33 changed files with 3249 additions and 3673 deletions

View File

@@ -6,12 +6,17 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- run: npm ci - run: npm ci
- run: npm run jslint - run: npm run jslint
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
- run: docker pull drachtio/sipp - run: docker pull drachtio/sipp
- run: npm test - run: npm test
env: env:

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021 Drachtio Communications Services, LLC Copyright (c) 2018-2024 FirstFive8, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -21,6 +21,7 @@ Configuration is provided via environment variables:
|ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes| |ENCRYPTION_SECRET| secret for credential encryption(JWT_SECRET is deprecated) |yes|
|GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes| |GOOGLE_APPLICATION_CREDENTIALS| path to gcp service key file|yes|
|HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes| |HTTP_PORT| tcp port to listen on for API requests from jambonz-api-server|yes|
|HTTP_IP| IP Address for API requests from jambonz-api-server |no|
|JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no| |JAMBONES_GATHER_EARLY_HINTS_MATCH| if true and hints are provided, gather will opportunistically review interim transcripts if possible to reduce ASR latency |no|
|JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes| |JAMBONES_FREESWITCH| IP:port:secret for Freeswitch server (e.g. '127.0.0.1:8021:JambonzR0ck$'|yes|
|JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no| |JAMBONES_LOGLEVEL| log level for application, 'info' or 'debug'|no|

View File

@@ -9,7 +9,112 @@
"can't take your call", "can't take your call",
"will get back to you", "will get back to you",
"I'll get back to you", "I'll get back to you",
"we are unable" "we are unable",
"Unable to take your call now",
"I'll reply soon",
"I'll call back",
"I'll reach out to you as soon as possible",
"Leave a message",
"Away from phone",
"Not available now",
"I'll return call",
"On another call",
"Currently on another call",
"I will return call later",
"Busy please leave message",
"Message will be returned promptly",
"Currently unavailable to answer",
"Planning to return your call soon",
"Apologies for missing your call",
"Not by the phone at the moment",
"Expecting to return your call",
"Currently not accessible",
"Intend to call back",
"Appreciate your patience!",
"Engaged in another conversation",
"I Will respond promptly",
"Kindly leave a message",
"Currently occupied leave a message",
"Unfortunately unable to answer right now",
"Occupied at the moment",
"Not present leave a message",
"Regrettably unavailable kindly leave a message",
"Will ensure a prompt response to your message",
"Currently engaged",
"Will return your call at the earliest opportunity",
"Your message will receive my prompt attention",
"I'll respond as soon as I can",
"Your message is important please leave it after the beep",
"Away from the phone at the moment",
"Unable to answer right now",
"Engaged in another task",
"Not by the phone presently",
"I'll respond at my earliest convenience",
"Away from the phone momentarily",
"I'll return your call shortly",
"Currently not able to answer",
"Your message is important please leave it after the tone",
"I'm unable to take your call right now",
"Please leave your message for me",
"I'll get back to you soon",
"Your call has been missed",
"Please leave a detailed message for me to respond to",
"Leave a message I'll make sure to respond",
"Feel free to leave a message",
"Your call is important to me",
"I'll get back to you shortly",
"Your message will be attended to promptly",
"Not available at the moment",
"I'll be sure to get back to you",
"I'll call you back soon",
"I'll ensure a prompt response",
"Sorry for the inconvenience",
"I'll return your call",
"I'll make sure to get back to you",
"I'll call you back shortly",
"I'll return your call as soon as possible",
"Apologies for the inconvenience leave your message",
"Your call is appreciated",
"I'm unavailable to answer",
"I'm currently away",
"I'll return your call as soon as I can",
"I'm away from the phone",
"I'm currently unavailable to take your call",
"Sorry for missing your call",
"I'll ensure it receives my immediate attention",
"I'm away from the phone momentarily",
"I'll reach out to you shortly",
"Apologies for the inconvenience",
"Currently occupied",
"Unable to answer your call at the moment",
"I'll make sure to follow up with you",
"Sorry for not being available",
"I'll reach out to you as soon as I can",
"I'm currently engaged",
"I'm currently busy",
"I'm currently unavailable",
"I'll respond to you at my earliest convenience",
"Your message is appreciated",
"I'll get back to you promptly",
"I'll get back to you without delay",
"Currently away from the phone",
"I'll return your call at my earliest opportunity",
"Sorry for the missed call",
"I'll make sure to address your concerns",
"Please provide your details for a callback",
"I'll make every effort to respond promptly",
"I'll ensure it's attended to promptly",
"Away from the phone temporarily",
"I'll get back to you as soon as I return",
"Currently not in a position to answer your call",
"Your call cannot be answered at the moment",
"I'll ensure to respond as soon as I'm able",
"Your call is important please leave a message",
"Unable to answer right now please leave your message",
"Currently not accessible intending to return your call",
"I'll respond promptly to your message",
"leave a memo",
"please leave a memo"
], ],
"es-ES": [ "es-ES": [
"le pasamos la llamada", "le pasamos la llamada",

View File

@@ -73,6 +73,7 @@ const JAMBONES_LOGLEVEL = process.env.JAMBONES_LOGLEVEL || 'info';
const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT; const JAMBONES_INJECT_CONTENT = process.env.JAMBONES_INJECT_CONTENT;
const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000; const PORT = parseInt(process.env.HTTP_PORT, 10) || 3000;
const HTTP_IP = process.env.HTTP_IP;
const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10); const HTTP_PORT_MAX = parseInt(process.env.HTTP_PORT_MAX, 10);
const K8S = process.env.K8S; const K8S = process.env.K8S;
@@ -170,6 +171,7 @@ module.exports = {
JAMBONES_CLUSTER_ID, JAMBONES_CLUSTER_ID,
PORT, PORT,
HTTP_PORT_MAX, HTTP_PORT_MAX,
HTTP_IP,
K8S, K8S,
K8S_SBC_SIP_SERVICE_NAME, K8S_SBC_SIP_SERVICE_NAME,
JAMBONES_SUBNET, JAMBONES_SUBNET,

View File

@@ -30,6 +30,20 @@ const appsMap = {
} }
] ]
}] }]
},
conference: {
// Dummy hook to follow later feature server logic.
call_hook: {
url: 'https://jambonz.org',
method: 'GET'
},
account_sid: '',
app_json: [{
verb: 'conference',
name: '',
beep: false,
startConferenceOnEnter: true
}]
} }
}; };
@@ -38,6 +52,7 @@ const createJambonzApp = (type, {account_sid, name, caller_id}) => {
app.account_sid = account_sid; app.account_sid = account_sid;
switch (type) { switch (type) {
case 'queue': case 'queue':
case 'conference':
app.app_json[0].name = name; app.app_json[0].name = name;
break; break;
case 'user': case 'user':

View File

@@ -97,7 +97,8 @@ router.post('/',
'X-Trace-ID': rootSpan.traceId, 'X-Trace-ID': rootSpan.traceId,
...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}), ...(req.body?.application_sid && {'X-Application-Sid': req.body.application_sid}),
...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}), ...(restDial.fromHost && {'X-Preferred-From-Host': restDial.fromHost}),
...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}) ...(record_all_calls && {'X-Record-All-Calls': recordOutputFormat}),
...target.headers
}; };
switch (target.type) { switch (target.type) {
@@ -218,7 +219,7 @@ router.post('/',
} }
if (!app.notifier && app.call_status_hook) { if (!app.notifier && app.call_status_hook) {
app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret); app.notifier = new HttpRequestor(logger, account.account_sid, app.call_status_hook, account.webhook_secret);
logger.debug({call_hook: app.call_hook}, 'creating http client for call status hook'); logger.debug({call_status_hook: app.call_status_hook}, 'creating http client for call status hook');
} }
else if (!app.notifier) { else if (!app.notifier) {
logger.debug('creating null call status hook'); logger.debug('creating null call status hook');

View File

@@ -75,13 +75,19 @@ module.exports = function(srf, logger) {
req.locals.application_sid = application_sid; req.locals.application_sid = application_sid;
} }
// check for call to queue // check for call to queue
if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) { else if (uri.user?.startsWith('queue-') && req.locals.originatingUser && clientDb?.allow_direct_queue_calling) {
const queue_name = uri.user.match(/queue-(.*)/)[1]; const queue_name = uri.user.match(/queue-(.*)/)[1];
logger.debug(`got Queue from Request URI header: ${queue_name}`); logger.debug(`got Queue from Request URI header: ${queue_name}`);
req.locals.queue_name = queue_name; req.locals.queue_name = queue_name;
} }
// check for call to conference
else if (uri.user?.startsWith('conference-') && req.locals.originatingUser && clientDb?.allow_direct_app_calling) {
const conference_id = uri.user.match(/conference-(.*)/)[1];
logger.debug(`got Conference from Request URI header: ${conference_id}`);
req.locals.conference_id = conference_id;
}
// check for call to registered user // check for call to registered user
if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) { else if (!JAMBONES_DISABLE_DIRECT_P2P_CALL && req.locals.originatingUser && clientDb?.allow_direct_user_calling) {
const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser); const arr = /^(.*)@(.*)/.exec(req.locals.originatingUser);
if (arr) { if (arr) {
const sipRealm = arr[2]; const sipRealm = arr[2];
@@ -237,6 +243,9 @@ module.exports = function(srf, logger) {
logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`); logger.debug(`calling to registered user ${req.locals.called_user}, generating dial app`);
app = createJambonzApp('user', app = createJambonzApp('user',
{account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber}); {account_sid, name: req.locals.called_user, caller_id: req.locals.callingNumber});
} else if (req.locals.conference_id) {
logger.debug(`calling to conference ${req.locals.conference_id}, generating conference app`);
app = createJambonzApp('conference', {account_sid, name: req.locals.conference_id});
} else if (req.locals.application_sid) { } else if (req.locals.application_sid) {
app = await lookupAppBySid(req.locals.application_sid); app = await lookupAppBySid(req.locals.application_sid);
} else if (req.locals.originatingUser) { } else if (req.locals.originatingUser) {
@@ -343,6 +352,17 @@ module.exports = function(srf, logger) {
direction: CallDirection.Inbound, direction: CallDirection.Inbound,
traceId: rootSpan.traceId traceId: rootSpan.traceId
}); });
// if transferred call contains callInfo, let update original data to newly created callInfo in this instance.
if (app.transferredCall && app.callInfo) {
const {direction, callerName, from, to, originatingSipIp, originatingSipTrunkName} = app.callInfo;
req.locals.callInfo.direction = direction;
req.locals.callInfo.callerName = callerName;
req.locals.callInfo.from = from;
req.locals.callInfo.to = to;
req.locals.callInfo.originatingSipIp = originatingSipIp;
req.locals.callInfo.originatingSipTrunkName = originatingSipTrunkName;
delete app.callInfo;
}
next(); next();
} catch (err) { } catch (err) {
span.end(); span.end();

View File

@@ -19,6 +19,7 @@ const { normalizeJambones } = require('@jambonz/verb-specifications');
const listTaskNames = require('../utils/summarize-tasks'); const listTaskNames = require('../utils/summarize-tasks');
const HttpRequestor = require('../utils/http-requestor'); const HttpRequestor = require('../utils/http-requestor');
const WsRequestor = require('../utils/ws-requestor'); const WsRequestor = require('../utils/ws-requestor');
const ActionHookDelayProcessor = require('../utils/action-hook-delay');
const { const {
JAMBONES_INJECT_CONTENT, JAMBONES_INJECT_CONTENT,
JAMBONES_EAGERLY_PRE_CACHE_AUDIO, JAMBONES_EAGERLY_PRE_CACHE_AUDIO,
@@ -115,6 +116,7 @@ class CallSession extends Emitter {
this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`); this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`);
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this)); this.requestor.on('handover', handover.bind(this));
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
}; };
if (!this.isConfirmCallSession) { if (!this.isConfirmCallSession) {
@@ -122,6 +124,7 @@ class CallSession extends Emitter {
this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`); this.logger.debug(`CallSession: ${this.callSid} listener count ${this.requestor.listenerCount('command')}`);
this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this)); this.requestor.on('connection-dropped', this._onWsConnectionDropped.bind(this));
this.requestor.on('handover', handover.bind(this)); this.requestor.on('handover', handover.bind(this));
this.requestor.on('reconnect-error', this._onSessionReconnectError.bind(this));
} }
} }
@@ -193,6 +196,24 @@ class CallSession extends Emitter {
this._synthesizer = synth; this._synthesizer = synth;
} }
/**
* ASR TTS fallback
*/
get hasFallbackAsr() {
return this._hasFallbackAsr || false;
}
set hasFallbackAsr(i) {
this._hasFallbackAsr = i;
}
get hasFallbackTts() {
return this._hasFallbackTts || false;
}
set hasFallbackTts(i) {
this._hasFallbackTts = i;
}
/** /**
* default vendor to use for speech synthesis if not provided in the app * default vendor to use for speech synthesis if not provided in the app
*/ */
@@ -318,6 +339,17 @@ class CallSession extends Emitter {
this.application.fallback_speech_recognizer_language = language; this.application.fallback_speech_recognizer_language = language;
} }
/**
* Vad
*/
get vad() {
return this._vad;
}
set vad(v) {
this._vad = v;
}
/** /**
* indicates whether the call currently in progress * indicates whether the call currently in progress
*/ */
@@ -488,14 +520,78 @@ class CallSession extends Emitter {
this._actionHookDelayRetries = e; this._actionHookDelayRetries = e;
} }
get actionHookDelayActions() { // Getter/setter for current tts vendor
return this._actionHookDelayActions; get currentTtsVendor() {
return this._currentTtsVendor;
} }
set actionHookDelayActions(e) { set currentTtsVendor(vendor) {
this._actionHookDelayActions = e; this._currentTtsVendor = vendor;
} }
get actionHookDelayProcessor() {
return this._actionHookDelayProcessor;
}
set actionHookDelayProperties(opts) {
if (this._actionHookDelayProcessor) {
this._actionHookDelayProcessor.stop();
if (!this._actionHookDelayProcessor.init(opts)) {
this._actionHookDelayProcessor.removeAllListeners();
this._actionHookDelayProcessor = null;
}
}
else {
try {
this._actionHookDelayProcessor = new ActionHookDelayProcessor(this.logger, opts, this, this.ep);
this._actionHookDelayProcessor.on('giveup', () => {
this.logger.info('CallSession: ActionHookDelayProcessor: giveup event - hanging up call');
this._jambonzHangup();
if (this.wakeupResolver) {
this.logger.debug('CallSession: Giveup timer expired - waking up');
this.wakeupResolver({reason: 'noResponseGiveUp'});
this.wakeupResolver = null;
}
});
} catch (err) {
this.logger.error({err}, 'CallSession: Error creating ActionHookDelayProcessor');
}
}
}
async clearOrRestoreActionHookDelayProcessor() {
if (this._actionHookDelayProcessor) {
await this._actionHookDelayProcessor.stop();
if (!this.popActionHookDelayProperties()) {
//this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - ahd settings');
//await this.clearActionHookDelayProcessor();
}
this.logger.debug('CallSession:clearOrRestoreActionHookDelayProcessor - say or play action completed');
}
}
async clearActionHookDelayProcessor() {
if (this._actionHookDelayProcessor) {
await this._actionHookDelayProcessor.stop();
this._actionHookDelayProcessor.removeAllListeners();
this._actionHookDelayProcessor = null;
}
}
stashActionHookDelayProperties() {
this._storedActionHookDelayProperties = this._actionHookDelayProcessor.properties;
}
popActionHookDelayProperties() {
if (this._storedActionHookDelayProperties) {
this._actionHookDelayProcessor.init(this._storedActionHookDelayProperties);
this._storedActionHookDelayProperties = null;
return true;
}
return false;
}
hasGlobalSttPunctuation() { hasGlobalSttPunctuation() {
return this._globalSttPunctuation !== undefined; return this._globalSttPunctuation !== undefined;
} }
@@ -702,7 +798,7 @@ class CallSession extends Emitter {
task = await this.backgroundTaskManager.newTask('bargeIn', gather); task = await this.backgroundTaskManager.newTask('bargeIn', gather);
task.sticky = autoEnable; task.sticky = autoEnable;
// listen to the bargein-done from background manager // listen to the bargein-done from background manager
this.backgroundTaskManager.once('bargeIn-done', () => { this.backgroundTaskManager.on('bargeIn-done', () => {
if (this.requestor instanceof WsRequestor) { if (this.requestor instanceof WsRequestor) {
try { try {
this.kill(true); this.kill(true);
@@ -775,7 +871,8 @@ class CallSession extends Emitter {
writeAlerts({ writeAlerts({
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor vendor,
target_sid: this.callSid
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
} }
} }
@@ -784,6 +881,7 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
accessKeyId: credential.access_key_id, accessKeyId: credential.access_key_id,
secretAccessKey: credential.secret_access_key, secretAccessKey: credential.secret_access_key,
roleArn: credential.role_arn,
region: credential.aws_region || AWS_REGION region: credential.aws_region || AWS_REGION
}; };
} }
@@ -820,6 +918,7 @@ class CallSession extends Emitter {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
api_key: credential.api_key, api_key: credential.api_key,
deepgram_stt_uri: credential.deepgram_stt_uri, deepgram_stt_uri: credential.deepgram_stt_uri,
deepgram_tts_uri: credential.deepgram_tts_uri,
deepgram_stt_use_tls: credential.deepgram_stt_use_tls deepgram_stt_use_tls: credential.deepgram_stt_use_tls
}; };
} }
@@ -862,6 +961,12 @@ class CallSession extends Emitter {
voice_engine: credential.voice_engine, voice_engine: credential.voice_engine,
options: credential.options options: credential.options
}; };
} else if ('rimelabs' === vendor) {
return {
api_key: credential.api_key,
model_id: credential.model_id,
options: credential.options
};
} else if ('assemblyai' === vendor) { } else if ('assemblyai' === vendor) {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
@@ -872,6 +977,12 @@ class CallSession extends Emitter {
api_key: credential.api_key, api_key: credential.api_key,
model_id: credential.model_id model_id: credential.model_id
}; };
} else if ('verbio' === vendor) {
return {
client_id: credential.client_id,
client_secret: credential.client_secret,
engine_version: credential.engine_version
};
} else if (vendor.startsWith('custom:')) { } else if (vendor.startsWith('custom:')) {
return { return {
speech_credential_sid: credential.speech_credential_sid, speech_credential_sid: credential.speech_credential_sid,
@@ -885,7 +996,8 @@ class CallSession extends Emitter {
writeAlerts({ writeAlerts({
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
account_sid: this.accountSid, account_sid: this.accountSid,
vendor vendor,
target_sid: this.callSid
}).catch((err) => this.logger.error({err}, 'Error writing tts alert')); }).catch((err) => this.logger.error({err}, 'Error writing tts alert'));
} }
} }
@@ -909,7 +1021,6 @@ class CallSession extends Emitter {
task.on('VerbHookSpanWaitForEnd', ({span}) => { task.on('VerbHookSpanWaitForEnd', ({span}) => {
this.verbHookSpan = span; this.verbHookSpan = span;
}); });
task.on('ActionHookDelayActionOptions', this._onActionHookDelayActions.bind(this));
try { try {
const resources = await this._evaluatePreconditions(task); const resources = await this._evaluatePreconditions(task);
let skip = false; let skip = false;
@@ -955,6 +1066,15 @@ class CallSession extends Emitter {
) { ) {
try { try {
await this._awaitCommandsOrHangup(); await this._awaitCommandsOrHangup();
//await this.clearOrRestoreActionHookDelayProcessor();
//TODO: remove filler noise code and simply create as action hook delay
if (this._isPlayingFillerNoise) {
this._isPlayingFillerNoise = false;
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
}
if (this.callGone) break; if (this.callGone) break;
} catch (err) { } catch (err) {
this.logger.info(err, 'CallSession:exec - error waiting for new commands'); this.logger.info(err, 'CallSession:exec - error waiting for new commands');
@@ -1180,9 +1300,8 @@ class CallSession extends Emitter {
this.currentTask.kill(this); this.currentTask.kill(this);
} }
this._endVerbHookSpan(); this._endVerbHookSpan();
// clear all delay action hook timeout if there is
this._clearActionHookNoResponseGiveUpTimer(); await this.clearOrRestoreActionHookDelayProcessor();
this._clearActionHookNoResponseTimer();
} }
/** /**
@@ -1241,7 +1360,7 @@ class CallSession extends Emitter {
async _lccConferenceParticipantAction(opts) { async _lccConferenceParticipantAction(opts) {
const task = this.currentTask; const task = this.currentTask;
if (!task || TaskName.Conference !== task.name || !this.isInConference) { if (!task || TaskName.Conference !== task.name || !this.isInConference) {
return this.logger.info('CallSession:_lccConferenceParticipantState - invalid cmd, call is not in conference'); return this.logger.info('CallSession:_lccConferenceParticipantAction - invalid cmd, call is not in conference');
} }
task.doConferenceParticipantAction(this, opts); task.doConferenceParticipantAction(this, opts);
} }
@@ -1493,7 +1612,7 @@ Duration=${duration} `
return this._lccTag(opts); return this._lccTag(opts);
} }
else if (opts.conferenceParticipantAction) { else if (opts.conferenceParticipantAction) {
return this._lccConferenceParticipantState(opts); return this._lccConferenceParticipantAction(opts.conferenceParticipantAction);
} }
else if (opts.dub) { else if (opts.dub) {
return this._lccDub(opts); return this._lccDub(opts);
@@ -1592,7 +1711,23 @@ Duration=${duration} `
} }
_preCacheAudio(newTasks) { _preCacheAudio(newTasks) {
for (const task of newTasks) { /**
* only precache audio for the a queued say if we have one or more non-Config verbs
* ahead of it in the queue. This is because the Config verb returns immediately
* and would not give us enough time to generate the audio. The point of precaching
* is to take advantage of getting the audio in advance of being needed, so we need
* to be confident we have some time before the say verb is executed, and the Config
* does not give us that confidence since it returns immediately.
*/
const haveQueuedNonConfig = this.tasks.findIndex((t) => t.name !== TaskName.Config) !== -1;
let tasks = haveQueuedNonConfig ? newTasks : [];
if (!haveQueuedNonConfig) {
const idxFirstNotConfig = newTasks.findIndex((t) => t.name !== TaskName.Config);
if (-1 === idxFirstNotConfig) return;
tasks = newTasks.slice(idxFirstNotConfig + 1);
}
for (const task of tasks) {
if (task.name === TaskName.Config && task.hasSynthesizer) { if (task.name === TaskName.Config && task.hasSynthesizer) {
/* if they change synthesizer settings don't try to precache */ /* if they change synthesizer settings don't try to precache */
break; break;
@@ -1664,7 +1799,24 @@ Duration=${duration} `
}, 'CallSession:_injectTasks - completed'); }, 'CallSession:_injectTasks - completed');
} }
_onCommand({msgid, command, call_sid, queueCommand, data}) { async _onSessionReconnectError(err) {
const {writeAlerts, AlertType} = this.srf.locals;
const sid = this.accountInfo.account.account_sid;
this.logger.info({err}, `_onSessionReconnectError for account ${sid}`);
try {
await writeAlerts({
alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE,
account_sid: this.accountSid,
detail: `Session:reconnect error ${err}`,
url: this.application.call_hook.url,
});
} catch (error) {
this.logger.error({error}, 'Error writing WEBHOOK_CONNECTION_FAILURE alert');
}
this._jambonzHangup();
}
async _onCommand({msgid, command, call_sid, queueCommand, data}) {
this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command'); this.logger.info({msgid, command, queueCommand, data}, 'CallSession:_onCommand - received command');
let resolution; let resolution;
switch (command) { switch (command) {
@@ -1689,9 +1841,9 @@ Duration=${duration} `
} }
resolution = {reason: 'received command, new tasks', queue: queueCommand, command}; resolution = {reason: 'received command, new tasks', queue: queueCommand, command};
resolution.command = listTaskNames(t); resolution.command = listTaskNames(t);
// clear all delay action hook timeout if there is // clear all delay action hook timeout if there is
this._clearActionHookNoResponseGiveUpTimer(); await this.clearOrRestoreActionHookDelayProcessor();
this._clearActionHookNoResponseTimer();
} }
else this._lccCallHook(data); else this._lccCallHook(data);
break; break;
@@ -1844,7 +1996,7 @@ Duration=${duration} `
}); });
//ep.cs = this; //ep.cs = this;
this.ep = ep; this.ep = ep;
this.logger.debug(`allocated endpoint ${ep.uuid}`); this.logger.info(`allocated endpoint ${ep.uuid}`);
this._configMsEndpoint(); this._configMsEndpoint();
@@ -1917,6 +2069,12 @@ Duration=${duration} `
this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg'); this.logger.error('CallSession:replaceEndpoint cannot be called without stable dlg');
return; return;
} }
// When this call kicked out from conference, session need to replace endpoint
// but this.ms might be undefined/null at this case.
this.ms = this.ms || this.getMS();
// Destroy previous ep if it's still running.
if (this.ep?.connected) this.ep.destroy();
this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp}); this.ep = await this.ms.createEndpoint({remoteSdp: this.dlg.remote.sdp});
this._configMsEndpoint(); this._configMsEndpoint();
@@ -1951,13 +2109,13 @@ Duration=${duration} `
this.rootSpan && this.rootSpan.end(); this.rootSpan && this.rootSpan.end();
// close all background tasks // close all background tasks
this.backgroundTaskManager.stopAll(); this.backgroundTaskManager.stopAll();
this._clearActionHookNoResponseGiveUpTimer(); this.clearOrRestoreActionHookDelayProcessor().catch((err) => {});
this._clearActionHookNoResponseTimer();
} }
/** /**
* called when the caller has hung up. Provided for subclasses to override * called when the caller has hung up. Provided for subclasses to override
* in order to apply logic at this point if needed. * in order to apply logic at this point if needed.
* return true if success fallback, return false if not
*/ */
_callerHungup() { _callerHungup() {
assert(false, 'subclass responsibility to override this method'); assert(false, 'subclass responsibility to override this method');
@@ -2359,6 +2517,30 @@ Duration=${duration} `
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.logger.info('_awaitCommandsOrHangup - waiting...'); this.logger.info('_awaitCommandsOrHangup - waiting...');
this.wakeupResolver = resolve; this.wakeupResolver = resolve;
if (this._actionHookDelayProcessor) {
this._actionHookDelayProcessor.start();
}
/**
* TODO: filler noise can be handled as an ActionHookDelayProcessor -
* it's just one specific scenario for action hook delay -
* remove the code below and simply implement filler noise as an action hook delay
*/
/* start filler noise if configured while we wait for new commands */
if (this.fillerNoise?.url && this.ep?.connected && !this.ep2) {
this.logger.debug('CallSession:_awaitCommandsOrHangup - playing filler noise');
this._isPlayingFillerNoise = true;
this.ep.play(this.fillerNoise.url);
this.ep.once('playback-start', (evt) => {
if (evt.file === this.fillerNoise.url && !this._isPlayingFillerNoise) {
this.logger.info('CallSession:_awaitCommandsOrHangup - filler noise started');
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
}
});
}
}); });
} }
@@ -2390,69 +2572,6 @@ Duration=${duration} `
this.verbHookSpan = null; this.verbHookSpan = null;
} }
} }
// actionHook delay actions
_onActionHookDelayActions(options) {
this._actionHookDelayRetryCount = 0;
this._startActionHookNoResponseTimer(options);
this._startActionHookNoResponseGiveUpTimer(options);
}
_startActionHookNoResponseTimer(options) {
this._clearActionHookNoResponseTimer();
if (options.noResponseTimeoutMs) {
this.logger.debug(`CallSession:_startActionHookNoResponseTimer ${options.noResponseTimeoutMs}`);
this._actionHookNoResponseTimer = setTimeout(() => {
if (this._actionHookDelayRetryCount >= options.retries) {
this._jambonzHangup();
}
const verb = options.actions[this._actionHookDelayRetryCount % options.actions.length];
// Inject verb to main stack
const t = normalizeJambones(this.logger, [verb])
.map((tdata) => makeTask(this.logger, tdata));
if (t.length) {
t[0].on('playDone', (err) => {
if (err) this.logger.error({err}, `Call-Session:exec Error delay action, play ${verb}`);
this._startActionHookNoResponseTimer(options);
});
}
this.tasks.push(...t);
if (this.wakeupResolver) {
this.wakeupResolver({reason: 'actionHook no response, applied delay actions', verb});
this.wakeupResolver = null;
}
this.logger.debug(`CallSession:_startActionHookNoResponseTimer, executing verb ${JSON.stringify(verb)}`);
this._actionHookDelayRetryCount++;
}, options.noResponseTimeoutMs);
}
}
_clearActionHookNoResponseTimer() {
if (this._actionHookNoResponseTimer) {
clearTimeout(this._actionHookNoResponseTimer);
}
this._actionHookNoResponseTimer = null;
}
_startActionHookNoResponseGiveUpTimer(options) {
this._clearActionHookNoResponseGiveUpTimer();
if (options.noResponseGiveUpTimeoutMs) {
this.logger.debug(`CallSession:_startActionHookNoResponseGiveUpTimer ${options.noResponseGiveUpTimeoutMs}`);
this._actionHookNoResponseGiveUpTimer = setTimeout(() => {
this.logger.debug('CallSession:_startActionHookNoResponseGiveUpTimer Timeout');
this._jambonzHangup();
this._actionHookNoResponseGiveUpTimer = null;
}, options.noResponseGiveUpTimeoutMs);
}
}
_clearActionHookNoResponseGiveUpTimer() {
if (this._actionHookNoResponseGiveUpTimer) {
clearTimeout(this._actionHookNoResponseGiveUpTimer);
}
this._actionHookNoResponseGiveUpTimer = null;
}
} }
module.exports = CallSession; module.exports = CallSession;

View File

@@ -71,7 +71,7 @@ class InboundCallSession extends CallSession {
} }
_jambonzHangup() { _jambonzHangup() {
this._hangup(); this.dlg?.destroy();
} }
_hangup(terminatedBy = 'jambonz') { _hangup(terminatedBy = 'jambonz') {
@@ -79,6 +79,7 @@ class InboundCallSession extends CallSession {
this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup'); this.logger.info('InboundCallSession:_hangup - race condition, dlg cleared by app hangup');
return; return;
} }
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
assert(this.dlg.connectTime); assert(this.dlg.connectTime);
const duration = moment().diff(this.dlg.connectTime, 'seconds'); const duration = moment().diff(this.dlg.connectTime, 'seconds');
this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`}); this.rootSpan.setAttributes({'call.termination': `hangup by ${terminatedBy}`});
@@ -87,7 +88,6 @@ class InboundCallSession extends CallSession {
callStatus: CallStatus.Completed, callStatus: CallStatus.Completed,
duration duration
}); });
this.logger.info(`InboundCallSession: ${terminatedBy} hung up`);
this._callReleased(); this._callReleased();
this.req.removeAllListeners('cancel'); this.req.removeAllListeners('cancel');
} }

View File

@@ -1,6 +1,9 @@
const CallSession = require('./call-session'); const CallSession = require('./call-session');
const {CallStatus} = require('../utils/constants'); const {CallStatus} = require('../utils/constants');
const moment = require('moment'); const moment = require('moment');
const {parseUri} = require('drachtio-srf');
const { normalizeJambones } = require('@jambonz/verb-specifications');
const makeTask = require('../tasks/make_task');
/** /**
* @classdesc Subclass of CallSession. This represents a CallSession that is * @classdesc Subclass of CallSession. This represents a CallSession that is
@@ -42,9 +45,63 @@ class RestCallSession extends CallSession {
setDialog(dlg) { setDialog(dlg) {
this.dlg = dlg; this.dlg = dlg;
dlg.on('destroy', this._callerHungup.bind(this)); dlg.on('destroy', this._callerHungup.bind(this));
dlg.on('refer', this._onRefer.bind(this));
dlg.on('modify', this._onReinvite.bind(this));
this.wrapDialog(dlg); this.wrapDialog(dlg);
} }
/**
* global referHook
*/
set referHook(hook) {
this._referHook = hook;
}
/**
* This is invoked when the called party sends REFER to Jambonz.
*/
async _onRefer(req, res) {
if (this._referHook) {
try {
const to = parseUri(req.getParsedHeader('Refer-To').uri);
const by = parseUri(req.getParsedHeader('Referred-By').uri);
const b3 = this.b3;
const httpHeaders = b3 && {b3};
const json = await this.requestor.request('verb:hook', this._referHook, {
...(this.callInfo.toJSON()),
refer_details: {
sip_refer_to: req.get('Refer-To'),
sip_referred_by: req.get('Referred-By'),
sip_user_agent: req.get('User-Agent'),
refer_to_user: to.scheme === 'tel' ? to.number : to.user,
referred_by_user: by.scheme === 'tel' ? by.number : by.user,
referring_call_sid: this.callSid,
referred_call_sid: null,
}
}, httpHeaders);
if (json && Array.isArray(json)) {
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info('RestCallSession:handleRefer received REFER, get new tasks');
this.replaceApplication(tasks);
if (this.wakeupResolver) {
this.wakeupResolver({reason: 'RestCallSession: referHook new taks'});
this.wakeupResolver = null;
}
}
}
res.send(202);
this.logger.info('RestCallSession:handleRefer - sent 202 Accepted');
} catch (err) {
this.logger.error({err}, 'RestCallSession:handleRefer - error while asking referHook');
res.send(err.statusCode || 501);
}
} else {
res.send(501);
}
}
/** /**
* This is invoked when the called party hangs up, in order to calculate the call duration. * This is invoked when the called party hangs up, in order to calculate the call duration.
*/ */
@@ -56,7 +113,7 @@ class RestCallSession extends CallSession {
this._hangup(); this._hangup();
} }
_hangup(terminatedBy = 'jamboz') { _hangup(terminatedBy = 'jambonz') {
if (this.restDialTask) { if (this.restDialTask) {
this.restDialTask.turnOffAmd(); this.restDialTask.turnOffAmd();
} }

View File

@@ -6,6 +6,7 @@ const { normalizeJambones } = require('@jambonz/verb-specifications');
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const bent = require('bent'); const bent = require('bent');
const assert = require('assert'); const assert = require('assert');
const HttpRequestor = require('../utils/http-requestor');
const WAIT = 'wait'; const WAIT = 'wait';
const JOIN = 'join'; const JOIN = 'join';
const START = 'start'; const START = 'start';
@@ -60,6 +61,8 @@ class Conference extends Task {
this.emitter = new Emitter(); this.emitter = new Emitter();
this.results = {}; this.results = {};
this.coaching = [];
this.speakOnlyTo = this.data.speakOnlyTo;
// transferred from another server in order to bridge to a local caller? // transferred from another server in order to bridge to a local caller?
if (this.data._ && this.data._.connectTime) { if (this.data._ && this.data._.connectTime) {
@@ -115,7 +118,9 @@ class Conference extends Task {
this.emitter.emit('kill'); this.emitter.emit('kill');
await this._doFinalMemberCheck(cs); await this._doFinalMemberCheck(cs);
if (this.ep && this.ep.connected) { if (this.ep && this.ep.connected) {
this.ep.conn.removeAllListeners('esl::event::CUSTOM::*'); // drachtio-fsmrf override esl::event::CUSTOM to conference join listerner, After finish the conference
// the application need to reset the esl::event::CUSTOM for another use on the same endpoint
this.ep.resetEslCustomEvent();
this.ep.api(`conference ${this.confName} kick ${this.memberId}`) this.ep.api(`conference ${this.confName} kick ${this.memberId}`)
.catch((err) => this.logger.info({err}, 'Error kicking participant')); .catch((err) => this.logger.info({err}, 'Error kicking participant'));
} }
@@ -132,15 +137,10 @@ class Conference extends Task {
* @param {SipDialog} dlg * @param {SipDialog} dlg
*/ */
async _init(cs, dlg) { async _init(cs, dlg) {
const friendlyName = this.confName;
const {createHash, retrieveHash} = cs.srf.locals.dbHelpers; const {createHash, retrieveHash} = cs.srf.locals.dbHelpers;
this.friendlyName = this.confName;
this.confName = `conf:${cs.accountSid}:${this.confName}`; this.confName = `conf:${cs.accountSid}:${this.confName}`;
this.statusParams = Object.assign({
conferenceSid: this.confName,
friendlyName
}, cs.callInfo);
// check if conference is in progress // check if conference is in progress
const obj = await retrieveHash(this.confName); const obj = await retrieveHash(this.confName);
if (obj) { if (obj) {
@@ -348,7 +348,9 @@ class Conference extends Task {
Object.assign(opts, {flags: { Object.assign(opts, {flags: {
...(this.endConferenceOnExit && {endconf: true}), ...(this.endConferenceOnExit && {endconf: true}),
...(this.startConferenceOnEnter && {moderator: true}), ...(this.startConferenceOnEnter && {moderator: true}),
...((this.joinMuted || this.data.speakOnlyTo) && {joinMuted: true}), //https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_conference_3965534/
// mute | Enter conference muted
...((this.joinMuted || this.speakOnlyTo) && {mute: true}),
}}); }});
/** /**
@@ -361,7 +363,7 @@ class Conference extends Task {
try { try {
const {memberId, confUuid} = await this.ep.join(this.confName, opts); const {memberId, confUuid} = await this.ep.join(this.confName, opts);
this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`); this.logger.debug({memberId, confUuid}, `Conference:_joinConference: successfully joined ${this.confName}`);
this.memberId = memberId; this.memberId = parseInt(memberId, 10);
this.confUuid = confUuid; this.confUuid = confUuid;
// set a tag for this member, if provided // set a tag for this member, if provided
@@ -395,8 +397,8 @@ class Conference extends Task {
.catch((err) => {}); .catch((err) => {});
} }
if (this.data.speakOnlyTo) { if (this.speakOnlyTo) {
this.setCoachMode(this.data.speakOnlyTo); this.setCoachMode(this.speakOnlyTo);
} }
} catch (err) { } catch (err) {
this.logger.error(err, `Failed to join conference ${this.confName}`); this.logger.error(err, `Failed to join conference ${this.confName}`);
@@ -488,7 +490,7 @@ class Conference extends Task {
} }
async doConferenceParticipantAction(cs, opts) { async doConferenceParticipantAction(cs, opts) {
const {action, tag} = opts; const {action, tag, wait_hook } = opts;
switch (action) { switch (action) {
case 'tag': case 'tag':
@@ -504,7 +506,10 @@ class Conference extends Task {
await this.clearCoachMode(); await this.clearCoachMode();
break; break;
case 'hold': case 'hold':
this.doConferenceHold(cs, {conf_hold_status: 'hold'}); this.doConferenceHold(cs, {
conf_hold_status: 'hold',
...(wait_hook && {wait_hook})
});
break; break;
case 'unhold': case 'unhold':
this.doConferenceHold(cs, {conf_hold_status: 'unhold'}); this.doConferenceHold(cs, {conf_hold_status: 'unhold'});
@@ -515,8 +520,11 @@ class Conference extends Task {
case 'unmute': case 'unmute':
this.doConferenceMute(cs, {conf_mute_status: 'unmute'}); this.doConferenceMute(cs, {conf_mute_status: 'unmute'});
break; break;
case 'kick':
this.kickMember(cs);
break;
default: default:
this.logger.info(`Conference:doConferenceParticipantState - unhandled action ${action}`); this.logger.info(`Conference:doConferenceParticipantAction - unhandled action ${action}`);
break; break;
} }
} }
@@ -538,6 +546,13 @@ class Conference extends Task {
} while (!this.killed && this.conf_hold_status === 'hold'); } while (!this.killed && this.conf_hold_status === 'hold');
} }
/**
* mute or unmute side of the call
*/
mute(callSid, doMute) {
this.doConferenceMute(this.callSession, {conf_mute_status: doMute});
}
/** /**
* Add ourselves to the waitlist of sessions to be notified once * Add ourselves to the waitlist of sessions to be notified once
* the conference starts * the conference starts
@@ -567,7 +582,7 @@ class Conference extends Task {
_normalizeHook(cs, hook) { _normalizeHook(cs, hook) {
if (typeof hook === 'object') return hook; if (typeof hook === 'object') return hook;
const url = hook.startsWith('/') ? const url = hook.startsWith('/') ?
`${cs.application.requestor.baseUrl}${hook}` : `${cs.application.requestor instanceof HttpRequestor ? cs.application.requestor.baseUrl : ''}${hook}` :
hook; hook;
return { url } ; return { url } ;
@@ -586,7 +601,7 @@ class Conference extends Task {
const response = await this.ep.api('conference', [this.confName, 'get', 'count']); const response = await this.ep.api('conference', [this.confName, 'get', 'count']);
if (response.body && confNoMatch(response.body)) this.participantCount = 0; if (response.body && confNoMatch(response.body)) this.participantCount = 0;
else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1; else if (response.body && /^\d+$/.test(response.body)) this.participantCount = parseInt(response.body) - 1;
this.logger.debug({response}, `Conference:_doFinalMemberCheck conference count ${this.participantCount}`); this.logger.debug(`Conference:_doFinalMemberCheck conference count ${this.participantCount}`);
} catch (err) { } catch (err) {
this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked'); this.logger.info({err}, 'Conference:_doFinalMemberCheck error retrieving count (we were probably kicked');
} }
@@ -596,7 +611,7 @@ class Conference extends Task {
* when we hang up as the last member, the current member count = 1 * when we hang up as the last member, the current member count = 1
* when we are kicked out of the call when the moderator leaves, the member count = 0 * when we are kicked out of the call when the moderator leaves, the member count = 0
*/ */
if (this.participantCount === 0) { if (this.participantCount === 0 || this.endConferenceOnExit) {
const {deleteKey} = cs.srf.locals.dbHelpers; const {deleteKey} = cs.srf.locals.dbHelpers;
try { try {
this._notifyConferenceEvent(cs, 'end'); this._notifyConferenceEvent(cs, 'end');
@@ -604,7 +619,8 @@ class Conference extends Task {
this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`); this.logger.info(`conf ${this.confName} deprovisioned: ${removed ? 'success' : 'failure'}`);
} }
catch (err) { catch (err) {
this.logger.error(err, `Error deprovisioning conference ${this.confName}`); this.logger.error(err, `Error deprovisioning conference ${this.confName},
might be the conference already cleaned by another moderator`);
} }
} }
} }
@@ -681,8 +697,24 @@ class Conference extends Task {
if (!params.time) params.time = (new Date()).toISOString(); if (!params.time) params.time = (new Date()).toISOString();
if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount; if (!params.members && typeof this.participantCount === 'number') params.members = this.participantCount;
cs.application.requestor cs.application.requestor
.request('verb:hook', this.statusHook, Object.assign(params, this.statusParams, httpHeaders)) .request(
.catch((err) => this.logger.info(err, 'Conference:notifyConferenceEvent - error')); 'verb:hook',
this.statusHook,
Object.assign(
params,
Object.assign(
{
conferenceSid: this.confName,
friendlyName: this.friendlyName,
},
cs.callInfo.toJSON()
),
httpHeaders
)
)
.catch((err) =>
this.logger.info(err, 'Conference:notifyConferenceEvent - error')
);
} }
} }
@@ -699,7 +731,12 @@ class Conference extends Task {
// conference event handlers // conference event handlers
_onAddMember(logger, cs, evt) { _onAddMember(logger, cs, evt) {
logger.debug({evt}, `Conference:_onAddMember - member added to conference ${this.confName}`); const memberId = parseInt(evt.getHeader('Member-ID')) ;
if (this.speakOnlyTo) {
logger.debug(`Conference:_onAddMember - member ${memberId} added to ${this.confName}, updating coaching mode`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
else logger.debug(`Conference:_onAddMember - member ${memberId} added to conference ${this.confName}`);
} }
_onDelMember(logger, cs, evt) { _onDelMember(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ; const memberId = parseInt(evt.getHeader('Member-ID')) ;
@@ -734,28 +771,64 @@ class Conference extends Task {
} }
} }
_onTag(logger, cs, evt) {
const memberId = parseInt(evt.getHeader('Member-ID')) ;
const tag = evt.getHeader('Tag') || '';
if (memberId !== this.memberId && this.speakOnlyTo) {
logger.info(`Conference:_onTag - member ${memberId} set tag to '${tag }'; updating coach mode accordingly`);
this.setCoachMode(this.speakOnlyTo).catch(() => {});
}
}
/**
* Set the conference to "coaching" mode, where the audio of the participant is only heard
* by a subset of the participants in the conference.
* We do this by first getting all of the members who do *not* have this tag, and then
* we configure this members audio to not be sent to them.
* @param {string} speakOnlyTo - tag of the members who should receive our audio
*
* N.B.: this feature requires jambonz patches to freeswitch mod_conference
*/
async setCoachMode(speakOnlyTo) { async setCoachMode(speakOnlyTo) {
this.speakOnlyTo = speakOnlyTo;
if (!this.memberId) {
this.logger.info('Conference:_setCoachMode: no member id yet');
return;
}
try { try {
const response = await this.ep.api('conference', [this.confName, 'gettag', speakOnlyTo, 'nomatch']); const members = (await this.ep.getNonMatchingConfParticipants(this.confName, speakOnlyTo))
this.logger.info(`Conference:_setCoachMode: my audio will only be sent to particpants ${response}`); .filter((m) => m !== this.memberId);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, response, 'nospeak']); if (members.length === 0) {
this.speakOnlyTo = speakOnlyTo; this.logger.info({members}, 'Conference:_setCoachMode: all participants have the tag, so all will hear me');
this.coaching = response; if (this.coaching.length) {
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching.join(','), 'clear']);
this.coaching = [];
}
}
else {
const memberList = members.join(',');
this.logger.info(`Conference:_setCoachMode: my audio will NOT be sent to ${memberList}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, memberList, 'nospeak']);
this.coaching = members;
}
} catch (err) { } catch (err) {
this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error'); this.logger.error({err, speakOnlyTo}, '_setCoachMode: Error');
} }
} }
async clearCoachMode() { async clearCoachMode() {
if (!this.memberId) return;
try { try {
if (!this.coaching) { if (this.coaching.length === 0) {
this.logger.info('Conference:_clearCoachMode: no coaching mode to clear'); this.logger.info('Conference:_clearCoachMode: no coaching mode to clear');
return;
} }
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${this.coaching}`); else {
await this.ep.api('conference', [this.confName, 'relate', this.memberId, this.coaching, 'clear']); const memberList = this.coaching.join(',');
this.logger.info(`Conference:_clearCoachMode: now sending my audio to all, including ${memberList}`);
await this.ep.api('conference', [this.confName, 'relate', this.memberId, memberList, 'clear']);
}
this.speakOnlyTo = null; this.speakOnlyTo = null;
this.coaching = null; this.coaching = [];
} catch (err) { } catch (err) {
this.logger.error({err}, '_clearCoachMode: Error'); this.logger.error({err}, '_clearCoachMode: Error');
} }
@@ -781,6 +854,16 @@ class Conference extends Task {
} }
} }
async kickMember(cs) {
assert(cs.isInConference);
try {
await this.ep.api('conference', [this.confName, 'kick', this.memberId]);
this.logger.info(`Conference:kickMember: kick ${this.memberId} out of conference ${this.confName}`);
} catch (err) {
this.logger.error({err}, `Error kicking member out of conference for ${this.memberId}`);
}
}
} }
module.exports = Conference; module.exports = Conference;

View File

@@ -15,7 +15,8 @@ class TaskConfig extends Task {
'transcribe', 'transcribe',
'fillerNoise', 'fillerNoise',
'actionHookDelayAction', 'actionHookDelayAction',
'boostAudioSignal' 'boostAudioSignal',
'vad'
].forEach((k) => this[k] = this.data[k] || {}); ].forEach((k) => this[k] = this.data[k] || {});
if ('notifyEvents' in this.data) { if ('notifyEvents' in this.data) {
@@ -70,6 +71,7 @@ class TaskConfig extends Task {
get hasListen() { return Object.keys(this.listen).length; } get hasListen() { return Object.keys(this.listen).length; }
get hasTranscribe() { return Object.keys(this.transcribe).length; } get hasTranscribe() { return Object.keys(this.transcribe).length; }
get hasDub() { return Object.keys(this.dub).length; } get hasDub() { return Object.keys(this.dub).length; }
get hasVad() { return Object.keys(this.vad).length; }
get hasFillerNoise() { return Object.keys(this.fillerNoise).length; } get hasFillerNoise() { return Object.keys(this.fillerNoise).length; }
get summary() { get summary() {
@@ -139,9 +141,8 @@ class TaskConfig extends Task {
cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default' cs.speechSynthesisVendor = this.synthesizer.vendor !== 'default'
? this.synthesizer.vendor ? this.synthesizer.vendor
: cs.speechSynthesisVendor; : cs.speechSynthesisVendor;
cs.speechSynthesisLabel = this.synthesizer.label !== 'default' cs.speechSynthesisLabel = this.synthesizer.label === 'default'
? this.synthesizer.label ? cs.speechSynthesisLabel : this.synthesizer.label;
: cs.speechSynthesisLabel;
cs.speechSynthesisLanguage = this.synthesizer.language !== 'default' cs.speechSynthesisLanguage = this.synthesizer.language !== 'default'
? this.synthesizer.language ? this.synthesizer.language
: cs.speechSynthesisLanguage; : cs.speechSynthesisLanguage;
@@ -153,15 +154,16 @@ class TaskConfig extends Task {
cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default' cs.fallbackSpeechSynthesisVendor = this.synthesizer.fallbackVendor !== 'default'
? this.synthesizer.fallbackVendor ? this.synthesizer.fallbackVendor
: cs.fallbackSpeechSynthesisVendor; : cs.fallbackSpeechSynthesisVendor;
cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel !== 'default' cs.fallbackSpeechSynthesisLabel = this.synthesizer.fallbackLabel === 'default'
? this.synthesizer.fallbackLabel ? cs.fallbackSpeechSynthesisLabel : this.synthesizer.fallbackLabel;
: cs.fallbackSpeechSynthesisLabel;
cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default' cs.fallbackSpeechSynthesisLanguage = this.synthesizer.fallbackLanguage !== 'default'
? this.synthesizer.fallbackLanguage ? this.synthesizer.fallbackLanguage
: cs.fallbackSpeechSynthesisLanguage; : cs.fallbackSpeechSynthesisLanguage;
cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default' cs.fallbackSpeechSynthesisVoice = this.synthesizer.fallbackVoice !== 'default'
? this.synthesizer.fallbackVoice ? this.synthesizer.fallbackVoice
: cs.fallbackSpeechSynthesisVoice; : cs.fallbackSpeechSynthesisVoice;
// new vendor is set, reset fallback vendor
cs.hasFallbackTts = false;
this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer'); this.logger.info({synthesizer: this.synthesizer}, 'Config: updated synthesizer');
} }
if (this.hasRecognizer) { if (this.hasRecognizer) {
@@ -169,9 +171,8 @@ class TaskConfig extends Task {
cs.speechRecognizerVendor = this.recognizer.vendor !== 'default' cs.speechRecognizerVendor = this.recognizer.vendor !== 'default'
? this.recognizer.vendor ? this.recognizer.vendor
: cs.speechRecognizerVendor; : cs.speechRecognizerVendor;
cs.speechRecognizerLabel = this.recognizer.label !== 'default' cs.speechRecognizerLabel = this.recognizer.label === 'default'
? this.recognizer.label ? cs.speechRecognizerLabel : this.recognizer.label;
: cs.speechRecognizerLabel;
cs.speechRecognizerLanguage = this.recognizer.language !== 'default' cs.speechRecognizerLanguage = this.recognizer.language !== 'default'
? this.recognizer.language ? this.recognizer.language
: cs.speechRecognizerLanguage; : cs.speechRecognizerLanguage;
@@ -180,9 +181,9 @@ class TaskConfig extends Task {
cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default' cs.fallbackSpeechRecognizerVendor = this.recognizer.fallbackVendor !== 'default'
? this.recognizer.fallbackVendor ? this.recognizer.fallbackVendor
: cs.fallbackSpeechRecognizerVendor; : cs.fallbackSpeechRecognizerVendor;
cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel !== 'default' cs.fallbackSpeechRecognizerLabel = this.recognizer.fallbackLabel === 'default' ?
? this.recognizer.fallbackLabel cs.fallbackSpeechRecognizerLabel :
: cs.fallbackSpeechRecognizerLabel; this.recognizer.fallbackLabel;
cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default' cs.fallbackSpeechRecognizerLanguage = this.recognizer.fallbackLanguage !== 'default'
? this.recognizer.fallbackLanguage ? this.recognizer.fallbackLanguage
: cs.fallbackSpeechRecognizerLanguage; : cs.fallbackSpeechRecognizerLanguage;
@@ -206,6 +207,8 @@ class TaskConfig extends Task {
if ('punctuation' in this.recognizer) { if ('punctuation' in this.recognizer) {
cs.globalSttPunctuation = this.recognizer.punctuation; cs.globalSttPunctuation = this.recognizer.punctuation;
} }
// new vendor is set, reset fallback vendor
cs.hasFallbackAsr = false;
this.logger.info({ this.logger.info({
recognizer: this.recognizer, recognizer: this.recognizer,
isContinuousAsr: cs.isContinuousAsr isContinuousAsr: cs.isContinuousAsr
@@ -246,12 +249,14 @@ class TaskConfig extends Task {
} }
if (this.hasTranscribe) { if (this.hasTranscribe) {
if (this.transcribe.enable) { if (this.transcribe.enable) {
this.transcribeOpts.recognizer = this.hasRecognizer ? if (!this.transcribeOpts.recognizer) {
this.recognizer : this.transcribeOpts.recognizer = this.hasRecognizer ?
{ this.recognizer :
vendor: cs.speechRecognizerVendor, {
language: cs.speechRecognizerLanguage vendor: cs.speechRecognizerVendor,
}; language: cs.speechRecognizerLanguage
};
}
this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe'); this.logger.debug(this.transcribeOpts, 'Config: enabling transcribe');
cs.startBackgroundTask('transcribe', this.transcribeOpts); cs.startBackgroundTask('transcribe', this.transcribeOpts);
} else { } else {
@@ -259,13 +264,8 @@ class TaskConfig extends Task {
cs.stopBackgroundTask('transcribe'); cs.stopBackgroundTask('transcribe');
} }
} }
if (Object.keys(this.actionHookDelayAction).length !== 0) {
if (this.actionHookDelayAction) { cs.actionHookDelayProperties = this.actionHookDelayAction;
cs.actionHookDelayEnabled = this.actionHookDelayAction.enabled || false;
cs.actionHookNoResponseTimeout = this.actionHookDelayAction.noResponseTimeout || 0;
cs.actionHookNoResponseGiveUpTimeout = this.actionHookDelayAction.noResponseGiveUpTimeout || 0;
cs.actionHookDelayRetries = this.actionHookDelayAction.retries || 1;
cs.actionHookDelayActions = this.actionHookDelayAction.actions || [];
} }
if (this.data.sipRequestWithinDialogHook) { if (this.data.sipRequestWithinDialogHook) {
cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; cs.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
@@ -288,6 +288,16 @@ class TaskConfig extends Task {
cs.enableFillerNoise(opts); cs.enableFillerNoise(opts);
} }
} }
if (this.hasVad) {
cs.vad = {
enable: this.vad.enable || false,
voiceMs: this.vad.voiceMs || 250,
silenceMs: this.vad.silenceMs || 150,
strategy: this.vad.strategy || 'one-shot',
mode: this.vad.mod || 2
};
}
} }
async kill(cs) { async kill(cs) {

View File

@@ -636,6 +636,8 @@ class TaskDial extends Task {
await this._connectSingleDial(cs, sd); await this._connectSingleDial(cs, sd);
} catch (err) { } catch (err) {
this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial '); this.logger.info({err}, 'Dial:_attemptCalls - Error calling _connectSingleDial ');
sd.removeAllListeners();
this.kill(cs);
} }
}) })
.on('decline', () => { .on('decline', () => {

View File

@@ -10,7 +10,9 @@ const {
IbmTranscriptionEvents, IbmTranscriptionEvents,
NvidiaTranscriptionEvents, NvidiaTranscriptionEvents,
JambonzTranscriptionEvents, JambonzTranscriptionEvents,
AssemblyAiTranscriptionEvents AssemblyAiTranscriptionEvents,
VadDetection,
VerbioTranscriptionEvents
} = require('../utils/constants.json'); } = require('../utils/constants.json');
const { const {
JAMBONES_GATHER_EARLY_HINTS_MATCH, JAMBONES_GATHER_EARLY_HINTS_MATCH,
@@ -27,7 +29,7 @@ class TaskGather extends SttTask {
[ [
'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits', 'finishOnKey', 'input', 'numDigits', 'minDigits', 'maxDigits',
'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein', 'interDigitTimeout', 'partialResultHook', 'bargein', 'dtmfBargein',
'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction', 'fillerNoise' 'speechTimeout', 'timeout', 'say', 'play', 'actionHookDelayAction', 'fillerNoise', 'vad'
].forEach((k) => this[k] = this.data[k]); ].forEach((k) => this[k] = this.data[k]);
// gather default input is digits // gather default input is digits
@@ -41,7 +43,8 @@ class TaskGather extends SttTask {
this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000; this.timeout = this.timeout === 0 ? 0 : (this.timeout || 15) * 1000;
this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0); this.interim = !!this.partialResultHook || this.bargein || (this.timeout > 0);
this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true; this.listenDuringPrompt = this.data.listenDuringPrompt === false ? false : true;
this.minBargeinWordCount = this.data.minBargeinWordCount || 1; this.minBargeinWordCount = this.data.minBargeinWordCount !== undefined ? this.data.minBargeinWordCount : 1;
this._vadEnabled = this.minBargeinWordCount === 0;
if (this.data.recognizer) { if (this.data.recognizer) {
/* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */ /* continuous ASR (i.e. compile transcripts until a special timeout or dtmf key) */
this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ? this.asrTimeout = typeof this.data.recognizer.asrTimeout === 'number' ?
@@ -114,6 +117,7 @@ class TaskGather extends SttTask {
} }
if (this.sayTask) s += ',with nested say task'; if (this.sayTask) s += ',with nested say task';
if (this.playTask) s += ',with nested play task'; if (this.playTask) s += ',with nested play task';
if (this.actionHookDelayAction) s += ',with actionHookDelayAction';
s += '}'; s += '}';
return s; return s;
} }
@@ -128,6 +132,11 @@ class TaskGather extends SttTask {
...(this.fillerNoise || {}) ...(this.fillerNoise || {})
}; };
this.vad = {
...(cs.vad || {}),
...(this.vad || {})
};
if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) { if (cs.hasGlobalSttHints && !this.maskGlobalSttHints) {
const {hints, hintsBoost} = cs.globalSttHints; const {hints, hintsBoost} = cs.globalSttHints;
const setOfHints = new Set((this.data.recognizer.hints || []) const setOfHints = new Set((this.data.recognizer.hints || [])
@@ -155,27 +164,22 @@ class TaskGather extends SttTask {
this.interim = true; this.interim = true;
this.logger.debug('Gather:exec - early hints match enabled'); this.logger.debug('Gather:exec - early hints match enabled');
} }
// actionHook delay
this._hookDelayEn = cs.actionHookDelayEnabled || !!this.actionHookDelayAction;
this._hookDelayActions = this.actionHookDelayAction?.actions || cs.actionHookDelayActions || []; // if we have actionHook delay, and the session does as well, stash the session config
if (this.actionHookDelayAction) {
if (cs.actionHookDelayProcessor) {
this.logger.debug('Gather:exec - stashing session-level ahd proprerties');
cs.stashActionHookDelayProperties();
}
cs.actionHookDelayProperties = this.actionHookDelayAction;
}
// Only enable NoResponseTimeout if there is _hookDelayActions this._startVad();
this._hookNoResponseTimeout = (this._hookDelayActions?.length ?
(this.actionHookDelayAction?.noResponseTimeout || cs.actionHookNoResponseTimeout || 0)
: 0) * 1000;
this._hookNoResponseGiveUpTimeout = (this.actionHookDelayAction?.noResponseGiveUpTimeout || const startDtmfListener = () => {
cs.actionHookNoResponseGiveUpTimeout || 0) * 1000; if (this.input.includes('digits') || this.dtmfBargein || this.asrDtmfTerminationDigit) {
ep.on('dtmf', this._onDtmf.bind(this, cs, ep));
this._hookDelayRetries = this.actionHookDelayAction?.retries || cs.actionHookDelayRetries || 1; }
this._hookDelayRetryCount = 0;
this.hookDelayActionOpts = {
enabled: this._hookDelayEn,
actions: this._hookDelayActions,
noResponseTimeoutMs: this._hookNoResponseTimeout,
noResponseGiveUpTimeoutMs: this._hookNoResponseGiveUpTimeout,
retries: this._hookDelayRetries
}; };
const startListening = async(cs, ep) => { const startListening = async(cs, ep) => {
@@ -191,12 +195,7 @@ class TaskGather extends SttTask {
this._startTranscribing(ep); this._startTranscribing(ep);
return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); return updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
} catch (e) { } catch (e) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) { await this._startFallback(cs, ep, {error: e});
await this._fallback();
startListening(cs, ep);
} else {
this.logger.error({error: e}, 'error in initSpeech');
}
} }
} }
}; };
@@ -204,13 +203,12 @@ class TaskGather extends SttTask {
try { try {
if (this.sayTask) { if (this.sayTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.sayTask.summary}`);
this.sayTask.span = span; const process = () => {
this.sayTask.ctx = ctx;
this.sayTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
this.sayTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
this.logger.debug('Gather: nested say task completed'); this.logger.debug('Gather: nested say task completed');
if (!this.listenDuringPrompt) {
startDtmfListener();
}
this._stopVad();
if (!this.killed) { if (!this.killed) {
startListening(cs, ep); startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) { if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
@@ -220,17 +218,27 @@ class TaskGather extends SttTask {
}); });
} }
} }
};
this.sayTask.span = span;
this.sayTask.ctx = ctx;
this.sayTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
.catch((err) => {
process();
});
this.sayTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing tts');
process();
}); });
} }
else if (this.playTask) { else if (this.playTask) {
const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`); const {span, ctx} = this.startChildSpan(`nested:${this.playTask.summary}`);
this.playTask.span = span; const process = () => {
this.playTask.ctx = ctx;
this.playTask.exec(cs, {ep}); // kicked off, _not_ waiting for it to complete
this.playTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
this.logger.debug('Gather: nested play task completed'); this.logger.debug('Gather: nested play task completed');
if (!this.listenDuringPrompt) {
startDtmfListener();
}
this._stopVad();
if (!this.killed) { if (!this.killed) {
startListening(cs, ep); startListening(cs, ep);
if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) { if (this.input.includes('speech') && this.vendor === 'nuance' && this.listenDuringPrompt) {
@@ -240,6 +248,17 @@ class TaskGather extends SttTask {
}); });
} }
} }
};
this.playTask.span = span;
this.playTask.ctx = ctx;
this.playTask.exec(cs, {ep}) // kicked off, _not_ waiting for it to complete
.catch((err) => {
process();
});
this.playTask.on('playDone', (err) => {
span.end();
if (err) this.logger.error({err}, 'Gather:exec Error playing url');
process();
}); });
} }
else { else {
@@ -252,16 +271,22 @@ class TaskGather extends SttTask {
if (this.input.includes('speech') && this.listenDuringPrompt) { if (this.input.includes('speech') && this.listenDuringPrompt) {
await this._setSpeechHandlers(cs, ep); await this._setSpeechHandlers(cs, ep);
this._startTranscribing(ep); if (!this.resolved && !this.killed) {
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) this._startTranscribing(ep);
.catch(() => {/*already logged error */}); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
else {
this.logger.info('Gather:exec - task was killed or resolved quickly, not starting transcription');
}
} }
if (this.input.includes('digits') || this.dtmfBargein || this.asrDtmfTerminationDigit) { if (this.listenDuringPrompt) {
ep.on('dtmf', this._onDtmf.bind(this, cs, ep)); startDtmfListener();
} }
await this.awaitTaskDone(); await this.awaitTaskDone();
this._killAudio(cs);
} catch (err) { } catch (err) {
this.logger.error(err, 'TaskGather:exec error'); this.logger.error(err, 'TaskGather:exec error');
} }
@@ -271,13 +296,13 @@ class TaskGather extends SttTask {
kill(cs) { kill(cs) {
super.kill(cs); super.kill(cs);
this._killAudio(cs); this._killAudio(cs);
this._killActionHookDelayAction();
this._clearFillerNoiseTimer(); this._clearFillerNoiseTimer();
this.ep.removeAllListeners('dtmf'); this.ep.removeAllListeners('dtmf');
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearAsrTimer(); this._clearAsrTimer();
this.playTask?.span.end(); this.playTask?.span.end();
this.sayTask?.span.end(); this.sayTask?.span.end();
this._stopVad();
this._resolve('killed'); this._resolve('killed');
} }
@@ -297,6 +322,9 @@ class TaskGather extends SttTask {
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
let resolved = false; let resolved = false;
if (this.dtmfBargein) { if (this.dtmfBargein) {
if (!this.playComplete) {
this.notifyStatus({event: 'dtmf-bargein-detected', ...evt});
}
this._killAudio(cs); this._killAudio(cs);
this.emit('dtmf', evt); this.emit('dtmf', evt);
} }
@@ -355,23 +383,19 @@ class TaskGather extends SttTask {
ep, GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, GoogleTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener( this.addCustomEventListener(
ep, GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep)); ep, GoogleTranscriptionEvents.EndOfUtterance, this._onEndOfUtterance.bind(this, cs, ep));
this.addCustomEventListener(
ep, GoogleTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'aws': case 'aws':
case 'polly': case 'polly':
this.bugname = `${this.bugname_prefix}aws_transcribe`; this.bugname = `${this.bugname_prefix}aws_transcribe`;
this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); this.addCustomEventListener(ep, AwsTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, AwsTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'microsoft': case 'microsoft':
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = `${this.bugname_prefix}azure_transcribe`;
this.addCustomEventListener( this.addCustomEventListener(
ep, AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, AzureTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected, //this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected,
this._onNoSpeechDetected.bind(this, cs, ep)); //this._onNoSpeechDetected.bind(this, cs, ep));
this.addCustomEventListener(ep, AzureTranscriptionEvents.VadDetected, this._onVadDetected.bind(this, cs, ep));
break; break;
case 'nuance': case 'nuance':
this.bugname = `${this.bugname_prefix}nuance_transcribe`; this.bugname = `${this.bugname_prefix}nuance_transcribe`;
@@ -381,8 +405,6 @@ class TaskGather extends SttTask {
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.TranscriptionComplete, this.addCustomEventListener(ep, NuanceTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NuanceTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* stall timers until prompt finishes playing */ /* stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -405,6 +427,12 @@ class TaskGather extends SttTask {
ep, SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep)); ep, SonioxTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break; break;
case 'verbio':
this.bugname = `${this.bugname_prefix}verbio_transcribe`;
this.addCustomEventListener(
ep, VerbioTranscriptionEvents.Transcription, this._onTranscription.bind(this, cs, ep));
break;
case 'cobalt': case 'cobalt':
this.bugname = `${this.bugname_prefix}cobalt_transcribe`; this.bugname = `${this.bugname_prefix}cobalt_transcribe`;
this.addCustomEventListener( this.addCustomEventListener(
@@ -452,8 +480,6 @@ class TaskGather extends SttTask {
this._onStartOfSpeech.bind(this, cs, ep)); this._onStartOfSpeech.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.TranscriptionComplete, this.addCustomEventListener(ep, NvidiaTranscriptionEvents.TranscriptionComplete,
this._onTranscriptionComplete.bind(this, cs, ep)); this._onTranscriptionComplete.bind(this, cs, ep));
this.addCustomEventListener(ep, NvidiaTranscriptionEvents.VadDetected,
this._onVadDetected.bind(this, cs, ep));
/* I think nvidia has this (??) - stall timers until prompt finishes playing */ /* I think nvidia has this (??) - stall timers until prompt finishes playing */
if ((this.sayTask || this.playTask) && this.listenDuringPrompt) { if ((this.sayTask || this.playTask) && this.listenDuringPrompt) {
@@ -520,7 +546,8 @@ class TaskGather extends SttTask {
account_sid: this.cs.accountSid, account_sid: this.cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: this.vendor, vendor: this.vendor,
detail: err.message detail: err.message,
target_sid: this.cs.callSid
}); });
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
} }
@@ -559,7 +586,10 @@ class TaskGather extends SttTask {
} }
_clearAsrTimer() { _clearAsrTimer() {
if (this._asrTimer) clearTimeout(this._asrTimer); if (this._asrTimer) {
this.logger.debug('_clearAsrTimer: asrTimer cleared');
clearTimeout(this._asrTimer);
}
this._asrTimer = null; this._asrTimer = null;
} }
@@ -568,99 +598,6 @@ class TaskGather extends SttTask {
this.cs.hangup(); this.cs.hangup();
} }
_actionHookDelaySayAction(verb) {
delete verb.verb;
this.logger.debug(`_actionHookDelaySayAction ${verb}`);
this._actionHookDelaySayTask = makeTask(this.logger, {say: verb}, this);
const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelaySayTask.summary}`);
this._actionHookDelaySayTask.span = span;
this._actionHookDelaySayTask.ctx = ctx;
this._actionHookDelaySayTask.exec(this.cs, {ep: this.ep});
this._actionHookDelaySayTask.on('playDone', (err) => {
this._actionHookDelaySayTask = null;
span.end();
if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts');
});
}
_killActionHookDelayAction() {
this.logger.debug('_killActionHookDelayAction');
if (this._actionHookDelaySayTask && !this._actionHookDelaySayTask.killed) {
this._actionHookDelaySayTask.removeAllListeners('playDone');
this._actionHookDelaySayTask.kill(this.cs);
this._actionHookDelaySayTask.span.end();
this._actionHookDelaySayTask = null;
}
if (this._actionHookDelayPlayTask && !this._actionHookDelayPlayTask.killed) {
this._actionHookDelayPlayTask.removeAllListeners('playDone');
this._actionHookDelayPlayTask.kill(this.cs);
this._actionHookDelayPlayTask.span.end();
this._actionHookDelayPlayTask = null;
}
}
_actionHookDelayPlayAction(verb) {
delete verb.verb;
this.logger.debug(`_actionHookDelayPlayAction ${verb}`);
this._actionHookDelayPlayTask = makeTask(this.logger, {play: verb}, this);
const {span, ctx} = this.startChildSpan(`actionHookDelayAction:${this._actionHookDelayPlayTask.summary}`);
this._actionHookDelayPlayTask.span = span;
this._actionHookDelayPlayTask.ctx = ctx;
this._actionHookDelayPlayTask.exec(this.cs, {ep: this.ep});
this._actionHookDelayPlayTask.on('playDone', (err) => {
this._actionHookDelayPlayTask = null;
span.end();
if (err) this.logger.error({err}, 'Gather:actionHookDelay Error playing tts');
});
}
_startActionHookNoResponseTimer() {
assert(this._hookNoResponseTimeout > 0);
this._clearActionHookNoResponseTimer();
this.logger.debug('startActionHookNoResponseTimer');
this._actionHookNoResponseTimer = setTimeout(() => {
if (this._hookDelayRetryCount >= this._hookDelayRetries) {
this._hangupCall();
return;
}
const verb = this._hookDelayActions[this._hookDelayRetryCount % this._hookDelayActions.length];
if (verb.verb === 'say') {
this._actionHookDelaySayAction(verb);
} else if (verb.verb === 'play') {
this._actionHookDelayPlayAction(verb);
}
this._hookDelayRetryCount++;
this._startActionHookNoResponseTimer();
}, this._hookNoResponseTimeout);
}
_clearActionHookNoResponseTimer() {
if (this._actionHookNoResponseTimer) {
clearTimeout(this._actionHookNoResponseTimer);
}
this._actionHookNoResponseTimer = null;
}
_startActionHookNoResponseGiveUpTimer() {
assert(this._hookNoResponseGiveUpTimeout > 0);
this._clearActionHookNoResponseGiveUpTimer();
this.logger.debug('startActionHookNoResponseGiveUpTimer');
this._actionHookNoResponseGiveUpTimer = setTimeout(() => {
this._hangupCall();
}, this._hookNoResponseGiveUpTimeout);
}
_clearActionHookNoResponseGiveUpTimer() {
if (this._actionHookNoResponseGiveUpTimer) {
clearTimeout(this._actionHookNoResponseGiveUpTimer);
}
this._actionHookNoResponseGiveUpTimer = null;
}
_startFastRecognitionTimer(evt) { _startFastRecognitionTimer(evt) {
assert(this.fastRecognitionTimeout > 0); assert(this.fastRecognitionTimeout > 0);
this._clearFastRecognitionTimer(); this._clearFastRecognitionTimer();
@@ -692,11 +629,44 @@ class TaskGather extends SttTask {
this._finalAsrTimer = null; this._finalAsrTimer = null;
} }
_startVad() {
if (!this._vadStarted && this._vadEnabled) {
this.logger.debug('_startVad');
this.addCustomEventListener(this.ep, VadDetection.Detection, this._onVadDetected.bind(this, this.cs, this.ep));
this.ep?.startVadDetection(this.vad);
this._vadStarted = true;
}
}
_stopVad() {
if (this._vadStarted) {
this.logger.debug('_stopVad');
this.ep?.stopVadDetection(this.vad);
this.ep?.removeCustomEventListener(VadDetection.Detection, this._onVadDetected);
this._vadStarted = false;
}
}
_startFillerNoise() {
this.logger.debug('Gather:_startFillerNoise - playing filler noise');
this.ep?.play(this.fillerNoise.url);
this._fillerNoiseOn = true;
this.ep.once('playback-start', (evt) => {
if (evt.file === this.fillerNoise.url && !this._fillerNoiseOn) {
this.logger.info({evt}, 'Gather:_startFillerNoise - race condition - kill filler noise here');
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing filler noise'));
return;
} else this.logger.debug({evt}, 'Gather:_startFillerNoise - playback started');
});
}
_startFillerNoiseTimer() { _startFillerNoiseTimer() {
this._clearFillerNoiseTimer(); this._clearFillerNoiseTimer();
this._fillerNoiseTimer = setTimeout(() => { this._fillerNoiseTimer = setTimeout(() => {
this.logger.debug('Gather:_startFillerNoiseTimer - playing filler noise'); this.logger.debug('Gather:_startFillerNoiseTimer - playing filler noise');
this.ep?.play(this.fillerNoise.url); this._startFillerNoise();
}, this.fillerNoise.startDelaySecs * 1000); }, this.fillerNoise.startDelaySecs * 1000);
} }
@@ -717,6 +687,7 @@ class TaskGather extends SttTask {
if (this.ep?.connected && (!this.playComplete || this.hasFillerNoise)) { if (this.ep?.connected && (!this.playComplete || this.hasFillerNoise)) {
this.logger.debug('Gather:_killAudio: killing playback of any audio'); this.logger.debug('Gather:_killAudio: killing playback of any audio');
this.playComplete = true; this.playComplete = true;
this._fillerNoiseOn = false; // in a race, if we just started audio it may sneak through here
this.ep.api('uuid_break', this.ep.uuid) this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err, 'Error killing audio')); .catch((err) => this.logger.info(err, 'Error killing audio'));
} }
@@ -732,6 +703,7 @@ class TaskGather extends SttTask {
this.playTask.kill(cs); this.playTask.kill(cs);
this.playTask = null; this.playTask = null;
} }
this.playComplete = true;
} }
_onTranscription(cs, ep, evt, fsEvent) { _onTranscription(cs, ep, evt, fsEvent) {
@@ -740,6 +712,7 @@ class TaskGather extends SttTask {
const finished = fsEvent.getHeader('transcription-session-finished'); const finished = fsEvent.getHeader('transcription-session-finished');
this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript'); this.logger.debug({evt, bugname, finished, vendor: this.vendor}, 'Gather:_onTranscription raw transcript');
if (bugname && this.bugname !== bugname) return; if (bugname && this.bugname !== bugname) return;
if (finished === 'true') return;
if (this.vendor === 'ibm' && evt?.state === 'listening') return; if (this.vendor === 'ibm' && evt?.state === 'listening') return;
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') { if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
@@ -768,6 +741,16 @@ class TaskGather extends SttTask {
this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening'); this.logger.info({evt}, 'TaskGather:_onTranscription - got empty transcript, continue listening');
return; return;
} }
const confidence = evt.alternatives[0].confidence;
const minConfidence = this.data.recognizer?.minConfidence;
this.logger.debug({evt},
`TaskGather:_onTranscription - confidence (${confidence}), minConfidence (${minConfidence})`);
if (confidence && minConfidence && confidence < minConfidence) {
this.logger.info({evt},
'TaskGather:_onTranscription - Transcript confidence ' +
`(${confidence}) < minConfidence (${minConfidence})`);
return this._resolve('stt-low-confidence', evt);
}
/* fast path: our first partial transcript exactly matches an early hint */ /* fast path: our first partial transcript exactly matches an early hint */
if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) { if (this.earlyHintsMatch && evt.is_final === false && this.partialTranscriptsCount++ === 0) {
@@ -834,33 +817,36 @@ class TaskGather extends SttTask {
if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep); if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep);
} }
else { else {
/* this was removed to fix https://github.com/jambonz/jambonz-feature-server/issues/783 */
/*
if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) { if (this.bargein && (words + bufferedWords) < this.minBargeinWordCount) {
this.logger.debug({evt, words, bufferedWords}, this.logger.debug({evt, words, bufferedWords},
'TaskGather:_onTranscription - final transcript but < min barge words'); 'TaskGather:_onTranscription - final transcript but < min barge words');
this._bufferedTranscripts.push(evt); if (!emptyTranscript) this._bufferedTranscripts.push(evt);
if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep); if (!['soniox', 'aws', 'microsoft', 'deepgram'].includes(this.vendor)) this._startTranscribing(ep);
return; return;
} }
else { else {
if (this.vendor === 'soniox') { */
/* compile transcripts into one */ if (this.vendor === 'soniox') {
this._sonioxTranscripts.push(evt.vendor.finalWords); /* compile transcripts into one */
evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language); this._sonioxTranscripts.push(evt.vendor.finalWords);
this._sonioxTranscripts = []; evt = this.compileSonioxTranscripts(this._sonioxTranscripts, 1, this.language);
} this._sonioxTranscripts = [];
else if (this.vendor === 'deepgram') {
/* compile transcripts into one */
if (!emptyTranscript) this._bufferedTranscripts.push(evt);
/* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */
if (this._bufferedTranscripts.length === 0) return;
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
this._bufferedTranscripts = [];
}
/* here is where we return a final transcript */
this._resolve('speech', evt);
} }
else if (this.vendor === 'deepgram') {
/* compile transcripts into one */
if (!emptyTranscript) this._bufferedTranscripts.push(evt);
/* deepgram can send an empty and final transcript; only if we have any buffered should we resolve */
if (this._bufferedTranscripts.length === 0) return;
evt = this.consolidateTranscripts(this._bufferedTranscripts, 1, this.language, this.vendor);
this._bufferedTranscripts = [];
}
/* here is where we return a final transcript */
this._resolve('speech', evt);
/*}*/
} }
} }
else { else {
@@ -881,6 +867,7 @@ class TaskGather extends SttTask {
if (!this.playComplete) { if (!this.playComplete) {
this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech'); this.logger.debug({transcript: evt.alternatives[0].transcript}, 'killing audio due to speech');
this.emit('vad'); this.emit('vad');
this.notifyStatus({event: 'speech-bargein-detected', ...evt});
} }
this._killAudio(cs); this._killAudio(cs);
} }
@@ -895,12 +882,18 @@ class TaskGather extends SttTask {
this.cs.callInfo, httpHeaders)); this.cs.callInfo, httpHeaders));
} }
if (this.vendor === 'soniox') { if (this.vendor === 'soniox') {
this._clearTimer();
if (evt.vendor.finalWords.length) { if (evt.vendor.finalWords.length) {
this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript'); this.logger.debug({evt}, 'TaskGather:_onTranscription - buffering soniox transcript');
this._sonioxTranscripts.push(evt.vendor.finalWords); this._sonioxTranscripts.push(evt.vendor.finalWords);
} }
} }
// If transcription received, reset timeout timer.
if (this._timeoutTimer) {
this._startTimer();
}
/* restart asr timer if we get a partial transcript (only if the asr timer is already running) */
/* note: https://github.com/jambonz/jambonz-feature-server/issues/866 */
if (this.isContinuousAsr && this._asrTimer) this._startAsrTimer();
} }
} }
_onEndOfUtterance(cs, ep) { _onEndOfUtterance(cs, ep) {
@@ -932,9 +925,9 @@ class TaskGather extends SttTask {
_onTranscriptionComplete(cs, ep) { _onTranscriptionComplete(cs, ep) {
this.logger.debug('TaskGather:_onTranscriptionComplete'); this.logger.debug('TaskGather:_onTranscriptionComplete');
} }
async _onJambonzError(cs, ep, evt) {
this.logger.info({evt}, 'TaskGather:_onJambonzError'); async _startFallback(cs, ep, evt) {
if (this.isHandledByPrimaryProvider && this.fallbackVendor) { if (this.canFallback) {
ep.stopTranscription({ ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -942,17 +935,35 @@ class TaskGather extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
await this._fallback(); this.logger.debug('gather:_startFallback');
await this._initSpeech(cs, ep); this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
await this._initFallback();
this._speechHandlersSet = false;
await this._setSpeechHandlers(cs, ep);
this._startTranscribing(ep); this._startTranscribing(ep);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return; return true;
} catch (error) { } catch (error) {
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
} }
} else {
this.logger.debug('gather:_startFallback no condition for falling back');
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
} }
const {writeAlerts, AlertType} = cs.srf.locals; return false;
}
async _onJambonzError(cs, ep, evt) {
if (this.vendor === 'google' && evt.error_code === 0) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError - ignoring google error code 0');
return;
}
this.logger.info({evt}, 'TaskGather:_onJambonzError');
const {writeAlerts, AlertType} = cs.srf.locals;
if (this.vendor === 'nuance') { if (this.vendor === 'nuance') {
const {code, error} = evt; const {code, error} = evt;
if (code === 404 && error === 'No speech') return this._resolve('timeout'); if (code === 404 && error === 'No speech') return this._resolve('timeout');
@@ -964,18 +975,25 @@ class TaskGather extends SttTask {
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`, message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`}); if (!(await this._startFallback(cs, ep, evt))) {
this.notifyTaskDone();
}
} }
_onVendorConnectFailure(cs, _ep, evt) { async _onVendorConnectFailure(cs, _ep, evt) {
super._onVendorConnectFailure(cs, _ep, evt); super._onVendorConnectFailure(cs, _ep, evt);
this.notifyTaskDone(); if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
}
} }
_onVendorError(cs, _ep, evt) { async _onVendorError(cs, _ep, evt) {
super._onVendorError(cs, _ep, evt); super._onVendorError(cs, _ep, evt);
this._resolve('stt-error', evt); if (!(await this._startFallback(cs, _ep, evt))) {
this._resolve('stt-error', evt);
}
} }
_onVadDetected(cs, ep) { _onVadDetected(cs, ep) {
@@ -984,6 +1002,10 @@ class TaskGather extends SttTask {
this._killAudio(cs); this._killAudio(cs);
this.emit('vad'); this.emit('vad');
} }
if (this.vad?.strategy === 'one-shot') {
this.ep?.removeCustomEventListener(VadDetection.Detection, this._onVadDetected);
this._vadStarted = false;
}
} }
_onNoSpeechDetected(cs, ep, evt, fsEvent) { _onNoSpeechDetected(cs, ep, evt, fsEvent) {
@@ -1002,7 +1024,20 @@ class TaskGather extends SttTask {
async _resolve(reason, evt) { async _resolve(reason, evt) {
this.logger.debug(`TaskGather:resolve with reason ${reason}`); this.logger.debug(`TaskGather:resolve with reason ${reason}`);
if (this.resolved) return; if (this.needsStt && this.ep && this.ep.connected) {
this.ep.stopTranscription({
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => {
if (this.resolved) return;
this.logger.error({err}, 'Error stopping transcription');
});
}
if (this.resolved) {
this.logger.debug('TaskGather:_resolve - already resolved');
return;
}
this.resolved = true; this.resolved = true;
// If bargin is false and ws application return ack to verb:hook // If bargin is false and ws application return ack to verb:hook
@@ -1013,19 +1048,14 @@ class TaskGather extends SttTask {
clearTimeout(this.interDigitTimer); clearTimeout(this.interDigitTimer);
this._clearTimer(); this._clearTimer();
this._clearFastRecognitionTimer(); this._clearFastRecognitionTimer();
this._clearAsrTimer();
this._clearFinalAsrTimer();
this.span.setAttributes({ this.span.setAttributes({
channel: 1, channel: 1,
'stt.resolve': reason, 'stt.resolve': reason,
'stt.result': JSON.stringify(evt) 'stt.result': JSON.stringify(evt)
}); });
if (this.needsStt && this.ep && this.ep.connected) {
this.ep.stopTranscription({
vendor: this.vendor,
bugname: this.bugname
})
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));
}
if (this.callSession && this.callSession.callGone) { if (this.callSession && this.callSession.callGone) {
this.logger.debug('TaskGather:_resolve - call is gone, not invoking web callback'); this.logger.debug('TaskGather:_resolve - call is gone, not invoking web callback');
@@ -1033,59 +1063,80 @@ class TaskGather extends SttTask {
return; return;
} }
// Enabled action Hook delay timer to applied actions // action hook delay
if (this._hookNoResponseTimeout > 0) { if (this.cs.actionHookDelayProcessor) {
this._startActionHookNoResponseTimer(); this.logger.debug('TaskGather:_resolve - actionHookDelayProcessor exists - starting it');
} this.cs.actionHookDelayProcessor.start();
if (this._hookNoResponseGiveUpTimeout > 0) {
this._startActionHookNoResponseGiveUpTimer();
} }
// TODO: remove and implement as actionHookDelay
if (this.hasFillerNoise && (reason.startsWith('dtmf') || reason.startsWith('speech'))) { if (this.hasFillerNoise && (reason.startsWith('dtmf') || reason.startsWith('speech'))) {
if (this.fillerNoiseStartDelaySecs > 0) { if (this.fillerNoiseStartDelaySecs > 0) {
this._startFillerNoiseTimer(); this._startFillerNoiseTimer();
} }
else { else {
this.logger.debug(`TaskGather:_resolve - playing filler noise: ${this.fillerNoiseUrl}`); this.logger.debug(`TaskGather:_resolve - playing filler noise: ${this.fillerNoiseUrl}`);
this.ep.play(this.fillerNoiseUrl); this._startFillerNoise();
} }
} }
let returnedVerbs = false;
try { try {
if (reason.startsWith('dtmf')) { if (reason.startsWith('dtmf')) {
if (this.parentTask) this.parentTask.emit('dtmf', evt); if (this.parentTask) this.parentTask.emit('dtmf', evt);
else { else {
this.emit('dtmf', evt); this.emit('dtmf', evt);
await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'}); returnedVerbs = await this.performAction({digits: this.digitBuffer, reason: 'dtmfDetected'});
} }
} }
else if (reason.startsWith('speech')) { else if (reason.startsWith('speech')) {
if (this.parentTask) this.parentTask.emit('transcription', evt); if (this.parentTask) this.parentTask.emit('transcription', evt);
else { else {
this.emit('transcription', evt); this.emit('transcription', evt);
await this.performAction({speech: evt, reason: 'speechDetected'}); this.logger.debug('TaskGather:_resolve - invoking performAction');
returnedVerbs = await this.performAction({speech: evt, reason: 'speechDetected'});
this.logger.debug({returnedVerbs}, 'TaskGather:_resolve - back from performAction');
} }
} }
else if (reason.startsWith('timeout')) { else if (reason.startsWith('timeout')) {
if (this.parentTask) this.parentTask.emit('timeout', evt); if (this.parentTask) this.parentTask.emit('timeout', evt);
else { else {
this.emit('timeout', evt); this.emit('timeout', evt);
await this.performAction({reason: 'timeout'}); returnedVerbs = await this.performAction({reason: 'timeout'});
} }
} }
else if (reason.startsWith('stt-error')) { else if (reason.startsWith('stt-error')) {
if (this.parentTask) this.parentTask.emit('stt-error', evt); if (this.parentTask) this.parentTask.emit('stt-error', evt);
else { else {
this.emit('stt-error', evt); this.emit('stt-error', evt);
await this.performAction({reason: 'error', details: evt.error}); returnedVerbs = await this.performAction({reason: 'error', details: evt.error});
}
} else if (reason.startsWith('stt-low-confidence')) {
if (this.parentTask) this.parentTask.emit('stt-low-confidence', evt);
else {
this.emit('stt-low-confidence', evt);
returnedVerbs = await this.performAction({reason: 'stt-low-confidence'});
} }
} }
} catch (err) { /*already logged error*/ } } catch (err) { /*already logged error*/ }
// Gather got response from hook, cancel all delay timers if there is any // Gather got response from hook, cancel actionHookDelay processing
this._clearActionHookNoResponseTimer(); this.logger.debug('TaskGather:_resolve - checking ahd');
this._clearActionHookNoResponseGiveUpTimer(); if (this.cs.actionHookDelayProcessor) {
if (returnedVerbs) {
this.logger.debug('TaskGather:_resolve - got response from action hook, cancelling actionHookDelay');
await this.cs.actionHookDelayProcessor.stop();
if (this.actionHookDelayAction && !this.cs.popActionHookDelayProperties()) {
// no session level ahd was running when this task started, so clear it
this.cs.clearActionHookDelayProcessor();
this.logger.debug('TaskGather:_resolve - clear ahd');
}
}
else {
this.logger.debug('TaskGather:_resolve - no response from action hook, continue actionHookDelay');
}
}
this._clearFillerNoiseTimer(); this._clearFillerNoiseTimer();
this.notifyTaskDone(); this.notifyTaskDone();

View File

@@ -17,6 +17,7 @@ class TaskRestDial extends Task {
this.call_hook = this.data.call_hook; this.call_hook = this.data.call_hook;
this.timeout = this.data.timeout || 60; this.timeout = this.data.timeout || 60;
this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook; this.sipRequestWithinDialogHook = this.data.sipRequestWithinDialogHook;
this.referHook = this.data.referHook;
this.on('connect', this._onConnect.bind(this)); this.on('connect', this._onConnect.bind(this));
this.on('callStatus', this._onCallStatus.bind(this)); this.on('callStatus', this._onCallStatus.bind(this));
@@ -38,9 +39,9 @@ class TaskRestDial extends Task {
if (this.data.amd) { if (this.data.amd) {
this.startAmd = cs.startAmd; this.startAmd = cs.startAmd;
this.stopAmd = cs.stopAmd;
this.on('amd', this._onAmdEvent.bind(this, cs)); this.on('amd', this._onAmdEvent.bind(this, cs));
} }
this.stopAmd = cs.stopAmd;
this._setCallTimer(); this._setCallTimer();
await this.awaitTaskDone(); await this.awaitTaskDone();
@@ -64,6 +65,7 @@ class TaskRestDial extends Task {
this.canCancel = false; this.canCancel = false;
const cs = this.callSession; const cs = this.callSession;
cs.setDialog(dlg); cs.setDialog(dlg);
cs.referHook = this.referHook;
this.logger.debug('TaskRestDial:_onConnect - call connected'); this.logger.debug('TaskRestDial:_onConnect - call connected');
if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg); if (this.sipRequestWithinDialogHook) this._initSipRequestWithinDialogHandler(cs, dlg);
try { try {

View File

@@ -52,27 +52,37 @@ class TaskSay extends TtsTask {
return `${this.name}{${this.text[0]}}`; return `${this.name}{${this.text[0]}}`;
} }
_validateURL(urlString) {
try {
new URL(urlString);
return true;
} catch (e) {
return false;
}
}
async exec(cs, {ep}) { async exec(cs, {ep}) {
const {srf, accountSid:account_sid} = cs; const {srf, accountSid:account_sid, callSid:target_sid} = cs;
const {writeAlerts, AlertType} = srf.locals; const {writeAlerts, AlertType} = srf.locals;
const {addFileToCache} = srf.locals.dbHelpers; const {addFileToCache} = srf.locals.dbHelpers;
const engine = this.synthesizer.engine || 'standard'; const engine = this.synthesizer.engine || cs.synthesizer?.engine || 'neural';
await super.exec(cs); await super.exec(cs);
this.ep = ep; this.ep = ep;
const vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ? let vendor = this.synthesizer.vendor && this.synthesizer.vendor !== 'default' ?
this.synthesizer.vendor : this.synthesizer.vendor :
cs.speechSynthesisVendor; cs.speechSynthesisVendor;
const language = this.synthesizer.language && this.synthesizer.language !== 'default' ? let language = this.synthesizer.language && this.synthesizer.language !== 'default' ?
this.synthesizer.language : this.synthesizer.language :
cs.speechSynthesisLanguage ; cs.speechSynthesisLanguage ;
const voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ? let voice = this.synthesizer.voice && this.synthesizer.voice !== 'default' ?
this.synthesizer.voice : this.synthesizer.voice :
cs.speechSynthesisVoice; cs.speechSynthesisVoice;
const label = this.synthesizer.label && this.synthesizer.label !== 'default' ? // label can be null/empty in synthesizer config, just use application level label if it's default
this.synthesizer.label : let label = this.synthesizer.label === 'default' ?
cs.speechSynthesisLabel; cs.speechSynthesisLabel :
this.synthesizer.label;
const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ? const fallbackVendor = this.synthesizer.fallbackVendor && this.synthesizer.fallbackVendor !== 'default' ?
this.synthesizer.fallbackVendor : this.synthesizer.fallbackVendor :
@@ -83,16 +93,24 @@ class TaskSay extends TtsTask {
const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ? const fallbackVoice = this.synthesizer.fallbackVoice && this.synthesizer.fallbackVoice !== 'default' ?
this.synthesizer.fallbackVoice : this.synthesizer.fallbackVoice :
cs.fallbackSpeechSynthesisVoice; cs.fallbackSpeechSynthesisVoice;
const fallbackLabel = this.synthesizer.fallbackLabel && this.synthesizer.fallbackLabel !== 'default' ? // label can be null/empty in synthesizer config, just use application level label if it's default
this.synthesizer.fallbackLabel : const fallbackLabel = this.synthesizer.fallbackLabel === 'default' ?
cs.fallbackSpeechSynthesisLabel; cs.fallbackSpeechSynthesisLabel :
this.synthesizer.fallbackLabel;
let filepath; if (cs.hasFallbackTts) {
try { vendor = fallbackVendor;
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label}); language = fallbackLanguage;
} catch (error) { voice = fallbackVoice;
if (fallbackVendor && this.isHandledByPrimaryProvider) { label = fallbackLabel;
}
const startFallback = async(error) => {
if (fallbackVendor && this.isHandledByPrimaryProvider && !cs.hasFallbackTts) {
this.notifyError(
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'in progress'});
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
cs.hasFallbackTts = true;
this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`); this.logger.info(`Synthesize error, fallback to ${fallbackVendor}`);
filepath = await this._synthesizeWithSpecificVendor(cs, ep, filepath = await this._synthesizeWithSpecificVendor(cs, ep,
{ {
@@ -102,8 +120,16 @@ class TaskSay extends TtsTask {
label: fallbackLabel label: fallbackLabel
}); });
} else { } else {
this.notifyError(
{ msg: 'TTS error', details:`TTS vendor ${vendor} error: ${error}`, failover: 'not available'});
throw error; throw error;
} }
};
let filepath;
try {
filepath = await this._synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label});
} catch (error) {
await startFallback(error);
} }
this.notifyStatus({event: 'start-playback'}); this.notifyStatus({event: 'start-playback'});
@@ -115,22 +141,25 @@ class TaskSay extends TtsTask {
await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]); await this.playToConfMember(ep, memberId, confName, confUuid, filepath[segment]);
} }
else { else {
if (filepath[segment].startsWith('say:{')) { const isStreaming = filepath[segment].startsWith('say:{');
if (isStreaming) {
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]); const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`); if (arr) this.logger.debug(`Say:exec sending streaming tts request: ${arr[1].substring(0, 64)}..`);
} }
else this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`); else this.logger.debug(`Say:exec sending ${filepath[segment].substring(0, 64)}`);
ep.once('playback-start', (evt) => { ep.once('playback-start', (evt) => {
this.logger.debug({evt}, 'got playback-start'); this.logger.debug({evt}, 'Say got playback-start');
if (this.otelSpan) { if (this.otelSpan) {
this._addStreamingTtsAttributes(this.otelSpan, evt); this._addStreamingTtsAttributes(this.otelSpan, evt);
this.otelSpan.end(); this.otelSpan.end();
this.otelSpan = null; this.otelSpan = null;
if (evt.variable_tts_cache_filename) cs.trackTmpFile(evt.variable_tts_cache_filename); if (evt.variable_tts_cache_filename) {
cs.trackTmpFile(evt.variable_tts_cache_filename);
}
} }
}); });
ep.once('playback-stop', (evt) => { ep.once('playback-stop', (evt) => {
this.logger.debug({evt}, 'got playback-stop'); this.logger.debug({evt}, 'Say got playback-stop');
if (evt.variable_tts_error) { if (evt.variable_tts_error) {
writeAlerts({ writeAlerts({
account_sid, account_sid,
@@ -139,24 +168,62 @@ class TaskSay extends TtsTask {
detail: evt.variable_tts_error detail: evt.variable_tts_error
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
} }
if (evt.variable_tts_cache_filename) { else {
const text = parseTextFromSayString(this.text[segment]); this.logger.debug({evt}, 'Say got playback-stop');
addFileToCache(evt.variable_tts_cache_filename, { if (evt.variable_tts_error) {
account_sid, writeAlerts({
vendor, account_sid,
language, alert_type: AlertType.TTS_FAILURE,
voice, vendor,
engine, detail: evt.variable_tts_error,
text target_sid
}).catch((err) => this.logger.info({err}, 'Error adding file to cache')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
}
if (evt.variable_tts_cache_filename && !this.killed) {
const text = parseTextFromSayString(this.text[segment]);
addFileToCache(evt.variable_tts_cache_filename, {
account_sid,
vendor,
language,
voice,
engine,
text
}).catch((err) => this.logger.info({err}, 'Error adding file to cache'));
}
}
if (this._playResolve) {
evt.variable_tts_error ? this._playReject(new Error(evt.variable_tts_error)) : this._playResolve();
} }
}); });
await ep.play(filepath[segment]); // wait for playback-stop event received to confirm if the playback is successful
this._playPromise = new Promise((resolve, reject) => {
this._playResolve = resolve;
this._playReject = reject;
});
const r = await ep.play(filepath[segment]);
this.logger.debug({r}, 'Say:exec play result');
try {
// wait for playback-stop event received to confirm if the playback is successful
await this._playPromise;
} catch (err) {
try {
await startFallback(err);
continue;
} catch (err) {
this.logger.info({err}, 'Error waiting for playback-stop event');
}
} finally {
this._playPromise = null;
this._playResolve = null;
this._playReject = null;
}
if (filepath[segment].startsWith('say:{')) { if (filepath[segment].startsWith('say:{')) {
const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]); const arr = /^say:\{.*\}\s*(.*)$/.exec(filepath[segment]);
if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`); if (arr) this.logger.debug(`Say:exec complete playing streaming tts request: ${arr[1].substring(0, 64)}..`);
} else {
// This log will print spech credentials in say command for tts stream mode
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
} }
this.logger.debug(`Say:exec completed play file ${filepath[segment]}`);
} }
segment++; segment++;
} }
@@ -178,6 +245,11 @@ class TaskSay extends TtsTask {
} }
this.ep.removeAllListeners('playback-start'); this.ep.removeAllListeners('playback-start');
this.ep.removeAllListeners('playback-stop'); this.ep.removeAllListeners('playback-stop');
// if we are waiting for playback-stop event, resolve the promise
if (this._playResolve) {
this._playResolve();
this._playResolve = null;
}
} }
} }
@@ -187,6 +259,10 @@ class TaskSay extends TtsTask {
if (key.startsWith('variable_tts_')) { if (key.startsWith('variable_tts_')) {
let newKey = key.substring('variable_tts_'.length) let newKey = key.substring('variable_tts_'.length)
.replace('whisper_', 'whisper.') .replace('whisper_', 'whisper.')
.replace('deepgram_', 'deepgram.')
.replace('playht_', 'playht.')
.replace('rimelabs_', 'rimelabs.')
.replace('verbio_', 'verbio.')
.replace('elevenlabs_', 'elevenlabs.'); .replace('elevenlabs_', 'elevenlabs.');
if (spanMapping[newKey]) newKey = spanMapping[newKey]; if (spanMapping[newKey]) newKey = spanMapping[newKey];
attrs[newKey] = value; attrs[newKey] = value;
@@ -198,6 +274,9 @@ class TaskSay extends TtsTask {
} }
const spanMapping = { const spanMapping = {
// IMPORTANT!!! JAMBONZ WEBAPP WILL SHOW TEXT PERFECTLY IF THE SPAN NAME IS SMALLER OR EQUAL 25 CHARACTERS.
// EX: whisper.ratelim_reqs has length 20 <= 25 which is perfect
// Elevenlabs
'elevenlabs.reported_latency_ms': 'elevenlabs.latency_ms', 'elevenlabs.reported_latency_ms': 'elevenlabs.latency_ms',
'elevenlabs.request_id': 'elevenlabs.req_id', 'elevenlabs.request_id': 'elevenlabs.req_id',
'elevenlabs.history_item_id': 'elevenlabs.item_id', 'elevenlabs.history_item_id': 'elevenlabs.item_id',
@@ -205,11 +284,37 @@ const spanMapping = {
'elevenlabs.name_lookup_time_ms': 'name_lookup_ms', 'elevenlabs.name_lookup_time_ms': 'name_lookup_ms',
'elevenlabs.connect_time_ms': 'connect_ms', 'elevenlabs.connect_time_ms': 'connect_ms',
'elevenlabs.final_response_time_ms': 'final_response_ms', 'elevenlabs.final_response_time_ms': 'final_response_ms',
// Whisper
'whisper.reported_latency_ms': 'whisper.latency_ms', 'whisper.reported_latency_ms': 'whisper.latency_ms',
'whisper.request_id': 'whisper.req_id', 'whisper.request_id': 'whisper.req_id',
'whisper.reported_organization': 'whisper.organization',
'whisper.reported_ratelimit_requests': 'whisper.ratelimit',
'whisper.reported_ratelimit_remaining_requests': 'whisper.ratelimit_remain',
'whisper.reported_ratelimit_reset_requests': 'whisper.ratelimit_reset',
'whisper.name_lookup_time_ms': 'name_lookup_ms', 'whisper.name_lookup_time_ms': 'name_lookup_ms',
'whisper.connect_time_ms': 'connect_ms', 'whisper.connect_time_ms': 'connect_ms',
'whisper.final_response_time_ms': 'final_response_ms', 'whisper.final_response_time_ms': 'final_response_ms',
// Deepgram
'deepgram.request_id': 'deepgram.req_id',
'deepgram.reported_model_name': 'deepgram.model_name',
'deepgram.reported_model_uuid': 'deepgram.model_uuid',
'deepgram.reported_char_count': 'deepgram.char_count',
'deepgram.name_lookup_time_ms': 'name_lookup_ms',
'deepgram.connect_time_ms': 'connect_ms',
'deepgram.final_response_time_ms': 'final_response_ms',
// Playht
'playht.request_id': 'playht.req_id',
'playht.name_lookup_time_ms': 'name_lookup_ms',
'playht.connect_time_ms': 'connect_ms',
'playht.final_response_time_ms': 'final_response_ms',
// Rimelabs
'rimelabs.name_lookup_time_ms': 'name_lookup_ms',
'rimelabs.connect_time_ms': 'connect_ms',
'rimelabs.final_response_time_ms': 'final_response_ms',
// verbio
'verbio.name_lookup_time_ms': 'name_lookup_ms',
'verbio.connect_time_ms': 'connect_ms',
'verbio.final_response_time_ms': 'final_response_ms',
}; };
module.exports = TaskSay; module.exports = TaskSay;

View File

@@ -33,7 +33,8 @@ class SttTask extends Task {
//fallback //fallback
this.fallbackVendor = recognizer.fallbackVendor || 'default'; this.fallbackVendor = recognizer.fallbackVendor || 'default';
this.fallbackLanguage = recognizer.fallbackLanguage || 'default'; this.fallbackLanguage = recognizer.fallbackLanguage || 'default';
this.fallbackLabel = recognizer.fallbackLabel || 'default'; // label can be empty and should not have default value.
this.fallbackLabel = recognizer.fallbackLabel;
/* let credentials be supplied in the recognizer object at runtime */ /* let credentials be supplied in the recognizer object at runtime */
this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer); this.sttCredentials = setSpeechCredentialsAtRuntime(recognizer);
@@ -81,7 +82,8 @@ class SttTask extends Task {
this.language = cs.speechRecognizerLanguage; this.language = cs.speechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.language = this.language; if (this.data.recognizer) this.data.recognizer.language = this.language;
} }
if ('default' === this.label || !this.label) { // label can be empty, should not assign application level label
if ('default' === this.label) {
this.label = cs.speechRecognizerLabel; this.label = cs.speechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.label = this.label; if (this.data.recognizer) this.data.recognizer.label = this.label;
} }
@@ -94,10 +96,18 @@ class SttTask extends Task {
this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage; this.fallbackLanguage = cs.fallbackSpeechRecognizerLanguage;
if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage; if (this.data.recognizer) this.data.recognizer.fallbackLanguage = this.fallbackLanguage;
} }
if ('default' === this.fallbackLabel || !this.fallbackLabel) { // label can be empty, should not assign application level label
if ('default' === this.fallbackLabel) {
this.fallbackLabel = cs.fallbackSpeechRecognizerLabel; this.fallbackLabel = cs.fallbackSpeechRecognizerLabel;
if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel; if (this.data.recognizer) this.data.recognizer.fallbackLabel = this.fallbackLabel;
} }
// If call is already fallback to 2nd ASR vendor
// use that.
if (cs.hasFallbackAsr) {
this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage;
this.label = this.fallbackLabel;
}
if (!this.data.recognizer.vendor) { if (!this.data.recognizer.vendor) {
this.data.recognizer.vendor = this.vendor; this.data.recognizer.vendor = this.vendor;
} }
@@ -115,9 +125,19 @@ class SttTask extends Task {
try { try {
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
} catch (error) { } catch (error) {
if (this.fallbackVendor && this.isHandledByPrimaryProvider) { if (this.canFallback) {
await this._fallback(); this.notifyError(
{
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
failover: 'in progress'
});
await this._initFallback();
} else { } else {
this.notifyError(
{
msg: 'ASR error', details:`Invalid vendor ${this.vendor}, Error: ${error}`,
failover: 'not available'
});
throw error; throw error;
} }
} }
@@ -149,7 +169,7 @@ class SttTask extends Task {
} }
async _initSpeechCredentials(cs, vendor, label) { async _initSpeechCredentials(cs, vendor, label) {
const {getNuanceAccessToken, getIbmAccessToken} = cs.srf.locals.dbHelpers; const {getNuanceAccessToken, getIbmAccessToken, getAwsAuthToken, getVerbioAccessToken} = cs.srf.locals.dbHelpers;
let credentials = cs.getSpeechCredentials(vendor, 'stt', label); let credentials = cs.getSpeechCredentials(vendor, 'stt', label);
if (!credentials) { if (!credentials) {
@@ -158,13 +178,9 @@ class SttTask extends Task {
writeAlerts({ writeAlerts({
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_NOT_PROVISIONED, alert_type: AlertType.STT_NOT_PROVISIONED,
vendor vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for no stt')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no stt'));
// Notify application that STT vender is wrong.
this.notifyError({
msg: 'ASR error',
details: `No speech-to-text service credentials for ${vendor} have been configured`
});
this.notifyTaskDone(); this.notifyTaskDone();
throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`); throw new Error(`No speech-to-text service credentials for ${vendor} have been configured`);
} }
@@ -182,13 +198,33 @@ class SttTask extends Task {
const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key); const {access_token, servedFromCache} = await getIbmAccessToken(stt_api_key);
this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`); this.logger.debug({stt_api_key}, `got ibm access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, access_token, stt_region}; credentials = {...credentials, access_token, stt_region};
} else if (['aws', 'polly'].includes(vendor) && credentials.roleArn) {
/* get aws access token */
const {roleArn, region} = credentials;
const {accessKeyId, secretAccessKey, sessionToken, servedFromCache} =
await getAwsAuthToken({
region,
roleArn
});
this.logger.debug({roleArn}, `got aws access token ${servedFromCache ? 'from cache' : ''}`);
credentials = {...credentials, accessKeyId, secretAccessKey, sessionToken};
} else if (vendor === 'verbio' && credentials.client_id && credentials.client_secret) {
const {access_token, servedFromCache} = await getVerbioAccessToken(credentials);
this.logger.debug({client_id: credentials.client_id},
`got verbio access token ${servedFromCache ? 'from cache' : ''}`);
credentials.access_token = access_token;
} }
return credentials; return credentials;
} }
async _fallback() { get canFallback() {
return this.fallbackVendor && this.isHandledByPrimaryProvider && !this.cs.hasFallbackAsr;
}
async _initFallback() {
assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration'); assert(this.fallbackVendor, 'fallback failed without fallbackVendor configuration');
this.isHandledByPrimaryProvider = false; this.isHandledByPrimaryProvider = false;
this.cs.hasFallbackAsr = true;
this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`); this.logger.info(`Failed to use primary STT provider, fallback to ${this.fallbackVendor}`);
this.vendor = this.fallbackVendor; this.vendor = this.fallbackVendor;
this.language = this.fallbackLanguage; this.language = this.fallbackLanguage;
@@ -197,6 +233,8 @@ class SttTask extends Task {
this.data.recognizer.language = this.language; this.data.recognizer.language = this.language;
this.data.recognizer.label = this.label; this.data.recognizer.label = this.label;
this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label); this.sttCredentials = await this._initSpeechCredentials(this.cs, this.vendor, this.label);
// cleanup previous listener from previous vendor
this.removeCustomEventListeners();
} }
async compileHintsForCobalt(ep, hostport, model, token, hints) { async compileHintsForCobalt(ep, hostport, model, token, hints) {
@@ -240,6 +278,20 @@ class SttTask extends Task {
_doContinuousAsrWithDeepgram(asrTimeout) { _doContinuousAsrWithDeepgram(asrTimeout) {
/* deepgram has an utterance_end_ms property that simplifies things */ /* deepgram has an utterance_end_ms property that simplifies things */
assert(this.vendor === 'deepgram'); assert(this.vendor === 'deepgram');
if (asrTimeout < 1000) {
this.notifyError({
msg: 'ASR error',
details:`asrTimeout ${asrTimeout} is too short for deepgram; setting it to 1000ms`
});
asrTimeout = 1000;
}
else if (asrTimeout > 5000) {
this.notifyError({
msg: 'ASR error',
details:`asrTimeout ${asrTimeout} is too long for deepgram; setting it to 5000ms`
});
asrTimeout = 5000;
}
this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`); this.logger.debug(`_doContinuousAsrWithDeepgram - setting utterance_end_ms to ${asrTimeout}`);
const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {}; const dgOptions = this.data.recognizer.deepgramOptions = this.data.recognizer.deepgramOptions || {};
dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout; dgOptions.utteranceEndMs = dgOptions.utteranceEndMs || asrTimeout;
@@ -258,8 +310,8 @@ class SttTask extends Task {
message: 'STT failure reported by vendor', message: 'STT failure reported by vendor',
detail: evt.error, detail: evt.error,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${evt.error}`});
} }
_onVendorConnectFailure(cs, _ep, evt) { _onVendorConnectFailure(cs, _ep, evt) {
@@ -271,8 +323,8 @@ class SttTask extends Task {
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`, message: `Failed connecting to ${this.vendor} speech recognizer: ${reason}`,
vendor: this.vendor, vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`)); }).catch((err) => this.logger.info({err}, `Error generating alert for ${this.vendor} connection failure`));
this.notifyError({msg: 'ASR error', details:`Failed connecting to speech vendor ${this.vendor}: ${reason}`});
} }
} }

View File

@@ -45,6 +45,10 @@ class Task extends Emitter {
return this.name; return this.name;
} }
set disableTracing(val) {
this._disableTracing = val;
}
toJSON() { toJSON() {
return this.data; return this.data;
} }
@@ -177,15 +181,16 @@ class Task extends Emitter {
// If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook // If actionHook delay action is configured, and ws application have not responded yet any verb for actionHook
// We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook // We have to transfer the task to call-session to await on next ws command verbs, and also run action Hook
// delay actions // delay actions
if (this.hookDelayActionOpts) { //if (this.hookDelayActionOpts) {
this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts); // this.emit('ActionHookDelayActionOptions', this.hookDelayActionOpts);
} //}
} }
if (expectResponse && json && Array.isArray(json)) { if (expectResponse && json && Array.isArray(json)) {
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) { if (tasks && tasks.length > 0) {
this.callSession.replaceApplication(tasks); this.callSession.replaceApplication(tasks);
return true;
} }
} }
} catch (err) { } catch (err) {
@@ -193,6 +198,7 @@ class Task extends Emitter {
span.end(); span.end();
throw err; throw err;
} }
return false;
} }
} }
@@ -272,6 +278,7 @@ class Task extends Emitter {
delete obj.requestor; delete obj.requestor;
delete obj.notifier; delete obj.notifier;
obj.tasks = cs.getRemainingTaskData(); obj.tasks = cs.getRemainingTaskData();
obj.callInfo = cs.callInfo.toJSON();
if (opts && obj.tasks.length > 0) { if (opts && obj.tasks.length > 0) {
const key = Object.keys(obj.tasks[0])[0]; const key = Object.keys(obj.tasks[0])[0];
Object.assign(obj.tasks[0][key], {_: opts}); Object.assign(obj.tasks[0][key], {_: opts});

View File

@@ -104,12 +104,15 @@ class TaskTranscribe extends SttTask {
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid) updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid)
.catch(() => {/*already logged error */}); .catch(() => {/*already logged error */});
await this.awaitTaskDone();
} catch (err) { } catch (err) {
this.logger.info(err, 'TaskTranscribe:exec - error'); if (!(await this._startFallback(cs, ep, {error: err}))) {
this.parentTask && this.parentTask.emit('error', err); this.logger.info(err, 'TaskTranscribe:exec - error');
this.parentTask && this.parentTask.emit('error', err);
this.removeCustomEventListeners();
return;
}
} }
await this.awaitTaskDone();
this.removeCustomEventListeners(); this.removeCustomEventListeners();
} }
@@ -123,7 +126,7 @@ class TaskTranscribe extends SttTask {
}) })
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
} }
if (this.transcribing2 && this.ep2.connected) { if (this.transcribing2 && this.ep2?.connected) {
stopTranscription = true; stopTranscription = true;
this.ep2.stopTranscription({vendor: this.vendor, bugname: this.bugname}) this.ep2.stopTranscription({vendor: this.vendor, bugname: this.bugname})
.catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill')); .catch((err) => this.logger.info(err, 'Error TaskTranscribe:kill'));
@@ -194,8 +197,8 @@ class TaskTranscribe extends SttTask {
this.bugname = `${this.bugname_prefix}azure_transcribe`; this.bugname = `${this.bugname_prefix}azure_transcribe`;
this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription, this.addCustomEventListener(ep, AzureTranscriptionEvents.Transcription,
this._onTranscription.bind(this, cs, ep, channel)); this._onTranscription.bind(this, cs, ep, channel));
this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected, //this.addCustomEventListener(ep, AzureTranscriptionEvents.NoSpeechDetected,
this._onNoAudio.bind(this, cs, ep, channel)); // this._onNoAudio.bind(this, cs, ep, channel));
break; break;
case 'nuance': case 'nuance':
this.bugname = `${this.bugname_prefix}nuance_transcribe`; this.bugname = `${this.bugname_prefix}nuance_transcribe`;
@@ -212,7 +215,7 @@ class TaskTranscribe extends SttTask {
this._onVendorConnectFailure.bind(this, cs, ep, channel)); this._onVendorConnectFailure.bind(this, cs, ep, channel));
/* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */ /* if app sets deepgramOptions.utteranceEndMs they essentially want continuous asr */
if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true; //if (opts.DEEPGRAM_SPEECH_UTTERANCE_END_MS) this.isContinuousAsr = true;
break; break;
case 'soniox': case 'soniox':
@@ -336,6 +339,12 @@ class TaskTranscribe extends SttTask {
if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') { if (this.vendor === 'deepgram' && evt.type === 'UtteranceEnd') {
/* we will only get this when we have set utterance_end_ms */ /* we will only get this when we have set utterance_end_ms */
/* DH: send a speech event when we get UtteranceEnd if they want interim events */
if (this.interim) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram, sending speech event');
this._resolve(channel, evt);
}
if (bufferedTranscripts.length === 0) { if (bufferedTranscripts.length === 0) {
this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts'); this.logger.debug('Gather:_onTranscription - got UtteranceEnd event from deepgram but no buffered transcripts');
} }
@@ -420,9 +429,13 @@ class TaskTranscribe extends SttTask {
/* here is where we return a final transcript */ /* here is where we return a final transcript */
this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript'); this.logger.debug({evt}, 'TaskTranscribe:_onTranscription - sending final transcript');
this._resolve(channel, evt); this._resolve(channel, evt);
/* some STT engines will keep listening after a final response, so no need to restart */ /* some STT engines will keep listening after a final response, so no need to restart */
if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google'] if (!['soniox', 'aws', 'microsoft', 'deepgram', 'google'].includes(this.vendor) &&
.includes(this.vendor)) this._startTranscribing(cs, ep, channel); !this.vendor.startsWith('custom:')) {
this.logger.debug('TaskTranscribe:_onTranscription - restarting transcribe');
this._startTranscribing(cs, ep, channel);
}
} }
} }
else { else {
@@ -445,26 +458,31 @@ class TaskTranscribe extends SttTask {
} }
async _resolve(channel, evt) { async _resolve(channel, evt) {
/* we've got a transcript, so end the otel child span for this channel */ if (evt.is_final) {
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { /* we've got a final transcript, so end the otel child span for this channel */
this.childSpan[channel - 1].span.setAttributes({ if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
channel, this.childSpan[channel - 1].span.setAttributes({
'stt.resolve': 'transcript', channel,
'stt.result': JSON.stringify(evt) 'stt.resolve': 'transcript',
}); 'stt.result': JSON.stringify(evt)
this.childSpan[channel - 1].span.end(); });
this.childSpan[channel - 1].span.end();
}
} }
if (this.transcriptionHook) { if (this.transcriptionHook) {
const b3 = this.getTracingPropagation(); const b3 = this.getTracingPropagation();
const httpHeaders = b3 && {b3}; const httpHeaders = b3 && {b3};
const payload = {
...this.cs.callInfo,
...httpHeaders,
...(evt.alternatives && {speech: evt}),
...(evt.type && {speechEvent: evt})
};
try { try {
const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, { this.logger.debug({payload}, 'sending transcriptionHook');
...this.cs.callInfo, const json = await this.cs.requestor.request('verb:hook', this.transcriptionHook, payload);
...httpHeaders, this.logger.info({json}, 'completed transcriptionHook');
speech: evt
});
this.logger.info({json}, 'sent transcriptionHook');
if (json && Array.isArray(json) && !this.parentTask) { if (json && Array.isArray(json) && !this.parentTask) {
const makeTask = require('./make_task'); const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata)); const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
@@ -485,7 +503,7 @@ class TaskTranscribe extends SttTask {
this._clearTimer(); this._clearTimer();
this.notifyTaskDone(); this.notifyTaskDone();
} }
else { else if (evt.is_final) {
/* start another child span for this channel */ /* start another child span for this channel */
const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`); const {span, ctx} = this.startChildSpan(`${STT_LISTEN_SPAN_NAME}:${channel}`);
this.childSpan[channel - 1] = {span, ctx}; this.childSpan[channel - 1] = {span, ctx};
@@ -534,10 +552,8 @@ class TaskTranscribe extends SttTask {
} }
} }
async _onJambonzError(cs, _ep, evt) { async _startFallback(cs, _ep, evt) {
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError'); if (this.canFallback) {
if (this.paused) return;
if (this.isHandledByPrimaryProvider && this.fallbackVendor) {
_ep.stopTranscription({ _ep.stopTranscription({
vendor: this.vendor, vendor: this.vendor,
bugname: this.bugname bugname: this.bugname
@@ -545,38 +561,58 @@ class TaskTranscribe extends SttTask {
.catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`)); .catch((err) => this.logger.error({err}, `Error stopping transcription for primary vendor ${this.vendor}`));
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, cs.srf);
try { try {
await this._fallback(); this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'in progress'});
await this._initFallback();
let channel = 1; let channel = 1;
if (this.ep !== _ep) { if (this.ep !== _ep) {
channel = 2; channel = 2;
} }
this[`_speechHandlersSet_${channel}`] = false;
this._startTranscribing(cs, _ep, channel); this._startTranscribing(cs, _ep, channel);
updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid); updateSpeechCredentialLastUsed(this.sttCredentials.speech_credential_sid);
return; return true;
} catch (error) { } catch (error) {
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`); this.logger.info({error}, `There is error while falling back to ${this.fallbackVendor}`);
} }
} else { } else {
const {writeAlerts, AlertType} = cs.srf.locals; this.logger.debug('transcribe:_startFallback no condition for falling back');
this.notifyError({ msg: 'ASR error',
details:`STT Vendor ${this.vendor} error: ${evt.error || evt.reason}`, failover: 'not available'});
}
return false;
}
if (this.vendor === 'nuance') { async _onJambonzError(cs, _ep, evt) {
const {code, error} = evt; if (this.vendor === 'google' && evt.error_code === 0) {
//TODO: fix below, currently _resolve does not send timeout events this.logger.info({evt}, 'TaskTranscribe:_onJambonzError - ignoring google error code 0');
if (code === 404 && error === 'No speech') return this._resolve('timeout'); return;
if (code === 413 && error === 'Too much speech') return this._resolve('timeout'); }
} this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError'); if (this.paused) return;
writeAlerts({ const {writeAlerts, AlertType} = cs.srf.locals;
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE, if (this.vendor === 'nuance') {
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`, const {code, error} = evt;
vendor: this.vendor, if (code === 404 && error === 'No speech') return this._resolve('timeout');
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure')); if (code === 413 && error === 'Too much speech') return this._resolve('timeout');
this.notifyError({msg: 'ASR error', details:`Custom speech vendor ${this.vendor} error: ${evt.error}`}); }
this.logger.info({evt}, 'TaskTranscribe:_onJambonzError');
writeAlerts({
account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE,
message: `Custom speech vendor ${this.vendor} error: ${evt.error}`,
vendor: this.vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for jambonz custom connection failure'));
if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
} }
} }
_onVendorConnectFailure(cs, _ep, channel, evt) { async _onVendorConnectFailure(cs, _ep, channel, evt) {
super._onVendorConnectFailure(cs, _ep, evt); super._onVendorConnectFailure(cs, _ep, evt);
if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) { if (this.childSpan[channel - 1] && this.childSpan[channel - 1].span) {
this.childSpan[channel - 1].span.setAttributes({ this.childSpan[channel - 1].span.setAttributes({
@@ -585,7 +621,9 @@ class TaskTranscribe extends SttTask {
}); });
this.childSpan[channel - 1].span.end(); this.childSpan[channel - 1].span.end();
} }
this.notifyTaskDone(); if (!(await this._startFallback(cs, _ep, evt))) {
this.notifyTaskDone();
}
} }
_startAsrTimer(channel) { _startAsrTimer(channel) {

View File

@@ -17,21 +17,31 @@ class TtsTask extends Task {
async exec(cs) { async exec(cs) {
super.exec(cs); super.exec(cs);
if (cs.synthesizer) {
this.options = {...cs.synthesizer.options, ...this.options};
this.data.synthesizer = this.data.synthesizer || {};
for (const k in cs.synthesizer) {
const newValue = this.data.synthesizer && this.data.synthesizer[k] !== undefined ?
this.data.synthesizer[k] :
cs.synthesizer[k];
if (Array.isArray(newValue)) {
this.data.synthesizer[k] = [...(this.data.synthesizer[k] || []), ...cs.synthesizer[k]];
} else if (typeof newValue === 'object' && newValue !== null) {
this.data.synthesizer[k] = { ...(this.data.synthesizer[k] || {}), ...cs.synthesizer[k] };
} else {
this.data.synthesizer[k] = newValue;
}
}
}
} }
async _synthesizeWithSpecificVendor(cs, ep, { async _synthesizeWithSpecificVendor(cs, ep, {vendor, language, voice, label, preCache = false}) {
vendor,
language,
voice,
label,
disableTtsStreaming,
preCache
}) {
const {srf, accountSid:account_sid} = cs; const {srf, accountSid:account_sid} = cs;
const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf); const {updateSpeechCredentialLastUsed} = require('../utils/db-utils')(this.logger, srf);
const {writeAlerts, AlertType, stats} = srf.locals; const {writeAlerts, AlertType, stats} = srf.locals;
const {synthAudio} = srf.locals.dbHelpers; const {synthAudio} = srf.locals.dbHelpers;
const engine = this.synthesizer.engine || 'standard'; const engine = this.synthesizer.engine || cs.synthesizer?.engine || 'neural';
const salt = cs.callSid; const salt = cs.callSid;
let credentials = cs.getSpeechCredentials(vendor, 'tts', label); let credentials = cs.getSpeechCredentials(vendor, 'tts', label);
@@ -65,23 +75,23 @@ class TtsTask extends Task {
} }
ep.set({ ep.set({
tts_engine: vendor, tts_engine: vendor.startsWith('custom:') ? 'custom' : vendor,
tts_voice: voice, tts_voice: voice,
cache_speech_handles: 1, cache_speech_handles: !cs.currentTtsVendor || cs.currentTtsVendor === vendor ? 1 : 0,
}).catch((err) => this.logger.info({err}, `${this.name}: Error setting tts_engine on endpoint`)); }).catch((err) => this.logger.info({err}, 'Error setting tts_engine on endpoint'));
// set the current vendor on the call session
// If vendor is changed from the previous one, then reset the cache_speech_handles flag
cs.currentTtsVendor = vendor;
if (!preCache) this.logger.info({vendor, language, voice, model}, `${this.name}:exec`); if (!preCache && !this._disableTracing) this.logger.info({vendor, language, voice, model}, 'TaskSay:exec');
try { try {
if (!credentials) { if (!credentials) {
writeAlerts({ writeAlerts({
account_sid, account_sid,
alert_type: AlertType.TTS_NOT_PROVISIONED, alert_type: AlertType.TTS_NOT_PROVISIONED,
vendor vendor,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for no tts')); }).catch((err) => this.logger.info({err}, 'Error generating alert for no tts'));
this.notifyError({
msg: 'TTS error',
details:`No speech credentials provisioned for selected vendor ${vendor}`
});
throw new Error('no provisioned speech credentials for TTS'); throw new Error('no provisioned speech credentials for TTS');
} }
// synthesize all of the text elements // synthesize all of the text elements
@@ -93,7 +103,7 @@ class TtsTask extends Task {
if (text.startsWith('silence_stream://')) return text; if (text.startsWith('silence_stream://')) return text;
/* otel: trace time for tts */ /* otel: trace time for tts */
if (!preCache && !this.parentTask) { if (!preCache && !this._disableTracing) {
const {span} = this.startChildSpan('tts-generation', { const {span} = this.startChildSpan('tts-generation', {
'tts.vendor': vendor, 'tts.vendor': vendor,
'tts.language': language, 'tts.language': language,
@@ -114,11 +124,10 @@ class TtsTask extends Task {
credentials, credentials,
options: this.options, options: this.options,
disableTtsCache : this.disableTtsCache, disableTtsCache : this.disableTtsCache,
disableTtsStreaming, renderForCaching: preCache
preCache
}); });
if (!filePath.startsWith('say:')) { if (!filePath.startsWith('say:')) {
this.logger.debug(`file ${filePath}, served from cache ${servedFromCache}`); this.logger.debug(`Say: file ${filePath}, served from cache ${servedFromCache}`);
if (filePath) cs.trackTmpFile(filePath); if (filePath) cs.trackTmpFile(filePath);
if (this.otelSpan) { if (this.otelSpan) {
this.otelSpan.setAttributes({'tts.cached': servedFromCache}); this.otelSpan.setAttributes({'tts.cached': servedFromCache});
@@ -129,7 +138,7 @@ class TtsTask extends Task {
lastUpdated = true; lastUpdated = true;
updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */}); updateSpeechCredentialLastUsed(credentials.speech_credential_sid).catch(() => {/* logged error */});
} }
if (!servedFromCache && rtt && !preCache) { if (!servedFromCache && rtt && !preCache && !this._disableTracing) {
this.notifyStatus({ this.notifyStatus({
event: 'synthesized-audio', event: 'synthesized-audio',
vendor, vendor,
@@ -140,7 +149,7 @@ class TtsTask extends Task {
} }
} }
else { else {
this.logger.debug('a streaming tts api will be used'); this.logger.debug('Say: a streaming tts api will be used');
const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`); const modifiedPath = filePath.replace('say:{', `say:{session-uuid=${ep.uuid},`);
return modifiedPath; return modifiedPath;
} }
@@ -152,9 +161,9 @@ class TtsTask extends Task {
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.TTS_FAILURE, alert_type: AlertType.TTS_FAILURE,
vendor, vendor,
detail: err.message detail: err.message,
target_sid: cs.callSid
}).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => this.logger.info({err}, 'Error generating alert for tts failure'));
this.notifyError({msg: 'TTS error', details: err.message || err});
throw err; throw err;
} }
}; };
@@ -165,6 +174,7 @@ class TtsTask extends Task {
this.logger.info(err, 'TaskSay:exec error'); this.logger.info(err, 'TaskSay:exec error');
throw err; throw err;
} }
} }
_validateURL(urlString) { _validateURL(urlString) {

View File

@@ -0,0 +1,168 @@
const makeTask = require('../tasks/make_task');
const Emitter = require('events');
const { normalizeJambones } = require('@jambonz/verb-specifications');
const {TaskName} = require('../utils/constants');
const assert = require('assert');
/**
* ActionHookDelayProcessor
* @extends Emitter
*
* @param {Object} logger - logger instance
* @param {Object} opts - options
* @param {Object} cs - call session
* @param {Object} ep - endpoint
*
* @emits {Event} 'giveup' - when associated giveup timer expires
*
* Ref:https://www.jambonz.org/docs/supporting-articles/handling-action-hook-delays/
*/
class ActionHookDelayProcessor extends Emitter {
constructor(logger, opts, cs) {
super();
this.logger = logger;
this.cs = cs;
this._active = false;
const enabled = this.init(opts);
if (enabled && (!this.actions || !Array.isArray(this.actions) || this.actions.length === 0)) {
throw new Error('ActionHookDelayProcessor: no actions specified');
}
else if (enabled && this.actions.some((a) => !a.verb || ![TaskName.Say, TaskName.Play].includes(a.verb))) {
throw new Error(`ActionHookDelayProcessor: invalid actions specified: ${JSON.stringify(this.actions)}`);
}
}
get properties() {
return {
actions: this.actions,
retries: this.retries,
noResponseTimeout: this.noResponseTimeout,
noResponseGiveUpTimeout: this.noResponseGiveUpTimeout
};
}
get ep() {
return this.cs.ep;
}
init(opts) {
this.logger.debug({opts}, 'ActionHookDelayProcessor#init');
this.actions = opts.actions;
this.retries = opts.retries || 0;
this.noResponseTimeout = opts.noResponseTimeout || 0;
this.noResponseGiveUpTimeout = opts.noResponseGiveUpTimeout;
// return false if these options actually disable the ahdp
return ('enable' in opts && opts.enable === true) ||
('enabled' in opts && opts.enabled === true) ||
(!('enable' in opts) && !('enabled' in opts));
}
start() {
this.logger.debug('ActionHookDelayProcessor#start');
if (this._active) {
this.logger.debug('ActionHookDelayProcessor#start: already started due to prior gather which is continuing');
return;
}
assert(!this._noResponseTimer);
this._active = true;
this._retryCount = 0;
const timeoutMs = this.noResponseTimeout === 0 ? 1 : this.noResponseTimeout * 1000;
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
if (this.noResponseGiveUpTimeout > 0) {
const timeoutMs = this.noResponseGiveUpTimeout * 1000;
this._noResponseGiveUpTimer = setTimeout(this._onNoResponseGiveUpTimer.bind(this), timeoutMs);
}
}
async stop() {
this._active = false;
if (this._noResponseTimer) {
clearTimeout(this._noResponseTimer);
this._noResponseTimer = null;
}
if (this._noResponseGiveUpTimer) {
clearTimeout(this._noResponseGiveUpTimer);
this._noResponseGiveUpTimer = null;
}
if (this._taskInProgress) {
this.logger.debug(`ActionHookDelayProcessor#stop: stopping ${this._taskInProgress.name}`);
this._sayResolver = () => {
this.logger.debug('ActionHookDelayProcessor#stop: play/say is done, continue on..');
//this._taskInProgress.kill(this.cs);
this._taskInProgress = null;
};
/* we let Say finish, but interrupt Play */
if (TaskName.Play === this._taskInProgress.name) {
await this._taskInProgress.kill(this.cs);
}
return new Promise((resolve) => this._sayResolver = resolve);
}
this.logger.debug('ActionHookDelayProcessor#stop returning');
}
_onNoResponseTimer() {
this.logger.debug('ActionHookDelayProcessor#_onNoResponseTimer');
this._noResponseTimer = null;
/* get the next play or say action */
const verb = this.actions[this._retryCount % this.actions.length];
const t = normalizeJambones(this.logger, [verb]);
this.logger.debug({verb}, 'ActionHookDelayProcessor#_onNoResponseTimer: starting action');
try {
this._taskInProgress = makeTask(this.logger, t[0]);
this._taskInProgress.disableTracing = true;
this._taskInProgress.exec(this.cs, {ep: this.ep});
} catch (err) {
this.logger.info(err, 'ActionHookDelayProcessor#_onNoResponseTimer: error starting action');
this._taskInProgress = null;
return;
}
this.ep.once('playback-start', (evt) => {
this.logger.debug({evt}, 'got playback-start');
if (!this._active) {
this.logger.info({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer: killing audio immediately');
this.ep.api('uuid_break', this.ep.uuid)
.catch((err) => this.logger.info(err,
'ActionHookDelayProcessor#_onNoResponseTimer Error killing audio'));
}
});
this.ep.once('playback-stop', (evt) => {
this._taskInProgress = null;
if (this._sayResolver) {
/* we were waiting for the play to finish before continuing to next task */
this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer got playback-stop');
this._sayResolver();
this._sayResolver = null;
}
else {
/* possibly start the no response timer again */
if (this._active && this.retries > 0 && this._retryCount < this.retries && this.noResponseTimeout > 0) {
this.logger.debug({evt}, 'ActionHookDelayProcessor#_onNoResponseTimer: playback-stop on play/say action');
const timeoutMs = this.noResponseTimeout * 1000;
this._noResponseTimer = setTimeout(this._onNoResponseTimer.bind(this), timeoutMs);
}
}
});
this._retryCount++;
}
_onNoResponseGiveUpTimer() {
this._active = false;
this.logger.info('ActionHookDelayProcessor#_onNoResponseGiveUpTimer');
this.stop().catch((err) => {});
this.emit('giveup');
}
}
module.exports = ActionHookDelayProcessor;

View File

@@ -210,7 +210,8 @@ module.exports = (logger) => {
account_sid: cs.accountSid, account_sid: cs.accountSid,
alert_type: AlertType.STT_FAILURE, alert_type: AlertType.STT_FAILURE,
vendor: vendor, vendor: vendor,
detail: err.message detail: err.message,
target_sid: cs.callSid
}); });
}).catch((err) => logger.info({err}, 'Error generating alert for tts failure')); }).catch((err) => logger.info({err}, 'Error generating alert for tts failure'));
@@ -245,7 +246,10 @@ module.exports = (logger) => {
const amd = ep.amd = new Amd(logger, cs, opts); const amd = ep.amd = new Amd(logger, cs, opts);
const {vendor, language} = amd; const {vendor, language} = amd;
let sttCredentials = amd.sttCredentials; let sttCredentials = amd.sttCredentials;
const hints = voicemailHints[language] || []; // hints from configuration might be too long for specific language and vendor that make transcribe freeswitch
// modules cannot connect to the vendor. hints is used in next step to validate if the transcription
// matchs voice mail hints.
const hints = [];
if (vendor === 'nuance' && sttCredentials.client_id) { if (vendor === 'nuance' && sttCredentials.client_id) {
/* get nuance access token */ /* get nuance access token */

View File

@@ -26,7 +26,7 @@ class BackgroundTaskManager extends Emitter {
return this.tasks.size; return this.tasks.size;
} }
async newTask(type, opts) { async newTask(type, opts, sticky = false) {
this.logger.info({opts}, `initiating Background task ${type}`); this.logger.info({opts}, `initiating Background task ${type}`);
if (this.tasks.has(type)) { if (this.tasks.has(type)) {
this.logger.info(`Background task ${type} is running, skipped`); this.logger.info(`Background task ${type} is running, skipped`);
@@ -52,6 +52,7 @@ class BackgroundTaskManager extends Emitter {
if (task) { if (task) {
this.tasks.set(type, task); this.tasks.set(type, task);
} }
if (task && sticky) task.sticky = true;
return task; return task;
} }
@@ -116,7 +117,8 @@ class BackgroundTaskManager extends Emitter {
this._taskCompleted('bargeIn', task); this._taskCompleted('bargeIn', task);
if (task.sticky && !this.cs.callGone && !this.cs._stopping) { if (task.sticky && !this.cs.callGone && !this.cs._stopping) {
this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn'); this.logger.info('BackgroundTaskManager:_initBargeIn: restarting background bargeIn');
this.newTask('bargeIn', opts); this._bargeInHandled = false;
this.newTask('bargeIn', opts, true);
} }
return; return;
}) })
@@ -185,6 +187,8 @@ class BackgroundTaskManager extends Emitter {
} }
_bargeInTaskCompleted(evt) { _bargeInTaskCompleted(evt) {
if (this._bargeInHandled) return;
this._bargeInHandled = true;
this.logger.debug({evt}, this.logger.debug({evt},
'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn, emitting bargein-done event'); 'BackgroundTaskManager:_bargeInTaskCompleted on event from background bargeIn, emitting bargein-done event');
this.emit('bargeIn-done', evt); this.emit('bargeIn-done', evt);

View File

@@ -97,6 +97,10 @@
"Transcription": "soniox_transcribe::transcription", "Transcription": "soniox_transcribe::transcription",
"Error": "soniox_transcribe::error" "Error": "soniox_transcribe::error"
}, },
"VerbioTranscriptionEvents": {
"Transcription": "verbio_transcribe::transcription",
"Error": "verbio_transcribe::error"
},
"CobaltTranscriptionEvents": { "CobaltTranscriptionEvents": {
"Transcription": "cobalt_speech::transcription", "Transcription": "cobalt_speech::transcription",
"CompileContext": "cobalt_speech::compile_context_response", "CompileContext": "cobalt_speech::compile_context_response",
@@ -134,6 +138,9 @@
"ConnectFailure": "assemblyai_transcribe::connect_failed", "ConnectFailure": "assemblyai_transcribe::connect_failed",
"Connect": "assemblyai_transcribe::connect" "Connect": "assemblyai_transcribe::connect"
}, },
"VadDetection": {
"Detection": "vad_detect:detection"
},
"ListenEvents": { "ListenEvents": {
"Connect": "mod_audio_fork::connect", "Connect": "mod_audio_fork::connect",
"ConnectFailure": "mod_audio_fork::connect_failed", "ConnectFailure": "mod_audio_fork::connect_failed",

View File

@@ -41,6 +41,7 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.access_key_id = o.access_key_id; obj.access_key_id = o.access_key_id;
obj.secret_access_key = o.secret_access_key; obj.secret_access_key = o.secret_access_key;
obj.role_arn = o.role_arn;
obj.aws_region = o.aws_region; obj.aws_region = o.aws_region;
} }
else if ('microsoft' === obj.vendor) { else if ('microsoft' === obj.vendor) {
@@ -76,6 +77,7 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.deepgram_stt_uri = o.deepgram_stt_uri; obj.deepgram_stt_uri = o.deepgram_stt_uri;
obj.deepgram_tts_uri = o.deepgram_tts_uri;
obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls; obj.deepgram_stt_use_tls = o.deepgram_stt_use_tls;
} }
else if ('soniox' === obj.vendor) { else if ('soniox' === obj.vendor) {
@@ -100,6 +102,11 @@ const speechMapper = (cred) => {
obj.user_id = o.user_id; obj.user_id = o.user_id;
obj.voice_engine = o.voice_engine; obj.voice_engine = o.voice_engine;
obj.options = o.options; obj.options = o.options;
} else if ('rimelabs' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key;
obj.model_id = o.model_id;
obj.options = o.options;
} else if ('assemblyai' === obj.vendor) { } else if ('assemblyai' === obj.vendor) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
@@ -107,6 +114,11 @@ const speechMapper = (cred) => {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.api_key = o.api_key; obj.api_key = o.api_key;
obj.model_id = o.model_id; obj.model_id = o.model_id;
} else if ('verbio' === obj.vendor) {
const o = JSON.parse(decrypt(credential));
obj.client_id = o.client_id;
obj.client_secret = o.client_secret;
obj.engine_version = o.engine_version;
} else if (obj.vendor.startsWith('custom:')) { } else if (obj.vendor.startsWith('custom:')) {
const o = JSON.parse(decrypt(credential)); const o = JSON.parse(decrypt(credential));
obj.auth_token = o.auth_token; obj.auth_token = o.auth_token;

View File

@@ -12,6 +12,7 @@ const {
JAMBONES_TIME_SERIES_HOST, JAMBONES_TIME_SERIES_HOST,
JAMBONES_ESL_LISTEN_ADDRESS, JAMBONES_ESL_LISTEN_ADDRESS,
PORT, PORT,
HTTP_IP,
NODE_ENV, NODE_ENV,
} = require('../config'); } = require('../config');
const Registrar = require('@jambonz/mw-registrar'); const Registrar = require('@jambonz/mw-registrar');
@@ -171,7 +172,7 @@ function installSrfLocals(srf, logger) {
retrieveFromSortedSet, retrieveFromSortedSet,
retrieveByPatternSortedSet, retrieveByPatternSortedSet,
sortedSetLength, sortedSetLength,
sortedSetPositionByPattern sortedSetPositionByPattern,
} = require('@jambonz/realtimedb-helpers')({}, logger, tracer); } = require('@jambonz/realtimedb-helpers')({}, logger, tracer);
const registrar = new Registrar(logger, client); const registrar = new Registrar(logger, client);
const { const {
@@ -179,6 +180,8 @@ function installSrfLocals(srf, logger) {
addFileToCache, addFileToCache,
getNuanceAccessToken, getNuanceAccessToken,
getIbmAccessToken, getIbmAccessToken,
getAwsAuthToken,
getVerbioAccessToken
} = require('@jambonz/speech-utils')({}, logger); } = require('@jambonz/speech-utils')({}, logger);
const { const {
writeAlerts, writeAlerts,
@@ -191,7 +194,8 @@ function installSrfLocals(srf, logger) {
let localIp; let localIp;
try { try {
localIp = ip.address(); // Either use the configured IP address or call ip.address() to find it
localIp = HTTP_IP || ip.address();
} catch (err) { } catch (err) {
logger.error({err}, 'installSrfLocals - error detecting local ipv4 address'); logger.error({err}, 'installSrfLocals - error detecting local ipv4 address');
} }
@@ -216,6 +220,7 @@ function installSrfLocals(srf, logger) {
listCalls, listCalls,
deleteCall, deleteCall,
synthAudio, synthAudio,
getAwsAuthToken,
addFileToCache, addFileToCache,
createHash, createHash,
retrieveHash, retrieveHash,
@@ -237,7 +242,8 @@ function installSrfLocals(srf, logger) {
retrieveFromSortedSet, retrieveFromSortedSet,
retrieveByPatternSortedSet, retrieveByPatternSortedSet,
sortedSetLength, sortedSetLength,
sortedSetPositionByPattern sortedSetPositionByPattern,
getVerbioAccessToken
}, },
parentLogger: logger, parentLogger: logger,
getSBC, getSBC,

View File

@@ -441,7 +441,7 @@ class SingleDialer extends Emitter {
}); });
app.requestor.request('session:adulting', '/adulting', { app.requestor.request('session:adulting', '/adulting', {
...cs.callInfo.toJSON(), ...cs.callInfo.toJSON(),
parentCallInfo: this.parentCallInfo parentCallInfo: this.parentCallInfo.toJSON()
}).catch((err) => { }).catch((err) => {
newLogger.error({err}, 'doAdulting: error sending adulting request'); newLogger.error({err}, 'doAdulting: error sending adulting request');
}); });

View File

@@ -45,7 +45,8 @@ const stickyVars = {
'DEEPGRAM_SPEECH_ENDPOINTING', 'DEEPGRAM_SPEECH_ENDPOINTING',
'DEEPGRAM_SPEECH_UTTERANCE_END_MS', 'DEEPGRAM_SPEECH_UTTERANCE_END_MS',
'DEEPGRAM_SPEECH_VAD_TURNOFF', 'DEEPGRAM_SPEECH_VAD_TURNOFF',
'DEEPGRAM_SPEECH_TAG' 'DEEPGRAM_SPEECH_TAG',
'DEEPGRAM_SPEECH_MODEL_VERSION'
], ],
aws: [ aws: [
'AWS_VOCABULARY_NAME', 'AWS_VOCABULARY_NAME',
@@ -141,7 +142,6 @@ const optimalDeepramModels = {
tr: ['nova-2', 'nova-2'], tr: ['nova-2', 'nova-2'],
uk: ['nova-2', 'nova-2'] uk: ['nova-2', 'nova-2']
}; };
const selectDefaultDeepgramModel = (task, language) => { const selectDefaultDeepgramModel = (task, language) => {
if (language in optimalDeepramModels) { if (language in optimalDeepramModels) {
const [gather, transcribe] = optimalDeepramModels[language]; const [gather, transcribe] = optimalDeepramModels[language];
@@ -150,6 +150,24 @@ const selectDefaultDeepgramModel = (task, language) => {
return 'base'; return 'base';
}; };
const optimalGoogleModels = {
'v1' : {
'en-IN':['telephony', 'latest_long']
},
'v2' : {
'en-IN':['telephony', 'long']
}
};
const selectDefaultGoogleModel = (task, language, version) => {
const useV2 = version === 'v2';
if (language in optimalGoogleModels[version]) {
const [gather, transcribe] = optimalGoogleModels[version][language];
return task.name === TaskName.Gather ? gather : transcribe;
}
return task.name === TaskName.Gather ?
(useV2 ? 'telephony_short' : 'command_and_search') :
(useV2 ? 'long' : 'latest_long');
};
const consolidateTranscripts = (bufferedTranscripts, channel, language, vendor) => { const consolidateTranscripts = (bufferedTranscripts, channel, language, vendor) => {
if (bufferedTranscripts.length === 1) return bufferedTranscripts[0]; if (bufferedTranscripts.length === 1) return bufferedTranscripts[0];
let totalConfidence = 0; let totalConfidence = 0;
@@ -316,8 +334,10 @@ const normalizeIbm = (evt, channel, language) => {
const normalizeGoogle = (evt, channel, language) => { const normalizeGoogle = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const language_code = evt.language_code || language;
return { return {
language_code: language, language_code: language_code,
channel_tag: channel, channel_tag: channel,
is_final: evt.is_final, is_final: evt.is_final,
alternatives: [evt.alternatives[0]], alternatives: [evt.alternatives[0]],
@@ -376,6 +396,20 @@ const normalizeNuance = (evt, channel, language) => {
}; };
}; };
const normalizeVerbio = (evt, channel, language) => {
const copy = JSON.parse(JSON.stringify(evt));
return {
language_code: language,
channel_tag: channel,
is_final: evt.is_final,
alternatives: evt.alternatives,
vendor: {
name: 'verbio',
evt: copy
}
};
};
const normalizeMicrosoft = (evt, channel, language, punctuation = true) => { const normalizeMicrosoft = (evt, channel, language, punctuation = true) => {
const copy = JSON.parse(JSON.stringify(evt)); const copy = JSON.parse(JSON.stringify(evt));
const nbest = evt.NBest; const nbest = evt.NBest;
@@ -463,6 +497,8 @@ module.exports = (logger) => {
return normalizeCobalt(evt, channel, language); return normalizeCobalt(evt, channel, language);
case 'assemblyai': case 'assemblyai':
return normalizeAssemblyAi(evt, channel, language, shortUtterance); return normalizeAssemblyAi(evt, channel, language, shortUtterance);
case 'verbio':
return normalizeVerbio(evt, channel, language);
default: default:
if (vendor.startsWith('custom:')) { if (vendor.startsWith('custom:')) {
return normalizeCustom(evt, channel, language, vendor); return normalizeCustom(evt, channel, language, vendor);
@@ -474,23 +510,13 @@ module.exports = (logger) => {
const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => { const setChannelVarsForStt = (task, sttCredentials, language, rOpts = {}) => {
let opts = {}; let opts = {};
const {enable, voiceMs = 0, mode = -1} = rOpts.vad || {};
const vad = {enable, voiceMs, mode};
const vendor = rOpts.vendor; const vendor = rOpts.vendor;
/* voice activity detection works across vendors */
opts = {
...opts,
...(vad.enable && {START_RECOGNIZING_ON_VAD: 1}),
...(vad.enable && vad.voiceMs && {RECOGNIZER_VAD_VOICE_MS: vad.voiceMs}),
...(vad.enable && typeof vad.mode === 'number' && {RECOGNIZER_VAD_MODE: vad.mode}),
};
if ('google' === vendor) { if ('google' === vendor) {
const useV2 = rOpts.googleOptions?.serviceVersion === 'v2'; const useV2 = rOpts.googleOptions?.serviceVersion === 'v2';
const model = task.name === TaskName.Gather ? const version = useV2 ? 'v2' : 'v1';
(useV2 ? 'telephony_short' : 'command_and_search') : let {model} = rOpts;
(useV2 ? 'long' : 'latest_long'); model = model || selectDefaultGoogleModel(task, language, version);
opts = { opts = {
...opts, ...opts,
...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}), ...(sttCredentials && {GOOGLE_APPLICATION_CREDENTIALS: JSON.stringify(sttCredentials.credentials)}),
@@ -552,9 +578,10 @@ module.exports = (logger) => {
...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}), ...(rOpts.vocabularyFilterName && {AWS_VOCABULARY_FILTER_NAME: rOpts.vocabularyFilterName}),
...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}), ...(rOpts.filterMethod && {AWS_VOCABULARY_FILTER_METHOD: rOpts.filterMethod}),
...(sttCredentials && { ...(sttCredentials && {
AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId, ...(sttCredentials.accessKeyId && {AWS_ACCESS_KEY_ID: sttCredentials.accessKeyId}),
AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey, ...(sttCredentials.secretAccessKey && {AWS_SECRET_ACCESS_KEY: sttCredentials.secretAccessKey}),
AWS_REGION: sttCredentials.region AWS_REGION: sttCredentials.region,
...(sttCredentials.sessionToken && {AWS_SESSION_TOKEN: sttCredentials.sessionToken}),
}), }),
}; };
} }
@@ -583,6 +610,8 @@ module.exports = (logger) => {
{AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}), {AZURE_SPEECH_SEGMENTATION_SILENCE_TIMEOUT_MS: azureOptions.speechSegmentationSilenceTimeoutMs}),
...(azureOptions.languageIdMode && ...(azureOptions.languageIdMode &&
{AZURE_LANGUAGE_ID_MODE: azureOptions.languageIdMode}), {AZURE_LANGUAGE_ID_MODE: azureOptions.languageIdMode}),
...(azureOptions.postProcessing &&
{AZURE_POST_PROCESSING_OPTION: azureOptions.postProcessing}),
...(sttCredentials && { ...(sttCredentials && {
...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}), ...(sttCredentials.api_key && {AZURE_SUBSCRIPTION_KEY: sttCredentials.api_key}),
...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}), ...(sttCredentials.region && {AZURE_REGION: sttCredentials.region}),
@@ -690,13 +719,17 @@ module.exports = (logger) => {
...(deepgramOptions.keywords) && ...(deepgramOptions.keywords) &&
{DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')}, {DEEPGRAM_SPEECH_KEYWORDS: deepgramOptions.keywords.join(',')},
...('endpointing' in deepgramOptions) && ...('endpointing' in deepgramOptions) &&
{DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing === false ? 'false' : deepgramOptions.endpointing}, {DEEPGRAM_SPEECH_ENDPOINTING: deepgramOptions.endpointing === false ? 'false' : deepgramOptions.endpointing,
// default DEEPGRAM_SPEECH_UTTERANCE_END_MS is 1000, will be override by user settings later if there is.
DEEPGRAM_SPEECH_UTTERANCE_END_MS: 1000},
...(deepgramOptions.utteranceEndMs) && ...(deepgramOptions.utteranceEndMs) &&
{DEEPGRAM_SPEECH_UTTERANCE_END_MS: deepgramOptions.utteranceEndMs}, {DEEPGRAM_SPEECH_UTTERANCE_END_MS: deepgramOptions.utteranceEndMs},
...(deepgramOptions.vadTurnoff) && ...(deepgramOptions.vadTurnoff) &&
{DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff}, {DEEPGRAM_SPEECH_VAD_TURNOFF: deepgramOptions.vadTurnoff},
...(deepgramOptions.tag) && ...(deepgramOptions.tag) &&
{DEEPGRAM_SPEECH_TAG: deepgramOptions.tag} {DEEPGRAM_SPEECH_TAG: deepgramOptions.tag},
...(deepgramOptions.version) &&
{DEEPGRAM_SPEECH_MODEL_VERSION: deepgramOptions.version}
}; };
} }
else if ('soniox' === vendor) { else if ('soniox' === vendor) {
@@ -803,9 +836,28 @@ module.exports = (logger) => {
...(rOpts.hints?.length > 0 && ...(rOpts.hints?.length > 0 &&
{ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)}) {ASSEMBLYAI_WORD_BOOST: JSON.stringify(rOpts.hints)})
}; };
} } else if ('verbio' === vendor) {
else if (vendor.startsWith('custom:')) { const {verbioOptions = {}} = rOpts;
opts = {
...opts,
...(sttCredentials.access_token && { VERBIO_ACCESS_TOKEN: sttCredentials.access_token}),
...(sttCredentials.engine_version && {VERBIO_ENGINE_VERSION: sttCredentials.engine_version}),
...(language && {VERBIO_LANGUAGE: language}),
...(verbioOptions.enable_formatting && {VERBIO_ENABLE_FORMATTING: verbioOptions.enable_formatting}),
...(verbioOptions.enable_diarization && {VERBIO_ENABLE_DIARIZATION: verbioOptions.enable_diarization}),
...(verbioOptions.topic && {VERBIO_TOPIC: verbioOptions.topic}),
...(verbioOptions.inline_grammar && {VERBIO_INLINE_GRAMMAR: verbioOptions.inline_grammar}),
...(verbioOptions.grammar_uri && {VERBIO_GRAMMAR_URI: verbioOptions.grammar_uri}),
...(verbioOptions.label && {VERBIO_LABEL: verbioOptions.label}),
...(verbioOptions.recognition_timeout && {VERBIO_RECOGNITION_TIMEOUT: verbioOptions.recognition_timeout}),
...(verbioOptions.speech_complete_timeout &&
{VERBIO_SPEECH_COMPLETE_TIMEOUT: verbioOptions.speech_complete_timeout}),
...(verbioOptions.speech_incomplete_timeout &&
{VERBIO_SPEECH_INCOMPLETE_TIMEOUT: verbioOptions.speech_incomplete_timeout}),
};
} else if (vendor.startsWith('custom:')) {
let {options = {}} = rOpts; let {options = {}} = rOpts;
const {sampleRate} = rOpts.customOptions || {};
const {auth_token, custom_stt_url} = sttCredentials; const {auth_token, custom_stt_url} = sttCredentials;
options = { options = {
...options, ...options,
@@ -813,14 +865,15 @@ module.exports = (logger) => {
{hints: rOpts.hints}), {hints: rOpts.hints}),
...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' && ...(rOpts.hints?.length > 0 && typeof rOpts.hints[0] === 'object' &&
{hints: JSON.stringify(rOpts.hints)}), {hints: JSON.stringify(rOpts.hints)}),
...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost}) ...(typeof rOpts.hintsBoost === 'number' && {hintsBoost: rOpts.hintsBoost}),
...(task.cs?.callSid && {callSid: task.cs.callSid})
}; };
opts = { opts = {
...opts, ...opts,
...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}), ...(auth_token && {JAMBONZ_STT_API_KEY: auth_token}),
JAMBONZ_STT_URL: custom_stt_url, JAMBONZ_STT_URL: custom_stt_url,
...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}), ...(Object.keys(options).length > 0 && {JAMBONZ_STT_OPTIONS: JSON.stringify(options)}),
...(sampleRate && {JAMBONZ_STT_SAMPLING: sampleRate})
}; };
} }

View File

@@ -56,6 +56,12 @@ class WsRequestor extends BaseRequestor {
} }
if (type === 'session:new') this.call_sid = params.callSid; if (type === 'session:new') this.call_sid = params.callSid;
if (type === 'session:reconnect') {
this._reconnectPromise = new Promise((resolve, reject) => {
this._reconnectResolve = resolve;
this._reconnectReject = reject;
});
}
/* if we have an absolute url, and it is http then do a standard webhook */ /* if we have an absolute url, and it is http then do a standard webhook */
if (this._isAbsoluteUrl(url) && url.startsWith('http')) { if (this._isAbsoluteUrl(url) && url.startsWith('http')) {
@@ -71,20 +77,23 @@ class WsRequestor extends BaseRequestor {
} }
/* connect if necessary */ /* connect if necessary */
const queueMsg = () => {
this.logger.debug(
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
if (wantsAck) {
const p = new Promise((resolve, reject) => {
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
});
return p;
}
else {
this.queuedMsg.push({type, hook, params, httpHeaders});
}
return;
};
if (!this.ws) { if (!this.ws) {
if (this.connectInProgress) { if (this.connectInProgress) {
this.logger.debug( return queueMsg();
`WsRequestor:request(${this.id}) - queueing ${type} message since we are connecting`);
if (wantsAck) {
const p = new Promise((resolve, reject) => {
this.queuedMsg.push({type, hook, params, httpHeaders, promise: {resolve, reject}});
});
return p;
}
else {
this.queuedMsg.push({type, hook, params, httpHeaders});
}
return;
} }
this.connectInProgress = true; this.connectInProgress = true;
this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`); this.logger.debug(`WsRequestor:request(${this.id}) - connecting since we do not have a connection for ${type}`);
@@ -102,6 +111,10 @@ class WsRequestor extends BaseRequestor {
return Promise.reject(err); return Promise.reject(err);
} }
} }
// If jambonz wait for ack from reconnect, queue the msg until reconnect is acked
if (type !== 'session:reconnect' && this._reconnectPromise) {
return queueMsg();
}
assert(this.ws); assert(this.ws);
/* prepare and send message */ /* prepare and send message */
@@ -139,6 +152,18 @@ class WsRequestor extends BaseRequestor {
} }
}; };
const rejectQueuedMsgs = (err) => {
if (this.queuedMsg.length > 0) {
for (const {promise} of this.queuedMsg) {
this.logger.debug(`WsRequestor:request - preparing queued ${type} for rejectQueuedMsgs`);
if (promise) {
promise.reject(err);
}
}
this.queuedMsg.length = 0;
}
};
//this.logger.debug({obj}, `websocket: sending (${url})`); //this.logger.debug({obj}, `websocket: sending (${url})`);
/* special case: reconnecting before we received ack to session:new */ /* special case: reconnecting before we received ack to session:new */
@@ -179,16 +204,37 @@ class WsRequestor extends BaseRequestor {
this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`); this.logger.debug({response}, `WsRequestor:request ${url} succeeded in ${rtt}ms`);
this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']); this.stats.histogram('app.hook.ws_response_time', rtt, ['hook_type:app']);
resolve(response); resolve(response);
if (this._reconnectResolve) {
this._reconnectResolve();
}
}, },
failure: (err) => { failure: (err) => {
if (this._reconnectReject) {
this._reconnectReject(err);
}
clearTimeout(timer); clearTimeout(timer);
reject(err); reject(err);
} }
}); });
/* send the message */ /* send the message */
this.ws.send(JSON.stringify(obj), () => { this.ws.send(JSON.stringify(obj), async() => {
this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`); this.logger.debug({obj}, `WsRequestor:request websocket: sent (${url})`);
// If session:reconnect is waiting for ack, hold here until ack to send queuedMsgs
if (this._reconnectPromise) {
try {
await this._reconnectPromise;
} catch (err) {
// bad thing happened to session:recconnect
rejectQueuedMsgs(err);
this.emit('reconnect-error');
return;
} finally {
this._reconnectPromise = null;
this._reconnectResolve = null;
this._reconnectReject = null;
}
}
sendQueuedMsgs(); sendQueuedMsgs();
}); });
}); });

4855
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "jambonz-feature-server", "name": "jambonz-feature-server",
"version": "0.9.0", "version": "0.9.2",
"main": "app.js", "main": "app.js",
"engines": { "engines": {
"node": ">= 18.x" "node": ">= 18.x"
@@ -27,14 +27,14 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-auto-scaling": "^3.549.0", "@aws-sdk/client-auto-scaling": "^3.549.0",
"@aws-sdk/client-sns": "^3.549.0", "@aws-sdk/client-sns": "^3.549.0",
"@jambonz/db-helpers": "^0.9.3", "@jambonz/db-helpers": "^0.9.6",
"@jambonz/http-health-check": "^0.0.1", "@jambonz/http-health-check": "^0.0.1",
"@jambonz/mw-registrar": "^0.2.7", "@jambonz/mw-registrar": "^0.2.7",
"@jambonz/realtimedb-helpers": "^0.8.8", "@jambonz/realtimedb-helpers": "^0.8.8",
"@jambonz/speech-utils": "^0.0.50", "@jambonz/speech-utils": "^0.1.15",
"@jambonz/stats-collector": "^0.1.9", "@jambonz/stats-collector": "^0.1.10",
"@jambonz/time-series": "^0.2.8", "@jambonz/time-series": "^0.2.9",
"@jambonz/verb-specifications": "^0.0.69", "@jambonz/verb-specifications": "^0.0.76",
"@opentelemetry/api": "^1.8.0", "@opentelemetry/api": "^1.8.0",
"@opentelemetry/exporter-jaeger": "^1.23.0", "@opentelemetry/exporter-jaeger": "^1.23.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0", "@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
@@ -47,8 +47,8 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"debug": "^4.3.4", "debug": "^4.3.4",
"deepcopy": "^2.1.0", "deepcopy": "^2.1.0",
"drachtio-fsmrf": "^3.0.40", "drachtio-fsmrf": "^3.0.45",
"drachtio-srf": "^4.5.31", "drachtio-srf": "^4.5.35",
"express": "^4.19.2", "express": "^4.19.2",
"express-validator": "^7.0.1", "express-validator": "^7.0.1",
"ip": "^2.0.1", "ip": "^2.0.1",
@@ -58,13 +58,13 @@
"polly-ssml-split": "^0.1.0", "polly-ssml-split": "^0.1.0",
"proxyquire": "^2.1.3", "proxyquire": "^2.1.3",
"sdp-transform": "^2.14.2", "sdp-transform": "^2.14.2",
"short-uuid": "^4.2.2", "short-uuid": "^5.1.0",
"sinon": "^17.0.1", "sinon": "^17.0.1",
"to-snake-case": "^1.0.0", "to-snake-case": "^1.0.0",
"undici": "^6.11.1", "undici": "^6.19.4",
"uuid-random": "^1.3.2", "uuid-random": "^1.3.2",
"verify-aws-sns-signature": "^0.1.0", "verify-aws-sns-signature": "^0.1.0",
"ws": "^8.16.0", "ws": "^8.18.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -57,7 +57,7 @@ services:
condition: service_healthy condition: service_healthy
freeswitch: freeswitch:
image: drachtio/drachtio-freeswitch-mrf:0.7.3 image: drachtio/drachtio-freeswitch-mrf:latest
restart: always restart: always
command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100 command: freeswitch --rtp-range-start 20000 --rtp-range-end 20100
environment: environment: