package org.myrobotlab.service; import java.awt.Frame; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.sound.sampled.AudioInputStream; import javax.swing.JDialog; import javax.swing.JOptionPane; import org.apache.commons.codec.digest.DigestUtils; import org.myrobotlab.framework.Service; import org.myrobotlab.framework.ServiceType; 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.SpeechRecognizer; import org.myrobotlab.service.interfaces.SpeechSynthesis; import org.myrobotlab.service.interfaces.TextListener; import org.slf4j.Logger; import org.xml.sax.SAXException; import marytts.LocalMaryInterface; import marytts.MaryInterface; import marytts.exceptions.SynthesisException; import marytts.tools.install.ComponentDescription; import marytts.tools.install.ComponentDescription.Status; import marytts.tools.install.InstallFileParser; import marytts.tools.install.LanguageComponentDescription; import marytts.tools.install.LicenseRegistry; import marytts.tools.install.ProgressPanel; import marytts.tools.install.VoiceComponentDescription; import marytts.util.MaryUtils; import marytts.util.data.audio.AudioPlayer; public class MarySpeech extends Service implements TextListener, SpeechSynthesis { public final static Logger log = LoggerFactory.getLogger(MarySpeech.class); private static final long serialVersionUID = 1L; transient MaryInterface marytts = null; String INSTALLFILEURL = "https://raw.github.com/marytts/marytts/master/download/marytts-components.xml"; private List<LanguageComponentDescription> possibleLanguages; private List<VoiceComponentDescription> possibleVoices; String installationstate = "noinstallationstarted"; Object installationstateparam1; Object installationstateparam2; transient List<ComponentDescription> installation_toInstall; // we need to subclass the audio player class here, so we know when the run // method exits and we can invoke // publish end speaking from it. private class MRLAudioPlayer extends AudioPlayer { private final String utterance; public MRLAudioPlayer(AudioInputStream ais, String utterance) { super(ais); this.utterance = utterance; } @Override public void run() { invoke("publishStartSpeaking", utterance); // give a small pause for sphinx to stop listening? try { Thread.sleep(100); log.info("Ok.. here we go."); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } super.run(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } invoke("publishEndSpeaking", utterance); } } @Override public boolean speakBlocking(String toSpeak) throws SynthesisException, InterruptedException { return speakInternal(toSpeak, true); } public boolean speakInternal(String toSpeak, boolean blocking) throws SynthesisException, InterruptedException { AudioInputStream audio; log.info("speakInternal Blocking {} Text: {}", blocking, toSpeak); if (toSpeak == null || toSpeak.length() == 0) { log.info("speech null or empty"); return false; } audio = marytts.generateAudio(toSpeak); // invoke("publishStartSpeaking", toSpeak); MRLAudioPlayer player = new MRLAudioPlayer(audio, toSpeak); // player.setAudio(audio); player.start(); // To make this blocking you can join the player thread. if (blocking) { player.join(); } // TODO: if this isn't blocking, we might just return immediately, rather // than // saying when the player has finished. // invoke("publishEndSpeaking", toSpeak); return true; } public MarySpeech(String reservedKey) { super(reservedKey); File file = new File("mary"); if (!file.exists()) { file.mkdirs(); } file = new File("mary\\download"); if (!file.exists()) { file.mkdirs(); } file = new File("mary\\installed"); if (!file.exists()) { file.mkdirs(); } file = new File("mary\\lib"); if (!file.exists()) { file.mkdirs(); } file = new File("mary\\log"); if (!file.exists()) { file.mkdirs(); } System.setProperty("mary.base", "mary"); System.setProperty("mary.downloadDir", "mary\\download"); System.setProperty("mary.installedDir", "mary\\installed"); try { // updateFromComponentUrl(); marytts = new LocalMaryInterface(); } catch (Exception e) { Logging.logError(e); } // Grab the first voice that's available. :-/ Set<String> voices = marytts.getAvailableVoices(); marytts.setVoice(voices.iterator().next()); } @Override public void onText(String text) { log.info("ON Text Called: {}", text); try { speak(text); } catch (Exception e) { Logging.logError(e); } } @Override public AudioData speak(String toSpeak) throws SynthesisException, InterruptedException { AudioData ret = new AudioData(toSpeak); // TODO: handle the isSpeaking logic/state speakInternal(toSpeak, false); // FIXME - play cache track return ret; } @Override public List<String> getVoices() { List<String> list = new ArrayList<>(marytts.getAvailableVoices()); return list; } @Override public boolean setVoice(String voice) { marytts.setVoice(voice); return true; // setVoice is void - if voice isn't available it throws an // exception } @Override public void setLanguage(String l) { marytts.setLocale(Locale.forLanguageTag(l)); } @Override public void onRequestConfirmation(String text) { try { // FIXME - not exactly language independent speakBlocking(String.format("did you say. %s", text)); } catch (Exception e) { Logging.logError(e); } } @Override public String getLanguage() { return marytts.getLocale().getLanguage(); } @Override public void setVolume(float volume) { // TODO Auto-generated method stub } @Override public float getVolume() { // TODO Auto-generated method stub return 0; } @Override public void interrupt() { // TODO Auto-generated method stub } @Override public String publishStartSpeaking(String utterance) { // TODO Auto-generated method stub log.info("Starting to speak: {}", utterance); return utterance; } @Override public String publishEndSpeaking(String utterance) { // TODO Auto-generated method stub log.info("End speaking: {}", utterance); return utterance; } @Override public String getVoice() { return marytts.getVoice(); } @Override public String getLocalFileName(SpeechSynthesis provider, String toSpeak, String audioFileType) throws UnsupportedEncodingException { return provider.getClass().getSimpleName() + File.separator + URLEncoder.encode(provider.getVoice(), "UTF-8") + File.separator + DigestUtils.md5Hex(toSpeak) + "." + audioFileType; } @Override public void addEar(SpeechRecognizer ear) { // when we add the ear, we need to listen for request confirmation addListener("publishStartSpeaking", ear.getName(), "onStartSpeaking"); addListener("publishEndSpeaking", ear.getName(), "onEndSpeaking"); } @Override public List<String> getLanguages() { List<String> ret = new ArrayList<>(); for (Locale locale : marytts.getAvailableLocales()) { ret.add(locale.getLanguage()); } return ret; } public static void main(String[] args) { LoggingFactory.init(Level.DEBUG); try { Runtime.start("webgui", "WebGui"); MarySpeech mary = (MarySpeech) Runtime.start("mary", "MarySpeech"); // mary.setVoice("dfki-spike en_GB male unitselection general"); // mary.speak("hello"); // mary.speak("world"); mary.speakBlocking("Hello world"); // mary.speakBlocking("I am Mary TTS and I am open source"); // mary.speakBlocking("and I will evolve quicker than any closed source // application if not in a short window of time"); // mary.speakBlocking("then in the long term evolution of software"); // mary.speak("Hello world"); } catch (Exception e) { Logging.logError(e); } } public void updateFromComponentUrl(String url) throws IOException, SAXException { // InstallFileParser p = new InstallFileParser(new URL(INSTALLFILEURL)); InstallFileParser p = new InstallFileParser(new URL(url)); possibleLanguages = p.getLanguageDescriptions(); possibleVoices = p.getVoiceDescriptions(); broadcastState(); } public void installSelectedLanguagesAndVoices(String[] toInstall_) { // TODO - remove last remaining parts of swing !!! (transform them) // a lot of code is copied and modified from // marytts.tools.install.InstallerGUI System.out.println("toInstall" + Arrays.toString(toInstall_)); for (VoiceComponentDescription voice : possibleVoices) { voice.setSelected(false); for (String toInstallVoice : toInstall_) { if (toInstallVoice.equals(voice.getName())) { voice.setSelected(true); } } } long downloadSize = 0; List<ComponentDescription> toInstall = new ArrayList<>(); for (VoiceComponentDescription voice : possibleVoices) { if (voice.isSelected() && (voice.getStatus() != ComponentDescription.Status.INSTALLED || voice.isUpdateAvailable())) { toInstall.add(voice); } } if (toInstall.isEmpty()) { // move to WebGui installationstate = "nothingselected"; broadcastState(); return; } // TODO - would be nice to enable this, but would require more hacking of // InstallerGUI // // Verify if all dependencies are met // // There are the following ways of meeting a dependency: // // - the component with the right name and version number is already // installed; // // - the component with the right name and version number is selected for // installation; // // - an update of the component with the right version number is selected // for installation. // Map<String, String> unmetDependencies = new TreeMap<String, String>(); // // map name to problem description // for (ComponentDescription cd : toInstall) { // if (cd instanceof VoiceComponentDescription) { // // Currently have dependencies only for voice components // VoiceComponentDescription vcd = (VoiceComponentDescription) cd; // String depLang = vcd.getDependsLanguage(); // String depVersion = vcd.getDependsVersion(); // // Two options for fulfilling the dependency: either it is already // installed, or it is in toInstall // LanguageComponentDescription lcd = languages.get(depLang); // if (lcd == null) { // unmetDependencies.put(depLang, "-- no such language component"); // } else if (lcd.getStatus() == ComponentDescription.Status.INSTALLED) { // if (ComponentDescription.isVersionNewerThan(depVersion, // lcd.getVersion())) { // ComponentDescription update = lcd.getAvailableUpdate(); // if (update == null) { // unmetDependencies.put(depLang, "version " + depVersion + " is required by // " + vcd.getName() // + ",\nbut older version " + lcd.getVersion() + " is installed and no // update is available"); // } else if (ComponentDescription.isVersionNewerThan(depVersion, // update.getVersion())) { // unmetDependencies.put(depLang, "version " + depVersion + " is required by // " + vcd.getName() // + ",\nbut only version " + update.getVersion() + " is available as an // update"); // } else if (!toInstall.contains(lcd)) { // unmetDependencies.put(depLang, "version " + depVersion + " is required by // " + vcd.getName() // + ",\nbut older version " + lcd.getVersion() + " is installed\nand update // to version " // + update.getVersion() + " is not selected for installation"); // } // } // } else if (!toInstall.contains(lcd)) { // if (ComponentDescription.isVersionNewerThan(depVersion, // lcd.getVersion())) { // unmetDependencies.put(depLang, "version " + depVersion + " is required by // " + vcd.getName() // + ",\nbut only older version " + lcd.getVersion() + " is available"); // } else { // unmetDependencies.put(depLang, "is required by " + vcd.getName() // + "\nbut is not selected for installation"); // } // } // } // } // // Any unmet dependencies? // if (unmetDependencies.size() > 0) { // StringBuilder buf = new StringBuilder(); // for (String compName : unmetDependencies.keySet()) { // buf.append("Component ").append(compName).append(" // ").append(unmetDependencies.get(compName)).append("\n"); // } // JOptionPane.showMessageDialog(this, buf.toString(), "Dependency problem", // JOptionPane.WARNING_MESSAGE); // return; // } // for (ComponentDescription cd : toInstall) { if (cd.getStatus() == ComponentDescription.Status.AVAILABLE) { downloadSize += cd.getPackageSize(); } else if (cd.getStatus() == ComponentDescription.Status.INSTALLED && cd.isUpdateAvailable()) { if (cd.getAvailableUpdate().getStatus() == ComponentDescription.Status.AVAILABLE) { downloadSize += cd.getAvailableUpdate().getPackageSize(); } } } installation_toInstall = toInstall; installationstate = "installcomponents"; installationstateparam1 = toInstall.size() + ""; installationstateparam2 = MaryUtils.toHumanReadableSize(downloadSize); broadcastState(); } public void installSelectedLanguagesAndVoices2() { List<ComponentDescription> toInstall = installation_toInstall; System.out.println("Check license(s)"); Map<URL, SortedSet<ComponentDescription>> licenseGroups = new HashMap<>(); // Group components by their license: for (ComponentDescription cd : toInstall) { URL licenseURL = cd.getLicenseURL(); // may be null // null is an acceptable key for HashMaps, so it's OK. SortedSet<ComponentDescription> compsUnderLicense = licenseGroups.get(licenseURL); if (compsUnderLicense == null) { compsUnderLicense = new TreeSet<>(); licenseGroups.put(licenseURL, compsUnderLicense); } assert compsUnderLicense != null; compsUnderLicense.add(cd); } Map<URL, String> licenseContents = new HashMap<>(); for (URL licenseURL : licenseGroups.keySet()) { if (licenseURL == null) { continue; } URL localURL = LicenseRegistry.getLicense(licenseURL); File file; try { file = new File(localURL.toURI()); } catch (URISyntaxException e) { file = new File(localURL.getPath()); } try { byte[] encoded = Files.readAllBytes(Paths.get(file.getPath())); String content = new String(encoded, "UTF-8"); licenseContents.put(licenseURL, content); } catch (IOException ex) { } } installationstate = "showlicenses"; installationstateparam1 = licenseGroups; installationstateparam2 = licenseContents; broadcastState(); } public void installSelectedLanguagesAndVoices3() { List<ComponentDescription> toInstall = installation_toInstall; System.out.println("Starting installation"); InstallationThread installthread = new InstallationThread(toInstall, true); new Thread(installthread).start(); // showProgressPanel(toInstall, true); } private void showProgressPanel(List<ComponentDescription> comps, boolean install) { final ProgressPanel pp = new ProgressPanel(comps, install); final JOptionPane optionPane = new JOptionPane(pp, JOptionPane.PLAIN_MESSAGE, JOptionPane.DEFAULT_OPTION, null, new String[] { "Abort" }, "Abort"); // optionPane.setPreferredSize(new Dimension(640,480)); final JDialog dialog = new JDialog((Frame) null, "Progress", false); dialog.setContentPane(optionPane); optionPane.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { String prop = e.getPropertyName(); if (dialog.isVisible() && (e.getSource() == optionPane) && (prop.equals(JOptionPane.VALUE_PROPERTY))) { pp.requestExit(); dialog.setVisible(false); } } }); dialog.pack(); dialog.setVisible(true); new Thread(pp).start(); } private class InstallationThread extends javax.swing.JPanel implements Runnable, Observer { /** * */ private static final long serialVersionUID = 1L; private List<ComponentDescription> allComponents; private ComponentDescription currentComponent = null; private boolean install; private boolean exitRequested = false; private int oldprogress; /** * Creates new form ProgressPanel * * @param componentsToProcess * componentsToProcess * @param install * install */ public InstallationThread(List<ComponentDescription> componentsToProcess, boolean install) { this.allComponents = componentsToProcess; this.install = install; } public synchronized void requestExit() { this.exitRequested = true; if (currentComponent != null) { currentComponent.cancel(); } } private synchronized boolean isExitRequested() { return exitRequested; } private void setCurrentComponent(ComponentDescription desc) { if (currentComponent != null) { currentComponent.deleteObserver(this); } currentComponent = desc; if (currentComponent != null) { currentComponent.addObserver(this); } verifyCurrentComponentDisplay(); } @Override public void run() { boolean error = false; ComponentDescription problematic = null; int i = 0; int max = allComponents.size(); installationstate = "installationprogress"; installationstateparam1 = new OverallProgress(-1, max); installationstateparam2 = null; broadcastState(); String action = install ? "install" : "uninstall"; for (ComponentDescription comp : allComponents) { if (isExitRequested()) { return; } System.out.println("Now " + action + "ing " + comp.getName() + "..."); installationstate = "installationprogress"; installationstateparam1 = new OverallProgress(i, max); installationstateparam2 = null; broadcastState(); setCurrentComponent(comp); if (install) { // ComponentDescription orig = null; if (comp.getStatus() == Status.INSTALLED) { // Installing an installed // component really means // replacing it with // its updated version assert comp.isUpdateAvailable(); // 1. uninstall current version; 2. install replacement comp.uninstall(); if (comp.getStatus() == Status.ERROR) { error = true; } else if (comp.isUpdateAvailable()) { comp.replaceWithUpdate(); } // And from here on, treat comp like any other component to install } if (!error && comp.getStatus() == Status.AVAILABLE || comp.getStatus() == Status.CANCELLED) { comp.download(true); if (comp.getStatus() == Status.ERROR) { error = true; } } if (!error && comp.getStatus() == Status.DOWNLOADED) { try { comp.install(true); } catch (Exception e) { e.printStackTrace(); error = true; } if (comp.getStatus() == Status.ERROR) { error = true; } } } else // uninstall if (comp.getStatus() == Status.INSTALLED) { comp.uninstall(); if (comp.getStatus() == Status.ERROR) { error = true; } else if (comp.isUpdateAvailable()) { comp.replaceWithUpdate(); } } if (error) { problematic = comp; System.err.println("Could not " + action + " " + comp.getName()); break; } comp.setSelected(false); i++; } if (error) { assert problematic != null; JOptionPane.showMessageDialog(this, "Could not " + action + " " + problematic.getName()); } else { installationstate = "installationprogress"; installationstateparam1 = new OverallProgress(max, max); installationstateparam2 = null; broadcastState(); // JOptionPane.showMessageDialog(this, max + " components "+action+"ed // successfully."); } this.setCurrentComponent(null); this.getTopLevelAncestor().setVisible(false); } @Override public void update(Observable o, Object arg) { if (o != currentComponent) { throw new IllegalStateException("We are observing " + o + " but the currentComponent is " + currentComponent); } verifyCurrentComponentDisplay(); } private void verifyCurrentComponentDisplay() { if (currentComponent == null) { return; } String name = currentComponent.getName(); String status = currentComponent.getStatus().toString(); int progress = currentComponent.getProgress(); if (progress < 0) { installationstate = "installationprogress"; installationstateparam1 = null; installationstateparam2 = new CurrentProgress(name, status, 0); // broadcastState(); } else { installationstate = "installationprogress"; installationstateparam1 = null; installationstateparam2 = new CurrentProgress(name, status, progress); // broadcastState(); if (oldprogress != progress) { oldprogress = progress; // StackOverflow in webgui somewhere here - dunno why // -seems unrelated yet related // System.out.println("MarySpeech - downloading|update" + progress); } } } private class OverallProgress { // int current; // int max; public OverallProgress(int current, int max) { // this.current = current; // this.max = max; } } private class CurrentProgress { // String name; // String status; // int progress; public CurrentProgress(String name, String status, int progress) { // this.name = name; // this.status = status; // this.progress = progress; } } } /** * 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(MarySpeech.class.getCanonicalName()); meta.addDescription("Speech synthesis based on MaryTTS"); meta.addCategory("speech", "sound"); meta.addDependency("marytts", "5.1.2"); meta.addDependency("com.sun.speech.freetts", "1.2"); meta.addDependency("opennlp", "1.6"); return meta; } public List<LanguageComponentDescription> getPossibleLanguages() { return possibleLanguages; } }