/**
*
* @author greg (at) myrobotlab.org
*
* This file is part of MyRobotLab (http://myrobotlab.org).
*
* MyRobotLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version (subject to the "Classpath" exception
* as provided in the LICENSE.txt file that accompanied this code).
*
* MyRobotLab is distributed in the hope that it will be useful or fun,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* All libraries in thirdParty bundle are subject to their own license
* requirements - please refer to http://myrobotlab.org/libraries for
* details.
*
* Enjoy !
*
* */
package org.myrobotlab.service;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Stack;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.myrobotlab.framework.Service;
import org.myrobotlab.framework.ServiceType;
import org.myrobotlab.io.FileIO;
import org.myrobotlab.logging.Level;
import org.myrobotlab.logging.LoggerFactory;
import org.myrobotlab.logging.Logging;
import org.myrobotlab.logging.LoggingFactory;
import org.myrobotlab.service.data.AudioData;
import org.myrobotlab.service.interfaces.AudioListener;
import org.myrobotlab.service.interfaces.SpeechRecognizer;
import org.myrobotlab.service.interfaces.SpeechSynthesis;
import org.myrobotlab.service.interfaces.TextListener;
import org.slf4j.Logger;
/**
* AcapelaSpeech - Use the acapela group speech synthesis API. This makes a HTTP
* request to generate an MP3 that represents the text to be spoken for a given
* voice. That mp3 is then cached and played back by the AudioFile service.
*
*/
public class AcapelaSpeech extends Service implements TextListener, SpeechSynthesis, AudioListener {
transient public final static Logger log = LoggerFactory.getLogger(AcapelaSpeech.class);
private static final long serialVersionUID = 1L;
// default voice
public String voice = "Ryan";
public HashSet<String> voices = new HashSet<String>();
// this is a peer service.
transient AudioFile audioFile = null;
// TODO: fix the volume control
// private float volume = 1.0f;
transient CloseableHttpClient client;
public AcapelaSpeech(String n) {
super(n);
// TODO: be country/language aware when asking for voices?
// maybe have a get voices by language/locale
// Arabic
voices.add("Leila");
voices.add("Mehdi");
voices.add("Nizar");
voices.add("Salma");
// Catalan
voices.add("Laia");
// Czech
voices.add("Eliska");
// Danish
voices.add("Mette");
voices.add("Rasmus");
// Dutch ( Belgium )
voices.add("Zoe");
voices.add("Jeroen");
voices.add("JeroenHappy");
voices.add("JeroenSad");
voices.add("Sofie");
// Dutch ( Netherlands )
voices.add("Jasmijn");
voices.add("Daan");
voices.add("Femke");
voices.add("Max");
// English (AU)
voices.add("Tyler");
voices.add("Lisa");
voices.add("Olivia");
voices.add("Liam");
// English ( India )
voices.add("Deepa");
// English ( Scottish )
voices.add("Rhona");
// English (UK)
voices.add("Rachel");
voices.add("Graham");
voices.add("Harry");
voices.add("Lucy");
voices.add("Nizareng");
voices.add("Peter");
voices.add("PeterHappy");
voices.add("PeterSad");
voices.add("QueenElizabeth");
voices.add("Rosie");
// English ( USA )
voices.add("Sharon");
voices.add("Ella");
voices.add("EmilioEnglish");
voices.add("Josh");
voices.add("Karen");
voices.add("Kenny");
voices.add("Laura");
voices.add("Micah");
voices.add("Nelly");
voices.add("Rod");
voices.add("Ryan");
voices.add("Saul");
voices.add("Scott");
voices.add("Tracy");
voices.add("ValeriaEnglish");
voices.add("Will");
voices.add("WillBadGuy");
voices.add("WillFromAfar");
voices.add("WillHappy");
voices.add("WillLittleCreature");
// Faroese
voices.add("Hanna");
voices.add("Hanus");
// Finnish
voices.add("Sanna");
// French ( Belgium )
voices.add("Justine");
// French ( Canada )
voices.add("Louise");
// French ( France )
voices.add("Manon");
voices.add("Alice");
voices.add("Antoine");
voices.add("AntoineFromAfar");
voices.add("AntoineHappy");
voices.add("AntoineSad");
voices.add("Bruno");
voices.add("Claire");
voices.add("Manon");
voices.add("Julie");
voices.add("Margaux");
voices.add("MargauxHappy");
voices.add("MargauxSad");
// German
voices.add("Claudia");
voices.add("Andreas");
voices.add("Jonas");
voices.add("Julia");
voices.add("Klaus");
voices.add("Lea");
voices.add("Sarah");
// Greek
voices.add("Dimitris");
voices.add("DimitrisHappy");
voices.add("DimitrisSad");
// Italian
voices.add("Fabiana");
voices.add("Chiara");
voices.add("Vittorio");
// Japanese
voices.add("Sakura");
// Korean
voices.add("Minji");
// Mandarin
voices.add("Lulu");
// Norwegian
voices.add("Bente");
voices.add("Kari");
voices.add("Olav");
// Polish
voices.add("Monika");
voices.add("Ania");
// Portuguese ( Brazil )
voices.add("Marcia");
// Portuguese ( Portugal )
voices.add("Celia");
// Russian
voices.add("Alyona");
// Sami ( North )
voices.add("Biera");
voices.add("Elle");
// Spanish ( Spain )
voices.add("Ines");
voices.add("Antonio");
voices.add("Maria");
// Spanish ( US )
voices.add("Rodrigo");
voices.add("Emilio");
voices.add("Rosa");
voices.add("Valeria");
// Swedish
voices.add("Elin");
voices.add("Emil");
voices.add("Emma");
voices.add("Erik");
// Swedish ( Finland )
voices.add("Samuel");
// Swedish ( Gothenburg )
voices.add("Kal");
// Swedish ( Scanian )
voices.add("Mia");
// Turkish
voices.add("Ipek");
}
public void startService() {
super.startService();
if (client == null) {
// new MultiThreadedHttpConnectionManager()
client = HttpClients.createDefault();
}
audioFile = (AudioFile) startPeer("audioFile");
audioFile.startService();
subscribe(audioFile.getName(), "publishAudioStart");
subscribe(audioFile.getName(), "publishAudioEnd");
// attach a listener when the audio file ends playing.
audioFile.addListener("finishedPlaying", this.getName(), "publishEndSpeaking");
}
public AudioFile getAudioFile() {
return audioFile;
}
@Override
public ArrayList<String> getVoices() {
return new ArrayList<String>(voices);
}
@Override
public String getVoice() {
return voice;
}
@Override
public boolean setVoice(String voice) {
this.voice = voice;
return voices.contains(voice);
}
@Override
public void setLanguage(String l) {
// FIXME ! "MyLanguages", "sonid8" ???
// FIXME - implement !!!
}
public String getMp3Url(String toSpeak) {
HttpPost post = null;
try {
// request form & send text
String url = "http://www.acapela-group.com/demo-tts/DemoHTML5Form_V2.php?langdemo=Powered+by+%3Ca+href%3D%22http%3A%2F%2Fwww.acapela-vaas.com"
+ "%22%3EAcapela+Voice+as+a+Service%3C%2Fa%3E.+For+demo+and+evaluation+purpose+only%2C+for+commercial+use+of+generated+sound+files+please+go+to+"
+ "%3Ca+href%3D%22http%3A%2F%2Fwww.acapela-box.com%22%3Ewww.acapela-box.com%3C%2Fa%3E";
post = new HttpPost(url);
List<NameValuePair> nvps = new ArrayList<NameValuePair>();
nvps.add(new BasicNameValuePair("MyLanguages", "sonid10"));
nvps.add(new BasicNameValuePair("MySelectedVoice", voice));
nvps.add(new BasicNameValuePair("MyTextForTTS", toSpeak));
nvps.add(new BasicNameValuePair("t", "1"));
nvps.add(new BasicNameValuePair("SendToVaaS", ""));
UrlEncodedFormEntity formData = new UrlEncodedFormEntity(nvps, "UTF-8");
post.setEntity(formData);
HttpResponse response = client.execute(post);
log.info(response.getStatusLine().toString());
HttpEntity entity = response.getEntity();
byte[] b = FileIO.toByteArray(entity.getContent());
// parse out mp3 file url
String mp3Url = null;
String data = new String(b);
String startTag = "var myPhpVar = '";
int startPos = data.indexOf(startTag);
if (startPos != -1) {
int endPos = data.indexOf("';", startPos);
if (endPos != -1) {
mp3Url = data.substring(startPos + startTag.length(), endPos);
}
}
if (mp3Url == null) {
error("could not get mp3 back from Acapela server !");
}
return mp3Url;
} catch (Exception e) {
Logging.logError(e);
} finally {
if (post != null) {
post.releaseConnection();
}
}
return null;
}
public byte[] getRemoteFile(String toSpeak) {
String mp3Url = getMp3Url(toSpeak);
HttpGet get = null;
byte[] b = null;
try {
HttpResponse response = null;
// fetch file
get = new HttpGet(mp3Url);
log.info("mp3Url {}", mp3Url);
// get mp3 file & save to cache
response = client.execute(get);
log.info("got {}", response.getStatusLine());
HttpEntity entity = response.getEntity();
// cache the mp3 content
b = FileIO.toByteArray(entity.getContent());
EntityUtils.consume(entity);
} catch (Exception e) {
Logging.logError(e);
} finally {
if (get != null) {
get.releaseConnection();
}
}
return b;
}
@Override
public boolean speakBlocking(String toSpeak) throws IOException {
log.info("speak blocking {}", toSpeak);
if (voice == null) {
log.warn("voice is null! setting to default: Ryan");
voice = "Ryan";
}
String localFileName = getLocalFileName(this, toSpeak, "mp3");
String filename = AudioFile.globalFileCacheDir + File.separator + localFileName;
if (!audioFile.cacheContains(localFileName)) {
byte[] b = getRemoteFile(toSpeak);
audioFile.cache(localFileName, b, toSpeak);
}
invoke("publishStartSpeaking", toSpeak);
audioFile.playBlocking(filename);
invoke("publishEndSpeaking", toSpeak);
log.info("Finished waiting for completion.");
return false;
}
@Override
public void setVolume(float volume) {
// TODO: fix the volume control
log.warn("Volume control not implemented in AcapelaSpeech yet.");
}
@Override
public float getVolume() {
return 0;
}
@Override
public void interrupt() {
// TODO: Implement me!
}
@Override
public void onText(String text) {
log.info("ON Text Called: {}", text);
try {
speak(text);
} catch (Exception e) {
Logging.logError(e);
}
}
@Override
public String getLanguage() {
return null;
}
// HashSet<String> audioFiles = new HashSet<String>();
Stack<String> audioFiles = new Stack<String>();
public AudioData speak(String toSpeak) throws IOException {
// this will flip to true on the audio file end playing.
AudioData ret = null;
log.info("speak {}", toSpeak);
if (voice == null) {
log.warn("voice is null! setting to default: Ryan");
voice = "Ryan";
}
String filename = this.getLocalFileName(this, toSpeak, "mp3");
if (audioFile.cacheContains(filename)) {
ret = audioFile.playCachedFile(filename);
utterances.put(ret, toSpeak);
return ret;
}
audioFiles.push(filename);
byte[] b = getRemoteFile(toSpeak);
audioFile.cache(filename, b, toSpeak);
ret = audioFile.playCachedFile(filename);
utterances.put(ret, toSpeak);
return ret;
}
public AudioData speak(String voice, String toSpeak) throws IOException {
setVoice(voice);
return speak(toSpeak);
}
@Override
public String getLocalFileName(SpeechSynthesis provider, String toSpeak, String audioFileType) throws UnsupportedEncodingException {
// TODO: make this a base class sort of thing.
return provider.getClass().getSimpleName() + File.separator + URLEncoder.encode(provider.getVoice(), "UTF-8") + File.separator + DigestUtils.md5Hex(toSpeak) + "."
+ audioFileType;
}
@Override
public void addEar(SpeechRecognizer ear) {
// TODO: move this to a base class. it's basically the same for all
// mouths/ speech synth stuff.
// when we add the ear, we need to listen for request confirmation
addListener("publishStartSpeaking", ear.getName(), "onStartSpeaking");
addListener("publishEndSpeaking", ear.getName(), "onEndSpeaking");
}
public void onRequestConfirmation(String text) {
try {
speakBlocking(String.format("did you say. %s", text));
} catch (Exception e) {
Logging.logError(e);
}
}
@Override
public List<String> getLanguages() {
// TODO Auto-generated method stub
ArrayList<String> ret = new ArrayList<String>();
// FIXME - add iso language codes currently supported e.g. en en_gb de
// etc..
return ret;
}
// audioData to utterance map TODO: revisit the design of this
HashMap<AudioData, String> utterances = new HashMap<AudioData, String>();
@Override
public String publishStartSpeaking(String utterance) {
log.info("publishStartSpeaking {}", utterance);
return utterance;
}
@Override
public String publishEndSpeaking(String utterance) {
log.info("publishEndSpeaking {}", utterance);
return utterance;
}
@Override
public void onAudioStart(AudioData data) {
log.info("onAudioStart {} {}", getName(), data.toString());
// filters on only our speech
if (utterances.containsKey(data)) {
String utterance = utterances.get(data);
invoke("publishStartSpeaking", utterance);
}
}
@Override
public void onAudioEnd(AudioData data) {
log.info("onAudioEnd {} {}", getName(), data.toString());
// filters on only our speech
if (utterances.containsKey(data)) {
String utterance = utterances.get(data);
invoke("publishEndSpeaking", utterance);
utterances.remove(data);
}
}
public static void main(String[] args) {
LoggingFactory.init(Level.INFO);
try {
// Runtime.start("webgui", "WebGui");
AcapelaSpeech speech = (AcapelaSpeech) Runtime.start("speech", "AcapelaSpeech");
// speech.setVoice("Ryan");
// TODO: fix the volume control
// speech.setVolume(0);
speech.speakBlocking("does this work");
speech.speakBlocking("uh oh");
speech.speakBlocking("to be or not to be that is the question, weather tis nobler in the mind to suffer the slings and arrows of ");
speech.speakBlocking("I'm afraid I can't do that.");
// speech.speak("this is a test");
//
// speech.speak("i am saying something new once again again");
// speech.speak("one");
// speech.speak("two");
// speech.speak("three");
// speech.speak("four");
/*
* speech.speak("what is going on"); //speech.speakBlocking(
* "Répète après moi"); speech.speak( "hello there my name is ryan");
* speech.speak("hello world"); speech.speak("one two three four");
*/
// arduino.setBoard(Arduino.BOARD_TYPE_ATMEGA2560);
// arduino.connect(port);
// arduino.broadcastState();
} catch (Exception e) {
Logging.logError(e);
}
}
/**
* This static method returns all the details of the class without it having
* to be constructed. It has description, categories, dependencies, and peer
* definitions.
*
* @return ServiceType - returns all the data
*
*/
static public ServiceType getMetaData() {
ServiceType meta = new ServiceType(AcapelaSpeech.class.getCanonicalName());
meta.addDescription("Acapela group speech synthesis service.");
meta.addCategory("speech");
meta.setSponsor("GroG");
meta.addPeer("audioFile", "AudioFile", "audioFile");
meta.addTodo("test speak blocking - also what is the return type and AudioFile audio track id ?");
meta.addDependency("org.apache.commons.httpclient", "4.5.2");
return meta;
}
}