org.myrobotlab.service.MarySpeech.java Source code

Java tutorial

Introduction

Here is the source code for org.myrobotlab.service.MarySpeech.java

Source

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;
    }
}