From 0360ca07d9c6289c4f3948702058288e30d86b76 Mon Sep 17 00:00:00 2001 From: shravanask Date: Tue, 26 Nov 2013 09:58:31 +0100 Subject: [PATCH] feature: #1) implemented TTS with language swithcing capabilities. #2) implemented defining DTMF length (new Media property: ANSWER_INPUT_LENGTH) --- .../dialog/adapter/VoiceXMLRESTProxy.java | 122 ++++++++++++------ .../almende/dialog/model/MediaProperty.java | 13 +- .../com/almende/dialog/model/Question.java | 64 ++++----- .../dialog/adapter/VoiceXMLServletTest.java | 2 +- .../almende/dialog/model/QuestionTest.java | 22 ++++ 5 files changed, 151 insertions(+), 72 deletions(-) diff --git a/Charlotte - Java dialog tooling/src/com/almende/dialog/adapter/VoiceXMLRESTProxy.java b/Charlotte - Java dialog tooling/src/com/almende/dialog/adapter/VoiceXMLRESTProxy.java index 169e76d4..bb059be7 100644 --- a/Charlotte - Java dialog tooling/src/com/almende/dialog/adapter/VoiceXMLRESTProxy.java +++ b/Charlotte - Java dialog tooling/src/com/almende/dialog/adapter/VoiceXMLRESTProxy.java @@ -1,8 +1,11 @@ package com.almende.dialog.adapter; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.StringWriter; import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; @@ -42,6 +45,7 @@ import com.almende.dialog.util.ServerUtils; import com.almende.util.ParallelInit; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.api.server.spi.config.DefaultValue; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; @@ -139,8 +143,7 @@ public static HashMap dial( Map addressNameMap, loadAddress = addressNameMap.keySet().iterator().next(); //fetch the question - Question question = Question.fromURL(url, config.getConfigId(), loadAddress); - String questionJson = question.toJSON(); + String questionJson = Question.getJSONFromURL( url, config.getConfigId(), loadAddress, "" ); for ( String address : addressNameMap.keySet() ) { @@ -206,8 +209,10 @@ public static boolean killActiveCalls(AdapterConfig config) { @Path("dtmf2hash") @GET @Produces("application/srgs+xml") - public Response getDTMF2Hash() { - + public Response getDTMF2Hash(@QueryParam("minlength") String minLength, @QueryParam("maxlength") String maxLength) { + minLength = (minLength != null && !minLength.isEmpty()) ? minLength : "0"; + maxLength = (maxLength != null && !maxLength.isEmpty()) ? maxLength : ""; + String repeat = minLength.equals( maxLength ) ? minLength : minLength + "-" + maxLength; String result = " "+ " "+ " "+ @@ -227,12 +232,11 @@ public Response getDTMF2Hash() { " "+ " "+ " "+ - " "+ + " "+ " # "+ " "+ " "+ " "; - return Response.ok(result).build(); } @@ -790,34 +794,18 @@ else if ( personality.getTextContent().equals( "Terminator" ) ) return Response.ok(reply).build(); } - - /** - * redirects to the TSS functionality to play the audio - * @param text to be converted to Voice - * @return - */ + @GET - @Path( "tts/{text}" ) - public HttpServletResponse redirectToTTS( @PathParam( "text" ) String text, - @Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse ) - throws Exception + @Path( "tts/{textForSpeech}" ) + public Response redirectToSpeechEngine( @PathParam( "textForSpeech" ) String textForSpeech, + @QueryParam( "hl" ) @DefaultValue( "nl-nl" ) String language, + @QueryParam( "c" ) @DefaultValue( "wav" ) String contentType, @Context HttpServletRequest req, + @Context HttpServletResponse resp ) throws IOException, URISyntaxException { - String ttsURL = "https://voicerss-text-to-speech.p.mashape.com/?key=afafc70fde4b4b32a730842e6fcf0c62&src=" - + text.replace( ".wav", "" ) + "&hl=en-us&r=0&c=wav&f=8khz_8bit_mono"; - httpServletResponse.addHeader( "X-Mashape-Authorization", "Fy6ynDFxV5Yi6K0pj8NYxPneKgfztwp4" ); - httpServletResponse.sendRedirect( ttsURL ); - - // Client client = ParallelInit.getClient(); - // httpServletRequest.getSession().setAttribute( "X-Mashape-Authorization", "Fy6ynDFxV5Yi6K0pj8NYxPneKgfztwp4" ); - // WebResource webResource = client.resource( ttsURL ); - // ClientResponse clientResponse = webResource.getRequestBuilder() - // .header( "X-Mashape-Authorization", "Fy6ynDFxV5Yi6K0pj8NYxPneKgfztwp4" ).get( ClientResponse.class ); - // return Response.ok( clientResponse.getEntityInputStream() ).build(); - // return Response.seeOther( webResource.getURI() ) - // .header( "X-Mashape-Authorization", "Fy6ynDFxV5Yi6K0pj8NYxPneKgfztwp4" ).build(); - return httpServletResponse; + String ttsURL = getTTSURL( textForSpeech, language, contentType ); + return Response.seeOther( new URI( ttsURL ) ).build(); } - + public class Return { ArrayList prompts; Question question; @@ -1046,14 +1034,14 @@ protected String renderOpenQuestion(Question question,ArrayList prompts, // Check if media property type equals audio // if so record audio message, if not record dtmf input String typeProperty = question.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.TYPE ); - String voiceMessageLengthProperty = question.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.VOICE_MESSAGE_LENGTH ); if(typeProperty!=null && typeProperty.equalsIgnoreCase("audio")) { //assign a default voice mail length if one is not specified - String voiceMessageLength = voiceMessageLengthProperty != null ? voiceMessageLengthProperty : "15s"; - if(!voiceMessageLength.endsWith("s")) + String voiceMessageLengthProperty = question.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.VOICE_MESSAGE_LENGTH ); + voiceMessageLengthProperty = voiceMessageLengthProperty != null ? voiceMessageLengthProperty : "15s"; + if(!voiceMessageLengthProperty.endsWith("s")) { - log.warning("Redirect timeout must be end with 's'. E.g. 40s. Found: "+ voiceMessageLength); - voiceMessageLength += "s"; + log.warning("Voicemail length must be end with 's'. E.g. 40s. Found: "+ voiceMessageLengthProperty); + voiceMessageLengthProperty += "s"; } // Fetch the upload url @@ -1072,7 +1060,7 @@ protected String renderOpenQuestion(Question question,ArrayList prompts, outputter.startTag("record"); outputter.attribute("name", "file"); outputter.attribute("beep", "true"); - outputter.attribute("maxtime", voiceMessageLength); + outputter.attribute("maxtime", voiceMessageLengthProperty); outputter.attribute("dtmfterm", "true"); //outputter.attribute("finalsilence", "3s"); for (String prompt : prompts){ @@ -1124,6 +1112,12 @@ protected String renderOpenQuestion(Question question,ArrayList prompts, outputter.endTag(); } else { + //see if a dtmf length is defined in the question + String dtmfMinLength = question.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.ANSWER_INPUT_MIN_LENGTH ); + dtmfMinLength = dtmfMinLength != null ? dtmfMinLength : ""; + String dtmfMaxLength = question.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.ANSWER_INPUT_MAX_LENGTH ); + dtmfMaxLength = dtmfMaxLength != null ? dtmfMaxLength : ""; + outputter.startTag("var"); outputter.attribute("name","answer_input"); outputter.endTag(); @@ -1140,7 +1134,8 @@ protected String renderOpenQuestion(Question question,ArrayList prompts, outputter.attribute("name", "answer"); outputter.startTag("grammar"); outputter.attribute("mode", "dtmf"); - outputter.attribute("src", DTMFGRAMMAR); + outputter.attribute( "src", DTMFGRAMMAR + "?minlength=" + dtmfMinLength + + "&maxlength=" + dtmfMaxLength ); outputter.attribute("type", "application/srgs+xml"); outputter.endTag(); for (String prompt: prompts){ @@ -1186,7 +1181,7 @@ private Response handleQuestion(Question question, String adapterID,String remot if(question !=null && !question.getType().equalsIgnoreCase("comment")) question = res.question; - log.info( "question formed at handleQuestion is: "+ question ); + log.info( "question formed at handleQuestion is: "+ ServerUtils.serializeWithoutException( question )); log.info( "prompts formed at handleQuestion is: "+ res.prompts ); if ( question != null ) @@ -1199,7 +1194,29 @@ private Response handleQuestion(Question question, String adapterID,String remot Session session = Session.getSession( sessionKey ); StringStore.storeString( "question_" + session.getRemoteAddress() + "_" + session.getLocalAddress(), questionJSON ); - + + //convert all text prompts to speech + if(res.prompts != null) + { + String language = question.getPreferred_language().equals( "nl" ) ? "nl-nl" : "en-us"; + ArrayList promptsCopy = new ArrayList(); + for ( String prompt : res.prompts ) + { + if ( !prompt.startsWith( "dtmfKey://" ) ) + { + if ( !prompt.endsWith( ".wav" ) ) + { + promptsCopy.add( getTTSURL( prompt, language, "wav" ) ); + } + else + { + promptsCopy.add( prompt ); + } + } + } + res.prompts = promptsCopy; + } + if ( question.getType().equalsIgnoreCase( "closed" ) ) { result = renderClosedQuestion( question, res.prompts, sessionKey ); @@ -1296,4 +1313,29 @@ private HashMap getTimeMap( String startTime, String answerTime, timeMap.put( "releaseTime", releaseTime ); return timeMap; } + + /** + * returns the TTS URL from voiceRSS. + * + * @param textForSpeech + * @param language + * @param contentType + * @return + */ + private String getTTSURL( String textForSpeech, String language, String contentType ) + { + try + { + textForSpeech = URLEncoder.encode( textForSpeech.replace( "text://", "" ), "UTF-8").replace( "+", "%20" ); + } + catch ( UnsupportedEncodingException e ) + { + e.printStackTrace(); + log.severe( e.getLocalizedMessage() ); + } + return "http://api.voicerss.org/?key=afafc70fde4b4b32a730842e6fcf0c62&src=" + textForSpeech + "&hl=" + language + + "&c=" + contentType + "&type=.wav"; + // return "http://www.voicerss.org/controls/speech.ashx?hl=" + language + "&src=" + // + textForSpeech.replace( ".wav", "" ) + "&c" + contentType + "&rnd=0.4776021621655673"; + } } diff --git a/Charlotte - Java dialog tooling/src/com/almende/dialog/model/MediaProperty.java b/Charlotte - Java dialog tooling/src/com/almende/dialog/model/MediaProperty.java index aff6fec0..4f91a832 100644 --- a/Charlotte - Java dialog tooling/src/com/almende/dialog/model/MediaProperty.java +++ b/Charlotte - Java dialog tooling/src/com/almende/dialog/model/MediaProperty.java @@ -11,8 +11,17 @@ public class MediaProperty { public enum MediaPropertyKey { - TIMEOUT, ANSWER_INPUT, LENGTH, TYPE; - + TIMEOUT, //defines the timeout associated with the call + ANSWER_INPUT, //defines if the answer is given via dtmf, text etc + ANSWER_INPUT_MIN_LENGTH, //defines the length of th answer input. Typically dtmf + ANSWER_INPUT_MAX_LENGTH, + // defines a subtype for the question type. + //E.g. open question with type: audio refers to voicemail + TYPE, + VOICE_MESSAGE_LENGTH, //defines the length of the voicemail to be recorded + //defines the number of times the question should repeat in case of a wrong answer input. + //works only for phonecalls so as to end a call with repeated input errors. + RETRY_LIMIT; @JsonCreator public static MediaPropertyKey fromJson(String name) { return valueOf(name.toUpperCase()); diff --git a/Charlotte - Java dialog tooling/src/com/almende/dialog/model/Question.java b/Charlotte - Java dialog tooling/src/com/almende/dialog/model/Question.java index 8036029e..b0d058ef 100644 --- a/Charlotte - Java dialog tooling/src/com/almende/dialog/model/Question.java +++ b/Charlotte - Java dialog tooling/src/com/almende/dialog/model/Question.java @@ -14,11 +14,11 @@ import com.almende.dialog.model.MediaProperty.MediumType; import com.almende.dialog.model.impl.Q_fields; import com.almende.dialog.model.intf.QuestionIntf; -import com.almende.dialog.util.QuestionTextTransformer; import com.almende.dialog.util.ServerUtils; import com.almende.util.ParallelInit; import com.eaio.uuid.UUID; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientHandlerException; @@ -27,7 +27,6 @@ import com.thetransactioncompany.cors.HTTPMethod; import flexjson.JSON; -import flexjson.JSONSerializer; public class Question implements QuestionIntf { private static final long serialVersionUID = -9069211642074173182L; @@ -54,11 +53,30 @@ public Question() { public static Question fromURL(String url,String adapterID) { return fromURL(url, adapterID,""); } + @JSON(include = false) public static Question fromURL(String url,String adapterID,String remoteID) { return fromURL(url, adapterID, remoteID, ""); } + public static String getJSONFromURL( String url, String adapterID, String remoteID, String fromID ) + { + Client client = ParallelInit.getClient(); + WebResource webResource; + try + { + webResource = client.resource( url ).queryParam( "responder", URLEncoder.encode( remoteID, "UTF-8" ) ) + .queryParam( "requester", fromID ); + return webResource.type( "text/plain" ).get( String.class ); + } + catch ( UnsupportedEncodingException e ) + { + log.severe( e.toString() ); + dialogLog.severe( adapterID, "ERROR loading question: " + e.toString() ); + } + return null; + } + @JSON( include = false ) public static Question fromURL( String url, String adapterID, String remoteID, String fromID ) { @@ -143,25 +161,15 @@ public String toJSON() { @JsonIgnore public String toJSON( boolean expanded_texts ) { - if ( ServerUtils.isInUnitTestingEnvironment() ) + try { - try - { - return ServerUtils.serialize( this ); - } - catch ( Exception e ) - { - e.printStackTrace(); - return null; - } + return ServerUtils.serialize( this ); } - else + catch ( Exception e ) { - return new JSONSerializer() - .exclude( "*.class" ) - .transform( new QuestionTextTransformer( expanded_texts ), "question_text", "question_expandedtext", - "answer_text", "answer_expandedtext", "answers.answer_text", "answers.answer_expandedtext" ) - .include( "answers", "event_callbacks" ).serialize( this ); + e.printStackTrace(); + log.severe( "Question serialization failed. Message: " + e.getLocalizedMessage() ); + return null; } } @@ -193,7 +201,6 @@ public void generateIds() { @JSON( include = false ) public Question answer( String responder, String adapterID, String answer_id, String answer_input, String sessionKey ) { - Client client = ParallelInit.getClient(); boolean answered = false; Answer answer = null; if ( this.getType().equals( "open" ) ) @@ -291,7 +298,7 @@ else if(answer_input == null) if ( newQ != null ) return newQ; - String retryLoadLimit = getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.LENGTH ); + String retryLoadLimit = getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.RETRY_LIMIT ); Integer retryCount = getRetryCount( sessionKey ); if ( retryLoadLimit != null && retryCount != null && retryCount < Integer.parseInt( retryLoadLimit ) ) { @@ -341,6 +348,7 @@ else if( sessionKey != null ) flushRetryCount( sessionKey ); } // Send answer to answer.callback. + Client client = ParallelInit.getClient(); WebResource webResource = client.resource( answer.getCallback() ); AnswerPost ans = new AnswerPost( this.getQuestion_id(), answer.getAnswer_id(), answer_input, responder ); // Check if answer.callback gives new question for this dialog @@ -523,14 +531,10 @@ public void setEvent_callbacks(ArrayList event_callbacks) { question.setEvent_callbacks(event_callbacks); } - @JSON(include = false) - @JsonIgnore public String getPreferred_language() { return preferred_language; } - @JSON(include = false) - @JsonIgnore public void setPreferred_language(String preferred_language) { this.preferred_language = preferred_language; } @@ -556,10 +560,17 @@ public void setTrackingToken(String token) { this.question.setTrackingToken(token); } + @JsonProperty("media_properties") public Collection getMedia_properties() { return media_properties; } + + @JsonProperty("media_properties") + public void setMedia_Properties( Collection media_properties ) + { + this.media_properties = media_properties; + } @JSON(include = false) public Map getMediaPropertyByType( MediumType type ) { @@ -587,11 +598,6 @@ public String getMediaPropertyValue( MediumType type, MediaPropertyKey key) { return null; } - public void setMedia_Properties( Collection media_properties ) - { - this.media_properties = media_properties; - } - public void addMedia_Properties( MediaProperty mediaProperty ) { media_properties = media_properties == null ? new ArrayList() : media_properties; diff --git a/Charlotte - Java dialog tooling/test/com/almende/dialog/adapter/VoiceXMLServletTest.java b/Charlotte - Java dialog tooling/test/com/almende/dialog/adapter/VoiceXMLServletTest.java index 3db4a403..21f6fb59 100644 --- a/Charlotte - Java dialog tooling/test/com/almende/dialog/adapter/VoiceXMLServletTest.java +++ b/Charlotte - Java dialog tooling/test/com/almende/dialog/adapter/VoiceXMLServletTest.java @@ -64,7 +64,7 @@ public void inbountPhoneCall_WithOpenQuestion_MissingAnswerTest() throws Excepti //answer the dialog Question retrivedQuestion = ServerUtils.deserialize( TestFramework.fetchResponse( HTTPMethod.GET, url, null ), Question.class ); - String mediaPropertyValue = retrivedQuestion.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.LENGTH ); + String mediaPropertyValue = retrivedQuestion.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.RETRY_LIMIT ); Integer retryCount = Question.getRetryCount( answerVariables.get( "sessionKey" ) ); int i = 0; diff --git a/Charlotte - Java dialog tooling/test/com/almende/dialog/model/QuestionTest.java b/Charlotte - Java dialog tooling/test/com/almende/dialog/model/QuestionTest.java index 3654e166..211c454f 100644 --- a/Charlotte - Java dialog tooling/test/com/almende/dialog/model/QuestionTest.java +++ b/Charlotte - Java dialog tooling/test/com/almende/dialog/model/QuestionTest.java @@ -55,4 +55,26 @@ public void parseOpenQuestionJSON() { assertTrue(properties.get(MediaPropertyKey.TYPE).equalsIgnoreCase("AuDiO")); } + + @Test + public void parseOpenQuestionWithMinMaxDtmfInputMediaPropertiesTest() + { + String questionText = "{\"preferred_language\":\"en\",\"question_id\":\"1\",\"question_text\":\"text://How are you doing\"," + + "\"type\":\"open\",\"answers\":[{\"answer_id\":\"6b321a81-6fdf-4f6e-8739-001c8413c883\",\"answer_text\":\"\"," + + "\"callback\":\"http://askfastmarket1.appspot.com/resource/question?url=comment\"}],\"event_callbacks\":[]," + + "\"media_properties\":[{\"medium\":\"BROADSOFT\",\"properties\":{\"ANSWER_INPUT_MIN_LENGTH\":\"3\"," + + "\"ANSWER_INPUT_MAX_LENGTH\":\"3\"}}]}"; + Question fromJSON = Question.fromJSON( questionText, null ); + assertEquals( "3", + fromJSON.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.ANSWER_INPUT_MIN_LENGTH ) ); + assertEquals( "3", + fromJSON.getMediaPropertyValue( MediumType.BROADSOFT, MediaPropertyKey.ANSWER_INPUT_MAX_LENGTH ) ); + + assertEquals( "en", fromJSON.getPreferred_language() ); + assertEquals( 1, fromJSON.getMedia_properties().size() ); + assertEquals( "http://askfastmarket1.appspot.com/resource/question?url=comment", fromJSON.getAnswers() + .iterator().next().getCallback() ); + + assertTrue( fromJSON.toJSON().contains( "ANSWER_INPUT_MIN_LENGTH\":\"3\"" ) ); + } }