Skip to content

Commit 964e03a

Browse files
committed
Add example code from webinar
0 parents  commit 964e03a

File tree

5 files changed

+247
-0
lines changed

5 files changed

+247
-0
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Sentiment analysis of voice calls using IBM Watson
2+
==================================================
3+
4+
This is the companion code for the ["Add Sentiment Analysis to Your Inbound Call Flow with IBM Watson and Nexmo" webinar](https://attendee.gotowebinar.com/recording/7952180850491069704). Please view the webinar
5+
recording for full details on how to run this example.
6+
7+
Quickstart
8+
----------
9+
10+
There are several environment variables in `call_objects.py` and `ws_server.py` which should be
11+
set to the correct values in your environment.
12+
13+
NEXMO_APPLICATION_ID
14+
TEST_HANDSET
15+
NEXMO_FROM_NUMBER
16+
TONE_ANALYZER_USERNAME
17+
TONE_ANALYZER_PASSWORD
18+
TRANSCRIBER_USERNAME
19+
TRANSCRIBER_PASSWORD
20+
21+
You should also update all urls within `call_objects.py` to point to your ngrok address.
22+
23+
# This code has been tested with Python 3.6.3
24+
# Install dependencies
25+
pip install -r requirements.txt
26+
27+
# Run Hug server
28+
hug -f call_objects.py
29+
30+
# Run Tornado server
31+
python ws_server.py
32+

bensound-acousticbreeze.mp3

2.1 MB
Binary file not shown.

call_objects.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
import uuid
3+
import hug
4+
import nexmo
5+
6+
7+
class CallObjectServer():
8+
9+
def __init__(self):
10+
self.conversation_name = str(uuid.uuid4())
11+
self.nexmo_client = nexmo.Client(
12+
application_id=os.environ['NEXMO_APPLICATION_ID'],
13+
private_key='private.key'
14+
)
15+
16+
def start(self):
17+
18+
self.nexmo_client.create_call({
19+
'to': [{'type': 'phone', 'number': os.environ['TEST_HANDSET']}],
20+
'from': {'type': 'phone', 'number': os.environ['NEXMO_FROM_NUMBER']},
21+
'answer_url': ['https://nexmo-sentiment.ngrok.io/moderator']
22+
})
23+
24+
self.ws_call = self.nexmo_client.create_call({
25+
'to': [
26+
{
27+
"type": "websocket",
28+
"uri": "ws://nexmo-sentiment-sockets.ngrok.io/audio",
29+
"content-type": "audio/l16;rate=16000",
30+
"headers": {}
31+
}
32+
],
33+
'from': {'type': 'phone', 'number': os.environ['NEXMO_FROM_NUMBER']},
34+
'answer_url': ['https://nexmo-sentiment.ngrok.io/attendee']
35+
})
36+
37+
return [
38+
{
39+
"action": "talk",
40+
"text": "Please wait while we connect you"
41+
},
42+
{
43+
"action": "conversation",
44+
"name": self.conversation_name,
45+
"startOnEnter": "false",
46+
"musicOnHoldUrl": ["https://nexmo-sentiment.ngrok.io/hold.mp3"]
47+
}
48+
]
49+
50+
def moderator(self):
51+
return [
52+
{
53+
"action": "conversation",
54+
"name": self.conversation_name,
55+
"record": "true",
56+
"endOnExit": "true"
57+
}
58+
]
59+
60+
def attendee(self):
61+
return [
62+
{
63+
"action": "conversation",
64+
"name": self.conversation_name,
65+
"startOnEnter": "false",
66+
"musicOnHoldUrl": ["https://nexmo-sentiment.ngrok.io/hold.mp3"]
67+
}
68+
]
69+
70+
def static(self):
71+
return open('bensound-acousticbreeze.mp3', mode='rb')
72+
73+
74+
server = CallObjectServer()
75+
76+
router = hug.route.API(__name__)
77+
router.get('/')(server.start)
78+
router.get('/hold.mp3', output=hug.output_format.file)(server.static)
79+
router.get('/moderator')(server.moderator)
80+
router.get('/attendee')(server.attendee)

requirements.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
asn1crypto==0.24.0
2+
certifi==2018.1.18
3+
cffi==1.11.4
4+
chardet==3.0.4
5+
cryptography==2.1.4
6+
falcon==1.3.0
7+
hug==2.3.2
8+
idna==2.6
9+
logzero==1.3.1
10+
nexmo==2.0.0
11+
pycparser==2.18
12+
PyJWT==1.5.3
13+
pyOpenSSL==17.5.0
14+
pysolr==3.7.0
15+
python-dateutil==2.6.1
16+
python-mimeparse==1.6.0
17+
requests==2.18.4
18+
six==1.11.0
19+
tornado==4.5.3
20+
urllib3==1.22
21+
watson-developer-cloud==1.0.2

ws_server.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import os
2+
import json
3+
4+
from watson_developer_cloud import ToneAnalyzerV3
5+
import tornado.httpserver
6+
import tornado.ioloop
7+
import tornado.web
8+
import tornado.websocket
9+
from tornado import gen
10+
import requests
11+
from logzero import logger
12+
13+
14+
class DashboardHandler(tornado.websocket.WebSocketHandler):
15+
16+
waiters = set()
17+
18+
def check_origin(self, origin):
19+
return True
20+
21+
def open(self):
22+
logger.warning('Dashboard socket open')
23+
DashboardHandler.waiters.add(self)
24+
25+
def on_close(self):
26+
logger.warning('Dashboard socket closed')
27+
DashboardHandler.waiters.remove(self)
28+
29+
@classmethod
30+
def send_updates(cls, tones):
31+
logger.warning('Sending update')
32+
logger.warning(tones)
33+
34+
for waiter in cls.waiters:
35+
try:
36+
waiter.write_message(tones)
37+
except:
38+
pass
39+
40+
41+
class AudioHandler(tornado.websocket.WebSocketHandler):
42+
43+
def initialize(self, **kwargs):
44+
pass
45+
46+
def open(self):
47+
logger.warning('Audio socket open')
48+
self.transcriber = tornado.websocket.websocket_connect(
49+
'wss://stream.watsonplatform.net/speech-to-text/api/v1/recognize?watson-token={token}&model={model}'.format(
50+
token=self.transcriber_token(),
51+
model='en-UK_NarrowbandModel'
52+
),
53+
on_message_callback=self.on_transcriber_message
54+
)
55+
56+
self.tone_analyzer = ToneAnalyzerV3(
57+
username=os.environ['TONE_ANALYZER_USERNAME'],
58+
password=os.environ['TONE_ANALYZER_PASSWORD'],
59+
version='2016-05-19'
60+
)
61+
62+
def transcriber_token(self):
63+
resp = requests.get(
64+
'https://stream.watsonplatform.net/authorization/api/v1/token',
65+
auth=(os.environ['TRANSCRIBER_USERNAME'], os.environ['TRANSCRIBER_PASSWORD']),
66+
params={'url': "https://stream.watsonplatform.net/speech-to-text/api"}
67+
)
68+
69+
return resp.content.decode('utf-8')
70+
71+
@gen.coroutine
72+
def on_message(self, message):
73+
transcriber = yield self.transcriber
74+
75+
if type(message) != str:
76+
transcriber.write_message(message, binary=True)
77+
else:
78+
logger.error(message)
79+
data = json.loads(message)
80+
data['action'] = "start"
81+
data['continuous'] = True
82+
data['interim_results'] = True
83+
transcriber.write_message(json.dumps(data), binary=False)
84+
85+
@gen.coroutine
86+
def on_close(self):
87+
logger.warning('Audio socket closed')
88+
89+
transcriber = yield self.transcriber
90+
data = {'action': 'stop'}
91+
transcriber.write_message(json.dumps(data), binary=False)
92+
transcriber.close()
93+
94+
def on_transcriber_message(self, message):
95+
if message:
96+
message = json.loads(message)
97+
if 'results' in message:
98+
transcript = message['results'][0]['alternatives'][0]['transcript']
99+
tone_results = self.tone_analyzer.tone(text=transcript)
100+
tones = tone_results['document_tone']['tone_categories'][0]['tones']
101+
102+
DashboardHandler.send_updates(json.dumps(tones))
103+
104+
105+
106+
if __name__ == "__main__":
107+
application = tornado.web.Application([
108+
(r"/audio", AudioHandler),
109+
(r"/dashboard", DashboardHandler),
110+
])
111+
http_server = tornado.httpserver.HTTPServer(application)
112+
port = int(os.environ.get("PORT", 3000))
113+
http_server.listen(port)
114+
tornado.ioloop.IOLoop.instance().start()

0 commit comments

Comments
 (0)