private static Dictionary <string, object> GenerateScoreKeywordCard(SkillResult result, dynamic boxBody) { var card = GetSkillCardTemplate(SkillType.keyword, boxBody, "Support Score", result.duration); var entry = new Dictionary <string, object>() { { "type", "text" }, { "text", $"Value: {result.supportScore}" } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); if (result.supportScore <= 0m) { entry = new Dictionary <string, object>() { { "type", "text" }, { "text", "Followup: Negative" } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } else if (result.supportScore > 2.5m) { entry = new Dictionary <string, object>() { { "type", "text" }, { "text", "Followup: Positive" } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } return(card); }
private static void CalculateSupportScore(ref SkillResult result) { var score = 1m; foreach (var check in result.scriptChecks) { score += check.Value ? .5m : -.75m; } if (result.resultsBySpeakerSentiment[result.supportIndex].ContainsKey(NEGATIVE)) { score += 1m - (0.05m * result.resultsBySpeakerSentiment[result.supportIndex][NEGATIVE].Count); } //TODO: bucketize across time and aggregate sentiment to see if moves from lower to higher (slope) foreach (var tuple in result.resultsBySpeakerSentiment) { if (tuple.Key == result.supportIndex) { continue; } if (tuple.Value.ContainsKey(NEGATIVE)) { score += 0m - (0.05m * tuple.Value[NEGATIVE].Count); } if (tuple.Value.ContainsKey(POSITIVE)) { score += (0.1m * tuple.Value[POSITIVE].Count); } } result.supportScore = score; }
private static void AggregateSpeakerSentiment(ref SkillResult result) { foreach (var speaker in result.resultBySpeaker.Keys) { var sentimentAg = new Dictionary <string, List <SpeakerResult> >(); result.resultsBySpeakerSentiment.Add(speaker, new Dictionary <string, List <SpeakerResult> >()); foreach (var speakerResult in result.resultBySpeaker[speaker]) { var sentString = speakerResult?.sentiment?.Sentiment?.Value; //if (!sentString.Equals("NEUTRAL")) if (sentString != null) { if (!sentimentAg.ContainsKey(sentString)) { sentimentAg.Add(sentString, new List <SpeakerResult>()); } sentimentAg[sentString].Add(speakerResult); } } foreach (var sentValue in sentimentAg.Keys) { //capitalized first char var formattedSentValue = sentValue.Remove(1) + sentValue.ToLower().Remove(0, 1); result.resultsBySpeakerSentiment[speaker].Add(formattedSentValue, sentimentAg[sentValue]); } } }
public static async Task GenerateCards(SkillResult result, dynamic boxBody) { var boxConfig = new BoxConfig(string.Empty, string.Empty, new Uri(config.BoxApiUrl)); var session = new OAuthSession(boxBody.token.write.access_token.Value, string.Empty, 3600, "bearer"); var client = new BoxClient(boxConfig, session); if (client == null) { throw new Exception("Unable to create box client"); } Console.WriteLine("======== BoxHelper Result ========="); Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.None)); var cards = new List <Dictionary <string, object> > { GenerateSupportHeaderCard(result, boxBody), GenerateScoreKeywordCard(result, boxBody), GenerateTopicsKeywordCard(result, boxBody), GenerateScriptAdherenceKeywordCard(result, boxBody), GenerateTranscriptCard(result, boxBody) }; cards.AddRange(GeneateSentimentTimelineCards(result, boxBody)); Console.WriteLine("======== Cards ========="); Console.WriteLine(JsonConvert.SerializeObject(cards, Formatting.None)); var skillsMetadata = new Dictionary <string, object>() { { "cards", cards } }; try { await client.MetadataManager.CreateFileMetadataAsync(boxBody.source.id.Value, skillsMetadata, "global", "boxSkillsCards"); Console.WriteLine("Created metadata"); } catch (Exception e) { Console.WriteLine("Exception creating metadata. Trying update"); Console.WriteLine(e); BoxMetadataUpdate updateObj = new BoxMetadataUpdate { Op = MetadataUpdateOp.replace, Path = "/cards", Value = cards }; try { await client.MetadataManager.UpdateFileMetadataAsync(boxBody.source.id.Value, new List <BoxMetadataUpdate>() { updateObj }, "global", "boxSkillsCards"); } catch (Exception e2) { Console.WriteLine("Exception updating metadata. giving up"); Console.WriteLine(e2); return; } Console.WriteLine("Successfully updated metadata"); } }
public static void ProcessTranscriptionResults(ref SkillResult result) { GenerateScriptAdherence(ref result); AggregateSpeakerSentiment(ref result); AdjustSpeakerTitles(ref result); CalculateSupportScore(ref result); Console.WriteLine("======== SkillResult ========="); Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.None)); }
private static void AdjustSpeakerTitles(ref SkillResult result) { result.speakerLabels[result.supportIndex] = "Support"; var spIdx = 1; for (var i = 0; i < result.speakerLabels.Count; i++) { if (i != result.supportIndex) { result.speakerLabels[i] = "Customer" + (result.speakerLabels.Count == 2?"":$" {spIdx++}"); } } }
public static Dictionary <string, object> GenerateScriptAdherenceKeywordCard(SkillResult result, dynamic boxBody) { var card = GetSkillCardTemplate(SkillType.keyword, boxBody, "Script Adherence", result.duration); foreach (var phraseKey in result.scriptChecks.Keys) { var entry = new Dictionary <string, object>() { { "type", "text" }, { "text", $"{phraseKey}: {result.scriptChecks[phraseKey]}" }, { "appears", new List <Dictionary <string, object> >() } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } return(card); }
public static Dictionary <string, object> GenerateSupportHeaderCard(SkillResult result, dynamic boxBody) { var card = GetSkillCardTemplate(SkillType.timeline, boxBody, "Support Representative", result.duration); dynamic images = config.PeopleImages["support"]; string imageUrl = images[random.Next(0, images.Count - 1)].Value; var entry = new Dictionary <string, object>() { { "type", "image" }, { "text", "Support Rep" }, { "image_url", imageUrl } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); return(card); }
private async Task <SkillResult> ProcessTranscriptionJob(TranscriptionJob finishedJob) { var result = new SkillResult(); if (finishedJob?.TranscriptionJobStatus.Value == JobStatus.FAILED) { Console.WriteLine($"Transcription job failed with reason: {finishedJob.FailureReason}"); } else if (finishedJob.TranscriptionJobStatus.Value == JobStatus.COMPLETED) { Console.WriteLine($"Transcription file located @: {finishedJob.Transcript.TranscriptFileUri}"); var json = GetJobResultsForAnalsys(finishedJob.Transcript.TranscriptFileUri); JObject transcriptionResults = JObject.Parse(json); result = await ProcessTranscriptionResults(transcriptionResults); } return(result); }
//Run through results, grouping words and saving the locations in the media file. Create card with top 20 //words more than 5 characters. // TODO: should have common word list to ignore instead of <5 chars // TODO: should calculate proximity to find phrases that appear together public static Dictionary <string, object> GenerateTopicsKeywordCard(SkillResult result, dynamic boxBody) { var card = GetSkillCardTemplate(SkillType.keyword, boxBody, "Topics", result.duration); var topics = new List <string>(result.topicLocations.Keys); var count = 0; topics.Sort(delegate(string a, string b) { return(result.topicLocations[a].Count.CompareTo(result.topicLocations[b].Count)); }); foreach (var topic in topics) { if (count++ == 20) { break; } var entry = new Dictionary <string, object>() { { "type", "text" }, { "text", topic }, { "appears", new List <Dictionary <string, object> >() } }; foreach (var speakerResult in result.topicLocations[topic]) { var location = new Dictionary <string, object>() { { "start", speakerResult.start }, { "end", speakerResult.end } }; ((List <Dictionary <string, object> >)entry["appears"]).Add(location); } ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } return(card); }
private static Dictionary <string, object> GenerateTranscriptCard(SkillResult result, dynamic boxBody) { var card = GetSkillCardTemplate(SkillType.transcript, boxBody, "Transcript", result.duration); foreach (var speakerResult in result.resultByTime) { var entry = new Dictionary <string, object>() { { "type", "text" }, { "text", $"[{result.speakerLabels[speakerResult.speaker]}] {speakerResult.text}" }, { "appears", new List <Dictionary <string, object> >() { new Dictionary <string, object>() { { "start", speakerResult.start }, { "end", speakerResult.end } } } } }; ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } return(card); }
private async Task <SkillResult> ProcessTranscriptionResults(JObject transcriptionResults) { var result = new SkillResult(); StringBuilder speakerText = new StringBuilder(); TranscribeAlternative alternative = null; var segments = transcriptionResults["results"]["speaker_labels"]["segments"].ToObject <List <Segment> >(); var transciptionsItems = transcriptionResults["results"]["items"].ToObject <List <TranscribeItem> >(); Console.WriteLine($"items: {transciptionsItems?.Count} segments: {segments.Count}"); var speakerLabel = string.Empty; var lastSpeaker = "nobody"; SpeakerResult currentSpeakerResult = new SpeakerResult(); var itemIdx = 0; var ti = transciptionsItems; // sements have a begin and end, however the items contained in it also // have begin and ends. the range of the items have a 1 to 1 correlation to the 'pronunciation' transcription // item types. These also have ends which are outside the range of the segement strangely. So will be using segment to // get the speaker, then will create an inclusive range for all items under it using the being of first and end of last. foreach (var segment in segments) { if (segment.items.Length == 0) { continue; } result.duration = segment.end_time; if (!lastSpeaker.Equals(segment.speaker_label)) { // these lines do nothing the first iteration, but tie up last // speaker result when the speaker is changing currentSpeakerResult.text = speakerText.ToString(); speakerText = new StringBuilder(); // create new speaker result for new speaker - or first speaker on first iteration var idx = result.speakerLabels.IndexOf(segment.speaker_label); if (idx == -1) { idx = result.speakerLabels.Count; result.speakerLabels.Add(segment.speaker_label); result.resultBySpeaker.Add(idx, new List <SpeakerResult>()); } currentSpeakerResult = new SpeakerResult(); currentSpeakerResult.speaker = idx; ConfigureTimeRange(ref currentSpeakerResult, segment); lastSpeaker = segment.speaker_label; result.resultBySpeaker[idx].Add(currentSpeakerResult); result.resultByTime.Add(currentSpeakerResult); } else { ConfigureTimeRange(ref currentSpeakerResult, segment); } for (; itemIdx < ti.Count && ((currentSpeakerResult.start <= ti[itemIdx].start_time && ti[itemIdx].end_time <= currentSpeakerResult.end) || (ti[itemIdx].start_time == 0m)) ; itemIdx++) { alternative = ti[itemIdx].alternatives.First(); if (alternative.content.Equals("[SILENCE]")) { speakerText.Append("."); } else { speakerText.Append(alternative.content); } speakerText.Append(" "); } } currentSpeakerResult.text = speakerText.ToString(); // Call AWS Comprehend client to get sentiment for all speaker results List <int> keyList = new List <int>(result.resultBySpeaker.Keys); for (int keyIdx = 0; keyIdx < keyList.Count; keyIdx++) { var spkKey = keyList[keyIdx]; for (int resultIdx = result.resultBySpeaker[spkKey].Count - 1; resultIdx >= 0; resultIdx--) { if (!IsBlankText(result.resultBySpeaker[spkKey][resultIdx].text)) { var speakerResult = result.resultBySpeaker[spkKey][resultIdx]; speakerResult.sentiment = await DetermineSentiment(result.resultBySpeaker[spkKey][resultIdx].text); var topics = await DetermineTopic(result.resultBySpeaker[spkKey][resultIdx].text); foreach (var topic in topics) { if (!result.topicLocations.ContainsKey(topic.Text)) { result.topicLocations.Add(topic.Text, new List <SpeakerResult>()); } result.topicLocations[topic.Text].Add(speakerResult); } } } } return(result); }
public static List <Dictionary <string, object> > GeneateSentimentTimelineCards(SkillResult result, dynamic boxBody) { List <Dictionary <string, object> > cards = new List <Dictionary <string, object> >(); foreach (var speaker in result.resultsBySpeakerSentiment.Keys) { var card = GetSkillCardTemplate(SkillType.timeline, boxBody, $"{result.speakerLabels[speaker]} Sentiment", result.duration); foreach (var sentValue in result.resultsBySpeakerSentiment[speaker].Keys) { dynamic image = config.SentimentImages[sentValue.ToLower()]; var entry = new Dictionary <string, object>() { { "type", "text" }, { "text", sentValue }, { "image_url", image.Value }, { "appears", new List <Dictionary <string, object> >() } }; foreach (var speakerResult in result.resultsBySpeakerSentiment[speaker][sentValue]) { var location = new Dictionary <string, object>() { { "start", speakerResult.start }, { "end", speakerResult.end } }; ((List <Dictionary <string, object> >)entry["appears"]).Add(location); } ((List <Dictionary <string, object> >)card["entries"]).Add(entry); } cards.Add(card); } return(cards); }
public static void GenerateScriptAdherence(ref SkillResult result) { //TODO: should use algorithm for symantic difference, but doing something quick for poc // close text search //we don't know which speaker is the support agent. So for each speaker, we will //try to match all of the phrases. If we get more than 2 matches, we'll assume that //is the support agent. Otherwise assume the first speaker and that they didn't //follow the script Console.WriteLine("======== Script Adherence ========="); Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.None)); int lastPhraseCount = 0; foreach (var speaker in result.resultBySpeaker.Keys) { var scriptChecks = new Dictionary <string, bool>(); var phraseCount = 0; foreach (var phraseKey in scriptPhrases.Keys) { foreach (var results in result.resultBySpeaker[speaker]) { var found = LevenshteinDistance.SearchPercent(scriptPhrases[phraseKey], CleanText(results.text)); //var found = CleanText(results.text).Contains(scriptPhrases[phraseKey]); if (found >= .8m) { scriptChecks.Add(phraseKey, true); break; } } if (scriptChecks.ContainsKey(phraseKey)) { phraseCount++; } else { scriptChecks.Add(phraseKey, false); } } if (phraseCount > lastPhraseCount) { lastPhraseCount = phraseCount; result.supportIndex = speaker; result.scriptChecks = scriptChecks; // With two matches, assume this has to be the support rep, so // don't bother processing remaining speakers if (phraseCount > 2) { break; } } } if (result.scriptChecks.Keys.Count == 0) { // no one matched anything, assume speaker one is support result.supportIndex = 0; foreach (var phraseKey in scriptPhrases.Keys) { result.scriptChecks.Add(phraseKey, false); } } }