/** * Copyright 2000-2006 DFKI GmbH. * All Rights Reserved. Use is subject to license terms. * * This file is part of MARY TTS. * * MARY TTS is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ package marytts.server; // General Java Classes import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.URL; import java.net.URLClassLoader; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.StringTokenizer; import java.util.TreeSet; import javax.sound.sampled.AudioFileFormat; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.TransformerFactory; import marytts.Version; import marytts.datatypes.MaryDataType; import marytts.exceptions.MaryConfigurationException; import marytts.exceptions.NoSuchPropertyException; import marytts.features.FeatureProcessorManager; import marytts.features.FeatureRegistry; import marytts.modules.MaryModule; import marytts.modules.ModuleRegistry; import marytts.modules.Synthesis; import marytts.modules.synthesis.Voice; import marytts.util.MaryCache; import marytts.util.MaryRuntimeUtils; import marytts.util.MaryUtils; import marytts.util.Pair; import marytts.util.data.audio.MaryAudioUtils; import marytts.util.io.FileUtils; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; /** * The main program for the mary TtS system. It can run as a socket server or as a stand-alone program. * * @author Marc Schröder */ public class Mary { public static final int STATE_OFF = 0; public static final int STATE_STARTING = 1; public static final int STATE_RUNNING = 2; public static final int STATE_SHUTTING_DOWN = 3; private static Logger logger; private static int currentState = STATE_OFF; private static boolean jarsAdded = false; /** * Inform about system state. * * @return an integer representing the current system state. * @see #STATE_OFF * @see #STATE_STARTING * @see #STATE_RUNNING * @see #STATE_SHUTTING_DOWN */ public static int currentState() { return currentState; } /** * Add jars to classpath. Normally this is called from startup(). * * @throws Exception * Exception */ protected static void addJarsToClasspath() throws Exception { if (true) return; // TODO: clean this up when the new modularity mechanism is in place if (jarsAdded) return; // have done this already File jarDir = new File(MaryProperties.maryBase() + "/java"); File[] jarFiles = jarDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); assert jarFiles != null; URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader(); Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class }); method.setAccessible(true); for (int i = 0; i < jarFiles.length; i++) { URL jarURL = new URL("file:" + jarFiles[i].getPath()); method.invoke(sysloader, new Object[] { jarURL }); } jarsAdded = true; } private static void startModules() throws ClassNotFoundException, InstantiationException, Exception { for (String moduleClassName : MaryProperties.moduleInitInfo()) { MaryModule m = ModuleRegistry.instantiateModule(moduleClassName); // Partially fill module repository here; // TODO: voice-specific entries will be added when each voice is loaded. ModuleRegistry.registerModule(m, m.getLocale(), null); } ModuleRegistry.setRegistrationComplete(); List<Pair<MaryModule, Long>> startupTimes = new ArrayList<Pair<MaryModule, Long>>(); // Separate loop for startup allows modules to cross-reference to each // other via Mary.getModule(Class) even if some have not yet been // started. for (MaryModule m : ModuleRegistry.getAllModules()) { // Only start the modules here if in server mode: if (((!MaryProperties.getProperty("server").equals("commandline")) || m instanceof Synthesis) && m.getState() == MaryModule.MODULE_OFFLINE) { long before = System.currentTimeMillis(); try { m.startup(); } catch (Throwable t) { throw new Exception("Problem starting module " + m.name(), t); } long after = System.currentTimeMillis(); startupTimes.add(new Pair<MaryModule, Long>(m, after - before)); } if (MaryProperties.getAutoBoolean("modules.poweronselftest", false)) { m.powerOnSelfTest(); } } if (startupTimes.size() > 0) { Collections.sort(startupTimes, new Comparator<Pair<MaryModule, Long>>() { public int compare(Pair<MaryModule, Long> o1, Pair<MaryModule, Long> o2) { return -o1.getSecond().compareTo(o2.getSecond()); } }); logger.debug("Startup times:"); for (Pair<MaryModule, Long> p : startupTimes) { logger.debug(p.getFirst().name() + ": " + p.getSecond() + " ms"); } } } private static void setupFeatureProcessors() throws Exception { for (String fpmInitInfo : MaryProperties.getList("featuremanager.classes.list")) { try { FeatureProcessorManager mgr = (FeatureProcessorManager) MaryRuntimeUtils.instantiateObject(fpmInitInfo); Locale locale = mgr.getLocale(); if (locale != null) { FeatureRegistry.setFeatureProcessorManager(locale, mgr); } else { logger.debug("Setting fallback feature processor manager to '" + fpmInitInfo + "'"); FeatureRegistry.setFallbackFeatureProcessorManager(mgr); } } catch (Throwable t) { throw new Exception("Cannot instantiate feature processor manager '" + fpmInitInfo + "'", t); } } } /** * Start the MARY system and all modules. This method must be called once before any calls to * {@link #process(String input, String inputTypeName, String outputTypeName, String localeString, String audioTypeName, String voiceName, String style, String effects, String outputTypeParams, OutputStream output)} * are possible. The method will dynamically extend the classpath to all jar files in MARY_BASE/java/*.jar. Use * <code>startup(false)</code> if you do not want to automatically extend the classpath in this way. * * @throws IllegalStateException * if the system is not offline. * @throws Exception * Exception */ public static void startup() throws Exception { startup(true); } /** * Start the MARY system and all modules. This method must be called once before any calls to * {@link #process(String input, String inputTypeName, String outputTypeName, String localeString, String audioTypeName, String voiceName, String style, String effects, String outputTypeParams, OutputStream output)} * are possible. * * @param addJarsToClasspath * if true, the method will dynamically extend the classpath to all jar files in MARY_BASE/java/*.jar; if false, * the classpath will remain unchanged. * @throws IllegalStateException * if the system is not offline. * @throws Exception * Exception */ public static void startup(boolean addJarsToClasspath) throws Exception { if (currentState != STATE_OFF) throw new IllegalStateException("Cannot start system: it is not offline"); currentState = STATE_STARTING; if (addJarsToClasspath) { addJarsToClasspath(); } configureLogging(); logger.info("Mary starting up..."); logger.info("Specification version " + Version.specificationVersion()); logger.info("Implementation version " + Version.implementationVersion()); logger.info("Running on a Java " + System.getProperty("java.version") + " implementation by " + System.getProperty("java.vendor") + ", on a " + System.getProperty("os.name") + " platform (" + System.getProperty("os.arch") + ", " + System.getProperty("os.version") + ")"); logger.debug("MARY_BASE: " + MaryProperties.maryBase()); String[] installedFilenames = new File(MaryProperties.maryBase() + "/installed").list(); if (installedFilenames == null) { logger.debug("The installed/ folder does not exist."); } else { StringBuilder installedMsg = new StringBuilder(); for (String filename : installedFilenames) { if (installedMsg.length() > 0) { installedMsg.append(", "); } installedMsg.append(filename); } logger.debug("Content of installed/ folder: " + installedMsg); } String[] confFilenames = new File(MaryProperties.maryBase() + "/conf").list(); if (confFilenames == null) { logger.debug("The conf/ folder does not exist."); } else { StringBuilder confMsg = new StringBuilder(); for (String filename : confFilenames) { if (confMsg.length() > 0) { confMsg.append(", "); } confMsg.append(filename); } logger.debug("Content of conf/ folder: " + confMsg); } logger.debug("Full dump of system properties:"); for (Object key : new TreeSet<Object>(System.getProperties().keySet())) { logger.debug(key + " = " + System.getProperties().get(key)); } logger.debug("XML libraries used:"); logger.debug("DocumentBuilderFactory: " + DocumentBuilderFactory.newInstance().getClass()); try { Class<? extends Object> xercesVersion = Class.forName("org.apache.xerces.impl.Version"); logger.debug(xercesVersion.getMethod("getVersion").invoke(null)); } catch (Exception e) { // Not xerces, no version number } logger.debug("TransformerFactory: " + TransformerFactory.newInstance().getClass()); try { // Nov 2009, Marc: This causes "[Deprecated] Xalan: org.apache.xalan.Version" to be written to the console. // Class xalanVersion = Class.forName("org.apache.xalan.Version"); // logger.debug(xalanVersion.getMethod("getVersion").invoke(null)); } catch (Exception e) { // Not xalan, no version number } // Essential environment checks: EnvironmentChecks.check(); Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { shutdown(); } }); setupFeatureProcessors(); // Instantiate module classes and startup modules: startModules(); logger.info("Startup complete."); currentState = STATE_RUNNING; } /** * Log4j initialisation, called from {@link #startup(boolean)}. * * @throws NoSuchPropertyException * NoSuchPropertyException * @throws IOException * IOException */ private static void configureLogging() throws MaryConfigurationException, IOException { if (!MaryUtils.isLog4jConfigured()) { // maybe log4j has been externally configured already? // Configure logging: /* * logger = MaryUtils.getLogger("main"); * Logger.getRootLogger().setLevel(Level.toLevel(MaryProperties.needProperty("log.level"))); PatternLayout layout = * new PatternLayout("%d [%t] %-5p %-10c %m\n"); File logFile = null; if * (MaryProperties.needAutoBoolean("log.tofile")) { String filename = MaryProperties.getFilename("log.filename", * "mary.log"); logFile = new File(filename); File parentFile = logFile.getParentFile(); // prevent a * NullPointerException in the following conditional if the user has requested a non-existing, *relative* log filename * if (parentFile == null) { parentFile = new File(logFile.getAbsolutePath()).getParentFile(); } if * (!(logFile.exists()&&logFile.canWrite() // exists and writable || parentFile.exists() && parentFile.canWrite())) { * // parent exists and writable // cannot write to file * System.err.print("\nCannot write to log file '"+filename+"' -- "); File fallbackLogFile = new * File(System.getProperty("user.home")+"/mary.log"); if (fallbackLogFile.exists()&&fallbackLogFile.canWrite() // * exists and writable || fallbackLogFile.exists()&&fallbackLogFile.canWrite()) { // parent exists and writable // * fallback log file is OK System.err.println("will log to '"+fallbackLogFile.getAbsolutePath()+"' instead."); logFile * = fallbackLogFile; } else { // cannot write to fallback log either * System.err.println("will log to standard output instead."); logFile = null; } } if (logFile != null && * logFile.exists()) logFile.delete(); } if (logFile != null) { BasicConfigurator.configure(new FileAppender(layout, * logFile.getAbsolutePath())); } else { BasicConfigurator.configure(new WriterAppender(layout, System.err)); } */ Properties logprops = new Properties(); InputStream propIS = new BufferedInputStream(MaryProperties.needStream("log.config")); logprops.load(propIS); propIS.close(); // Now replace MARY_BASE with the install location of MARY in every property: for (Object key : logprops.keySet()) { String val = (String) logprops.get(key); if (val.contains("MARY_BASE")) { String maryBase = MaryProperties.maryBase(); if (maryBase.contains("\\")) { maryBase = maryBase.replaceAll("\\\\", "/"); } val = val.replaceAll("MARY_BASE", maryBase); logprops.put(key, val); } } // And allow MaryProperties (and thus System properties) to overwrite the single entry // log4j.logger.marytts: String loggerMaryttsKey = "log4j.logger.marytts"; String loggerMaryttsValue = MaryProperties.getProperty(loggerMaryttsKey); if (loggerMaryttsValue != null) { logprops.setProperty(loggerMaryttsKey, loggerMaryttsValue); } PropertyConfigurator.configure(logprops); } logger = MaryUtils.getLogger("main"); } /** * Orderly shut down the MARY system. * * @throws IllegalStateException * if the MARY system is not running. */ public static void shutdown() { if (currentState != STATE_RUNNING) throw new IllegalStateException("MARY system is not running"); currentState = STATE_SHUTTING_DOWN; logger.info("Shutting down modules..."); // Shut down modules: for (MaryModule m : ModuleRegistry.getAllModules()) { if (m.getState() == MaryModule.MODULE_RUNNING) m.shutdown(); } if (MaryCache.haveCache()) { MaryCache cache = MaryCache.getCache(); try { cache.shutdown(); } catch (SQLException e) { logger.warn("Cannot shutdown cache: ", e); } } logger.info("Shutdown complete."); currentState = STATE_OFF; } /** * Process input into output using the MARY system. For inputType TEXT and output type AUDIO, this does text-to-speech * conversion; for other settings, intermediate processing results can be generated or provided as input. * * @param input * input * @param inputTypeName * inputTypeName * @param outputTypeName * outputTypeName * @param localeString * localeString * @param audioTypeName * audioTypeName * @param voiceName * voiceName * @param style * style * @param effects * effects * @param outputTypeParams * outputTypeParams * @param output * the output stream into which the processing result will be written. * @throws IllegalStateException * if the MARY system is not running. * @throws Exception * Exception */ public static void process(String input, String inputTypeName, String outputTypeName, String localeString, String audioTypeName, String voiceName, String style, String effects, String outputTypeParams, OutputStream output) throws Exception { if (currentState != STATE_RUNNING) throw new IllegalStateException("MARY system is not running"); MaryDataType inputType = MaryDataType.get(inputTypeName); MaryDataType outputType = MaryDataType.get(outputTypeName); Locale locale = MaryUtils.string2locale(localeString); Voice voice = null; if (voiceName != null) voice = Voice.getVoice(voiceName); AudioFileFormat audioFileFormat = null; AudioFileFormat.Type audioType = null; if (audioTypeName != null) { audioType = MaryAudioUtils.getAudioFileFormatType(audioTypeName); AudioFormat audioFormat = null; if (audioTypeName.equals("MP3")) { audioFormat = MaryRuntimeUtils.getMP3AudioFormat(); } else if (audioTypeName.equals("Vorbis")) { audioFormat = MaryRuntimeUtils.getOggAudioFormat(); } else if (voice != null) { audioFormat = voice.dbAudioFormat(); } else { audioFormat = Voice.AF22050; } audioFileFormat = new AudioFileFormat(audioType, audioFormat, AudioSystem.NOT_SPECIFIED); } Request request = new Request(inputType, outputType, locale, voice, effects, style, 1, audioFileFormat, false, outputTypeParams); request.setInputData(input); request.process(); request.writeOutputData(output); } /** * The starting point of the standalone Mary program. If server mode is requested by property settings, starts the * <code>MaryServer</code>; otherwise, a <code>Request</code> is created reading from the file given as first argument and * writing to System.out. * * <p> * Usage: * <p> * As a socket server: * * <pre> * java -Dmary.base=$MARY_BASE -Dserver=true marytts.server.Mary * </pre> * <p> * As a stand-alone program: * * <pre> * java -Dmary.base=$MARY_BASE marytts.server.Mary myfile.txt * </pre> * * @param args * args * @throws Exception * Exception * @see MaryProperties * @see MaryServer * @see RequestHandler * @see Request */ public static void main(final String[] args) throws Exception { long startTime = System.currentTimeMillis(); addJarsToClasspath(); String server = MaryProperties.needProperty("server"); System.err.print("MARY server " + Version.specificationVersion() + " starting as "); if (server.equals("socket")) System.err.print("a socket server..."); else if (server.equals("http")) System.err.print("an HTTP server..."); else System.err.print("a command-line application..."); // first thing we do, let's test if the port is available: int localPort = MaryProperties.needInteger("socket.port"); if (!server.equals("commandline")) { try { ServerSocket serverSocket = new ServerSocket(localPort); serverSocket.close(); } catch (IOException e) { System.err.println("\nPort " + localPort + " already in use!"); throw e; } } startup(); System.err.println(" started in " + (System.currentTimeMillis() - startTime) / 1000. + " s on port " + localPort); Runnable main = null; if (server.equals("socket")) { // socket server mode main = (Runnable) Class.forName("marytts.server.MaryServer").newInstance(); } else if (server.equals("http")) { // http server mode main = (Runnable) Class.forName("marytts.server.http.MaryHttpServer").newInstance(); } else { // command-line mode main = new Runnable() { public void run() { try { InputStream inputStream; if (args.length == 0 || args[0].equals("-")) inputStream = System.in; else inputStream = new FileInputStream(args[0]); String input = FileUtils.getStreamAsString(inputStream, "UTF-8"); process(input, MaryProperties.getProperty("input.type", "TEXT"), MaryProperties.getProperty("output.type", "AUDIO"), MaryProperties.getProperty("locale", "en_US"), MaryProperties.getProperty("audio.type", "WAVE"), MaryProperties.getProperty("voice", null), MaryProperties.getProperty("style", null), MaryProperties.getProperty("effect", null), MaryProperties.getProperty("output.type.params", null), System.out); } catch (Exception e) { throw new RuntimeException(e); } } }; } main.run(); // shutdown(); } }