Skip to content

Commit 20fab66

Browse files
author
Kyle Bridburg
authored
AISDK-197: Add sentiment analysis client (#91)
1 parent 5f07e16 commit 20fab66

15 files changed

+697
-10
lines changed

HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ History
9797

9898
2.16.0
9999
------------------
100+
* Add sentiment analysis client
100101
* Add source_config and notification_config job options to support customer provided urls with authentication headers
101102
* Deprecate media_url option, replace with source_config
102103
* Deprecate callback_url option, replace with notification_config
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Copyright 2022 REV
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""
15+
16+
import time
17+
from rev_ai import sentiment_analysis_client, apiclient
18+
from rev_ai.models import SentimentValue
19+
20+
21+
# String containing your access token
22+
access_token = "<your_access_token>"
23+
24+
# Create your api client
25+
client = sentiment_analysis_client.SentimentAnalysisClient(access_token)
26+
27+
# Submit a job with whatever text you want by changing this input
28+
text = "An umbrella or parasol is a folding canopy supported by wooden or metal ribs that is \
29+
usually mounted on a wooden, metal, or plastic pole. It is designed to protect a person \
30+
against rain or sunlight. The term umbrella is traditionally used when protecting oneself from \
31+
rain, with parasol used when protecting oneself from sunlight, though the terms continue to be \
32+
used interchangeably. Often the difference is the material used for the canopy; some parasols \
33+
are not waterproof, and some umbrellas are transparent. Umbrella canopies may be made of \
34+
fabric or flexible plastic. There are also combinations of parasol and umbrella that are \
35+
called en-tout-cas (French for 'in any case')."
36+
job = client.submit_job_from_text(text,
37+
metadata=None,
38+
callback_url=None,
39+
delete_after_seconds=None,
40+
language=None,
41+
notification_config=None)
42+
43+
# If you'd like to submit the transcript of an existing transcription job you can do so by
44+
# uncommenting the lines below
45+
#
46+
# async_job_id = "your_job_id"
47+
# async_api_client = apiclient.RevAiAPIClient(access_token)
48+
# transcript = async_api_client.get_transcript_object(async_job_id)
49+
# transcript_json = transcript
50+
# job = client.submit_job_from_transcript(transcript_json,
51+
# metadata=None,
52+
# callback_url=None,
53+
# delete_after_seconds=None,
54+
# language=None,
55+
# notification_config=None)
56+
57+
print("Submitted Job")
58+
59+
while True:
60+
# Obtains details of a job in json format
61+
job_details = client.get_job_details(job.id)
62+
status = job_details.status.name
63+
64+
print("Job Status : {}".format(status))
65+
66+
# Checks if the job has been completed. Please note that this is not the recommended way
67+
# of getting job status in a real application. For recommended methods of getting job status
68+
# please see our documentation on callback_urls here:
69+
# https://docs.rev.ai/resources/tutorials/get-started-api-webhooks/
70+
if status == "IN_PROGRESS":
71+
time.sleep(2)
72+
continue
73+
74+
elif status == "FAILED":
75+
print("Job Failed : {}".format(job_details.failure_detail))
76+
break
77+
78+
if status == "COMPLETED":
79+
# Getting a list of current sentiment analysis jobs connected with your account
80+
# The optional parameters limits the length of the list.
81+
# starting_after is a job id which causes the removal of
82+
# all jobs from the list which were created before that job
83+
list_of_jobs = client.get_list_of_jobs(limit=None, starting_after=None)
84+
85+
# obtain a list of topics and their scores for the job
86+
result = client.get_result_object(job.id, filter_for=None)
87+
remove_none_elements = lambda dictionary: {k: v for k, v in dictionary.items() if v}
88+
print([remove_none_elements(message.__dict__) for message in result.messages])
89+
90+
break
91+
92+
# Use the objects however you please
93+
# Once you are done with the job, you can delete it.
94+
# NOTE : This will PERMANENTLY DELETE all data related to a job. Exercise only
95+
# if you're sure you want to delete the job.
96+
#
97+
# client.delete_job(job.id)
98+
99+
print("Job Submission and Collection Finished.")

examples/topic_extraction_example.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@
2424
client = topic_extraction_client.TopicExtractionClient(access_token)
2525

2626
# Submit a job with whatever text you want by changing this input
27-
text = "<input_text>"
27+
text = "An umbrella or parasol is a folding canopy supported by wooden or metal ribs that is \
28+
usually mounted on a wooden, metal, or plastic pole. It is designed to protect a person \
29+
against rain or sunlight. The term umbrella is traditionally used when protecting oneself from \
30+
rain, with parasol used when protecting oneself from sunlight, though the terms continue to be \
31+
used interchangeably. Often the difference is the material used for the canopy; some parasols \
32+
are not waterproof, and some umbrellas are transparent. Umbrella canopies may be made of \
33+
fabric or flexible plastic. There are also combinations of parasol and umbrella that are \
34+
called en-tout-cas (French for 'in any case')."
2835
job = client.submit_job_from_text(text,
2936
metadata=None,
3037
delete_after_seconds=None,
@@ -41,7 +48,8 @@
4148
# job = client.submit_job_from_transcript(transcript_json,
4249
# metadata=None,
4350
# delete_after_seconds=None,
44-
# language=None)
51+
# language=None,
52+
# notification_config=None)
4553

4654
print("Submitted Job")
4755

src/rev_ai/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
from .models import Job, JobStatus, Account, Transcript, Monologue, Element, MediaConfig, \
77
CaptionType, CustomVocabulary, TopicExtractionJob, TopicExtractionResult, Topic, Informant, \
8-
SpeakerName, LanguageIdentificationJob, LanguageIdentificationResult, LanguageConfidence
8+
SpeakerName, LanguageIdentificationJob, LanguageIdentificationResult, LanguageConfidence, \
9+
SentimentAnalysisResult, SentimentValue, SentimentMessage, SentimentAnalysisJob, CustomerUrlData

src/rev_ai/models/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55
from .streaming import MediaConfig
66
from .asynchronous import Job, JobStatus, Account, Transcript, Monologue, Element, CaptionType, \
77
SpeakerName
8-
from .insights import TopicExtractionJob, TopicExtractionResult, Topic, Informant
8+
from .insights import TopicExtractionJob, TopicExtractionResult, Topic, Informant, \
9+
SentimentAnalysisResult, SentimentValue, SentimentMessage, SentimentAnalysisJob
910
from .language_id import LanguageIdentificationJob, LanguageIdentificationResult, LanguageConfidence
11+
from .customer_url_data import CustomerUrlData
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
22
"""Insights Models"""
33

4-
from .topic_extraction import TopicExtractionJob, TopicExtractionResult, Topic, Informant
4+
from .sentiment_analysis import SentimentAnalysisResult, SentimentValue, SentimentMessage, \
5+
SentimentAnalysisJob
6+
from .topic_extraction import TopicExtractionResult, Topic, Informant, TopicExtractionJob
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# -*- coding: utf-8 -*-
2+
"""Sentiment Analysis Models"""
3+
4+
from .sentiment_analysis_job import SentimentAnalysisJob
5+
from .sentiment_analysis_result import SentimentAnalysisResult, SentimentMessage
6+
from .sentiment_value import SentimentValue
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8 -*-
2+
"""Job model"""
3+
4+
from ...asynchronous.job_status import JobStatus
5+
6+
7+
class SentimentAnalysisJob:
8+
def __init__(
9+
self, id_, created_on, status,
10+
completed_on=None,
11+
callback_url=None,
12+
metadata=None,
13+
failure=None,
14+
failure_detail=None,
15+
word_count=None,
16+
delete_after_seconds=None):
17+
"""
18+
:param id_: unique id of job
19+
:param created_on: date and time at which this job was started
20+
:param status: current job status 'IN_PROGRESS', 'COMPLETED',
21+
or 'FAILED'
22+
:param completed_on: date and time at which this job finished
23+
being processed
24+
:param callback_url: callback_url if provided
25+
:param metadata: metadata if provided
26+
:param failure: type of failure if job has failed
27+
:param failure_detail: more detailed failure message if job has failed
28+
:param word_count: count of words in job
29+
:param delete_after_seconds: seconds before deletion if provided
30+
"""
31+
32+
self.id = id_
33+
self.created_on = created_on
34+
self.status = status
35+
self.completed_on = completed_on
36+
self.callback_url = callback_url,
37+
self.metadata = metadata
38+
self.failure = failure
39+
self.failure_detail = failure_detail
40+
self.delete_after_seconds = delete_after_seconds
41+
self.word_count = word_count
42+
43+
def __eq__(self, other):
44+
"""Override default equality operator"""
45+
if isinstance(other, self.__class__):
46+
return self.__dict__ == other.__dict__
47+
return False
48+
49+
@classmethod
50+
def from_json(cls, json):
51+
"""Alternate constructor used for parsing json"""
52+
return cls(
53+
json['id'],
54+
json['created_on'],
55+
JobStatus.from_string(json['status']),
56+
completed_on=json.get('completed_on'),
57+
callback_url=json.get('callback_url'),
58+
metadata=json.get('metadata'),
59+
failure=json.get('failure'),
60+
failure_detail=json.get('failure_detail'),
61+
word_count=json.get('word_count'),
62+
delete_after_seconds=json.get('delete_after_seconds'),
63+
)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# -*- coding: utf-8 -*-
2+
"""Sentiment analysis result model"""
3+
4+
from .sentiment_value import SentimentValue
5+
6+
7+
class SentimentAnalysisResult:
8+
def __init__(self, messages):
9+
"""
10+
:param messages: list of sentimented statements from the input in order of how they appeared
11+
in the input.
12+
"""
13+
self.messages = messages
14+
15+
def __eq__(self, other):
16+
"""Override default equality operator"""
17+
if isinstance(other, self.__class__):
18+
return all(a == b for a, b in zip(self.messages, other.messages))
19+
return False
20+
21+
@classmethod
22+
def from_json(cls, json):
23+
"""Alternate constructor used for parsing json"""
24+
return cls([SentimentMessage.from_json(message) for message in json.get('messages', [])])
25+
26+
27+
class SentimentMessage:
28+
def __init__(self, content, score, sentiment, timestamp=None, end_timestamp=None,
29+
offset=None, length=None):
30+
"""
31+
:param content: content of the informant, pulled from input
32+
:param score: Sentimental “score” of the content. Numbers less than 0 indicate a negative
33+
(sad, angry) sentiment. Numbers above 0 indicate positive (joyful, happy)
34+
sentiment
35+
:param: sentiment: Overall detected sentiment of the content, based off of score
36+
:param timestamp: time at which this element starts if input was json
37+
:param end_timestamp: time at which this element ends if input was json
38+
:param offset: Character index at which the content started in the source transcript,
39+
excludes invisible characters
40+
:param length: Length of the content in characters, excludes invisible characters
41+
"""
42+
self.content = content
43+
self.score = score
44+
self.sentiment = sentiment
45+
self.timestamp = timestamp
46+
self.end_timestamp = end_timestamp
47+
self.offset = offset
48+
self.length = length
49+
50+
def __eq__(self, other):
51+
"""Override default equality operator"""
52+
if isinstance(other, self.__class__):
53+
return self.__dict__ == other.__dict__
54+
return False
55+
56+
@classmethod
57+
def from_json(cls, json):
58+
"""Alternate constructor used for parsing json"""
59+
return cls(
60+
json['content'],
61+
json['score'],
62+
SentimentValue.from_string(json['sentiment']),
63+
json.get('ts'),
64+
json.get('end_ts'),
65+
json.get('offset'),
66+
json.get('length'))
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# -*- coding: utf-8 -*-
2+
"""Enum for possible sentiments"""
3+
4+
from enum import Enum
5+
6+
7+
class SentimentValue(Enum):
8+
POSITIVE = 1
9+
NEGATIVE = 2
10+
NEUTRAL = 3
11+
12+
def __str__(self):
13+
return self.name.lower()
14+
15+
@classmethod
16+
def from_string(cls, status):
17+
return cls[status.upper()]

0 commit comments

Comments
 (0)