All Downloads are FREE. Search and download functionalities are using the official Maven repository.

marytts.tools.upgrade.Mary4To5VoiceConverter Maven / Gradle / Ivy

The newest version!
package marytts.tools.upgrade;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;

import com.twmacinta.util.MD5;

import marytts.Version;
import marytts.exceptions.MaryConfigurationException;
import marytts.tools.install.InstallFileParser;
import marytts.tools.install.VoiceComponentDescription;
import marytts.tools.voiceimport.VoiceCompiler;
import marytts.util.MaryUtils;
import marytts.util.dom.DomUtils;
import marytts.util.io.FileUtils;

public class Mary4To5VoiceConverter {

	private static final String EOL = IOUtils.LINE_SEPARATOR_WINDOWS;

	/**
	 * The list of property suffixes which can read from a resource file.
	 */
	private static final String[] PROPS_FOR_RESOURCES = new String[] { "Ftd", "Ftf", "Ftm", "Fts", "Fmd", "Fmf", "Fmm", "Fms",
			"Fgvf", "Fgvm", "Fgvs", "FeaFile", "trickyPhonesFile", "Fif", "targetCostWeights", "joinCostWeights", "cartFile",
			"duration.data", "F0.data", "midF0.data", "rightF0.data", "exampleTextFile", };

	private static final String[] PROPS_FOR_FILESYSTEM = new String[] { "featureFile", "joinCostFile", "unitsFile",
			"audioTimelineFile", "basenameTimeline", "vocalization.unitfile", "vocalization.timeline",
			"vocalization.featurefile", "vocalization.featureDefinitionFile", "vocalization.intonationfile",
			"vocalization.mlsafeaturefile", "vocalization.mixedexcitationfilter",
			"vocalization.intonation.featureDefinitionFile",
			// 4.0 prosody carts:
			"duration.cart", "duration.featuredefinition", "f0.cart.left", "f0.cart.mid", "f0.cart.right", "f0.featuredefinition" };

	/**
	 * The list of property suffixes which should be dropped when upgrading the config file.
	 */
	private static final String[] SUFFIXES_TO_DROP_UNITSEL = new String[] {};
	private static final String[] SUFFIXES_TO_DROP_HMM = new String[] { "F0.data", "duration.data", };
	private static final String[] PROPS_TO_DROP = new String[] { "requires", "provides", "voice.version", "en_US-voice.version",
			"de-voice.version", "it-voice.version", "requires.marybase.version", "requires.hmm.version",
			"requires.en_US.version", "requires.de.version", "requires.it.version", "requires.en_US.download",
			"requires.de.download", "requires.it.download", };

	private Logger logger;
	private VoiceComponentDescription voiceDescription;
	private File mary4Zip;
	private Properties config;
	private List originalConfig;

	private File extractedDir;
	private File compileDir;
	private String domain;
	private int samplingRate;
	private File[] filesForResources;
	private File[] filesForFilesystem;

	VoiceCompiler.MavenVoiceCompiler compiler;

	public Mary4To5VoiceConverter(List voiceDescriptions, File voiceZip) {
		voiceDescription = null;
		mary4Zip = voiceZip;
		for (VoiceComponentDescription d : voiceDescriptions) {
			if (d.getPackageFilename().equals(mary4Zip.getName())) {
				voiceDescription = d;
				break;
			}
		}
		if (voiceDescription == null) {
			throw new IllegalArgumentException("No matching voice description for file " + mary4Zip.getName());
		}
		if (!MaryUtils.isLog4jConfigured()) {
			BasicConfigurator.configure();
		}
		logger = Logger.getLogger(this.getClass());
		logger.info(voiceDescription.getName() + " " + voiceDescription.getVersion() + " (" + voiceDescription.getLocale() + " "
				+ voiceDescription.getGender() + ")");
	}

	private void convert() throws Exception {
		logger.info("converting...");
		File rootDir = mary4Zip.getParentFile();
		extractedDir = new File(rootDir, voiceDescription.getName() + "-" + voiceDescription.getVersion());
		logger.debug("... extracting archive to " + extractedDir.getPath());
		if (extractedDir.exists()) {
			logger.debug("Folder " + extractedDir.getPath() + " exists, trying to delete...");
			extractedDir.delete();
		}
		FileUtils.unzipArchive(mary4Zip, extractedDir);

		loadConfig(findConfigFile());

		compileDir = new File(rootDir, voiceDescription.getName() + "-" + Version.specificationVersion() + "-maven");

		domain = config.getProperty(getPropertyPrefix() + "domain");
		samplingRate = Integer.parseInt(config.getProperty(getPropertyPrefix() + "samplingRate"));

		filesForResources = getFilesForResources();
		filesForFilesystem = getFilesForFilesystem();
		Map extraVariablesToSubstitute = null;

		compiler = new VoiceCompiler.MavenVoiceCompiler(compileDir, getVoiceName(), Version.specificationVersion(),
				voiceDescription.getLocale(), voiceDescription.getGender(), domain, samplingRate, isUnitSelectionVoice(),
				filesForResources, filesForFilesystem, extraVariablesToSubstitute);

		logger.debug("Creating directories");
		compiler.createDirectories();

		logger.debug("Copying template files");
		compiler.copyTemplateFiles();

		updateConfig(compiler.getPackageName());
		saveConfig(compiler.getConfigFile());

		logger.debug("Copying voice files");
		compiler.copyVoiceFiles();

		if (!isUnitSelectionVoice()) {
			logger.debug("Converting HMM PDF files from Mary 4.0 to Mary 5.0 format");
			convertMary4ToMary5HmmPdfFiles(compiler.getMainResourcesDir());
		}

		logger.debug("Compiling with Maven");
		compiler.compileWithMaven();

		String convertedZipFilename = getFilenamePrefix() + ".zip";
		File convertedZipFile = new File(compileDir + "/target/" + convertedZipFilename);
		if (!convertedZipFile.exists()) {
			throw new IOException("Maven should have created file " + convertedZipFile.getAbsolutePath()
					+ " but file does not exist.");
		}

		updateVoiceDescription(rootDir, convertedZipFile);

		File finalZipFile = new File(rootDir, convertedZipFilename);
		if (finalZipFile.exists()) {
			finalZipFile.delete();
		}
		boolean success = convertedZipFile.renameTo(finalZipFile);
		if (!success) {
			throw new IOException("Failure trying to move " + convertedZipFile.getAbsolutePath() + " to "
					+ finalZipFile.getAbsolutePath());
		}

	}

	protected void saveConfig(File configFile) throws IOException {
		saveConfigToStream(new BufferedOutputStream(new FileOutputStream(configFile)));
	}

	private boolean isEmpty(String line) {
		return line.trim().isEmpty();
	}

	private boolean isComment(String line) {
		return line.trim().startsWith("#");

	}

	protected void saveConfigToStream(OutputStream out) throws IOException {
		PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
		StringBuilder comments = new StringBuilder();
		try {
			for (String line : originalConfig) {
				if (isEmpty(line) || isComment(line)) {
					comments.append(line).append(EOL);
					continue;
				}
				String key = new StringTokenizer(line.trim()).nextToken();
				if (config.containsKey(key)) {
					pw.print(comments.toString());
					pw.print(key + " = " + config.getProperty(key) + EOL);
					if (key.equals("name")) {
						pw.print("locale = " + config.getProperty("locale") + EOL);
					}
				}
				comments = new StringBuilder();
			}
		} finally {
			pw.flush();
			pw.close();
		}
	}

	private File[] getFilesForFilesystem() throws IOException {
		return getFilesFromProperties(PROPS_FOR_FILESYSTEM);
	}

	private File[] getFilesForResources() throws IOException {
		return getFilesFromProperties(PROPS_FOR_RESOURCES);
	}

	private File[] getFilesFromProperties(String[] propertySuffixes) throws IOException {
		ArrayList files = new ArrayList();
		for (String suffix : propertySuffixes) {
			String key = getPropertyPrefix() + suffix;
			if (config.containsKey(key)) {
				String value = config.getProperty(key);
				if (!value.startsWith("MARY_BASE")) {
					throw new IOException("Property '" + key + "' should hold a filename but the value is '" + value + "'");
				}
				value = value.replace("MARY_BASE", extractedDir.getAbsolutePath());
				File f = new File(value);
				if (!f.exists()) {
					throw new IOException("Config file refers to non-existing file '" + f.getAbsolutePath() + "'");
				}
				files.add(f);
			}
		}
		return files.toArray(new File[0]);
	}

	protected void updateConfig(String packageName) {
		updatePropsForResources(packageName);
		dropOutdatedProps();
		addNewProps();
	}

	private void addNewProps() {
		config.setProperty("locale", voiceDescription.getLocale().toString());

	}

	private void dropOutdatedProps() {
		String[] suffixesToDrop = isUnitSelectionVoice() ? SUFFIXES_TO_DROP_UNITSEL : SUFFIXES_TO_DROP_HMM;
		for (String suffix : suffixesToDrop) {
			String key = getPropertyPrefix() + suffix;
			config.remove(key);
		}
		for (String prop : PROPS_TO_DROP) {
			config.remove(prop);
		}
	}

	private void updatePropsForResources(String packageName) {
		String oldPrefix = "MARY_BASE/lib/voices/(.*)/";
		String newPrefix = "jar:/marytts/voice/" + packageName + "/";
		for (String suffix : PROPS_FOR_RESOURCES) {
			String key = getPropertyPrefix() + suffix;
			if (config.containsKey(key)) {
				String value = config.getProperty(key);
				value = value.replaceFirst(oldPrefix, newPrefix);
				config.setProperty(key, value);
			}
		}
	}

	private File findConfigFile() throws IOException {
		File confDir = new File(extractedDir, "conf");
		if (!confDir.isDirectory()) {
			throw new IOException("Expected directory " + confDir.getAbsolutePath() + " doesn't exist.");
		}
		File[] confFiles = confDir.listFiles();
		if (confFiles.length != 1) {
			throw new IOException("Conf directory " + confDir.getAbsolutePath()
					+ " should contain exactly one config file but contains " + confFiles.length);
		}
		return confFiles[0];
	}

	protected void loadConfig(File configFile) throws IOException {
		FileInputStream configStream = new FileInputStream(configFile);
		InputStream bufferedConfigStream = IOUtils.toBufferedInputStream(configStream);
		loadConfigFromStream(bufferedConfigStream);
	}

	protected void loadConfigFromStream(InputStream in) throws IOException {
		config = new Properties();
		try {
			byte[] byteArray = IOUtils.toByteArray(in);
			config.load(new ByteArrayInputStream(byteArray));
			originalConfig = IOUtils.readLines(new ByteArrayInputStream(byteArray), "UTF-8");
		} finally {
			in.close();
		}
	}

	/**
	 * Returns true for a unit selection voice, false for an HMM-based voice.
	 * 
	 * @return true if config.containsKey("unitselection.voices.list"), false if config.containsKey("hmm.voices.list")
	 * @throws UnsupportedOperationException
	 *             if the voice is neither a unit selection nor an HMM-based voice.
	 */
	protected boolean isUnitSelectionVoice() throws UnsupportedOperationException {
		if (config.containsKey("unitselection.voices.list")) {
			return true;
		} else if (config.containsKey("hmm.voices.list")) {
			return false;
		} else {
			throw new UnsupportedOperationException(
					"The voice is neither a unit selection voice nor an HMM-based voice -- cannot convert to MARY 5 format.");
		}
	}

	private String getVoiceNameFromConfig() {
		if (isUnitSelectionVoice()) {
			return config.getProperty("unitselection.voices.list");
		}
		return config.getProperty("hmm.voices.list");
	}

	private String getVoiceNameFromVoiceDescription() {
		return voiceDescription.getName();
	}

	protected String getVoiceName() {
		String voiceNameFromConfig = getVoiceNameFromConfig();
		String voiceNameFromVoiceDescription = getVoiceNameFromVoiceDescription();
		if (!voiceNameFromConfig.equals(voiceNameFromVoiceDescription)) {
			logger.warn("Name discrepancy: component.xml says '" + voiceNameFromVoiceDescription + "', config file says '"
					+ voiceNameFromConfig + "'");
		}
		return voiceNameFromVoiceDescription;
	}

	protected String getPropertyPrefix() {
		return "voice." + getVoiceNameFromConfig() + ".";
	}

	private String getFilenamePrefix() {
		return "voice-" + voiceDescription.getName() + "-" + Version.specificationVersion();
	}

	private void updateVoiceDescription(File rootDir, File packageFile) throws MalformedURLException,
			ParserConfigurationException, MaryConfigurationException, IOException {
		logger.debug("writing new voice description...");
		voiceDescription.setVersion(Version.specificationVersion());
		voiceDescription.setDependsVersion(Version.specificationVersion());
		voiceDescription.setPackageFilename(packageFile.getName());
		voiceDescription.setPackageMD5Sum(computeMD5(packageFile));
		voiceDescription.setPackageSize((int) packageFile.length());
		voiceDescription.removeAllLocations();
		voiceDescription.addLocation(URI.create("http://mary.dfki.de/download/" + Version.specificationVersion() + "/").toURL());
		Document doc = voiceDescription.createComponentXML();
		File newVoiceDescriptionFile = new File(rootDir, getFilenamePrefix() + "-component.xml");
		DomUtils.document2File(doc, newVoiceDescriptionFile);
		logger.debug("... created " + newVoiceDescriptionFile.getPath());
	}

	private String computeMD5(File packageFile) throws IOException {
		return MD5.asHex(MD5.getHash(packageFile));
	}

	private void convertMary4ToMary5HmmPdfFiles(File mainResourcesDir) throws Exception {

		File list[] = mainResourcesDir.listFiles();
		for (File f : list) {
			// if mainResources dir contains f0.pdf mgc.pdf str.pdf and
			if (f.getName().contains("dur.pdf") || f.getName().contains("lf0.pdf") || f.getName().contains("mgc.pdf")
					|| f.getName().contains("str.pdf")) {
				logger.debug("converting file: " + f.getName());
				convertPdfBinaryFile(f);
			}
			// if mainResource contains gv-lf0-littend.pdf, gv-mgc-littend.pdf and gv-str-littend.pdf
			else if (f.getName().contains("gv-lf0-littend.pdf") || f.getName().contains("gv-mgc-littend.pdf")
					|| f.getName().contains("gv-str-littend.pdf")) {
				logger.debug("converting file: " + f.getName());
				convertGvBinaryFile(f);
			}
		}

	}

	/**
	 * Converts format from pdf Mary format 4 to Mary 5, the converted file will have the same input name
	 * 
	 * @param pdfInFile
	 *            pdfInFile
	 * @throws Exception
	 *             Exception
	 */
	public void convertPdfBinaryFile(File pdfInFile) throws Exception {
		int i, j, k, l;
		boolean lf0 = false;

		String pdfInFileString = pdfInFile.getName();
		// the destination file name will be the same as the input file so
		String pdfOutFile = pdfInFile.getAbsolutePath();
		String path = pdfInFile.getParent();
		// I make a copy or the original file
		FileUtils.copy(pdfInFile.getAbsolutePath(), path + "/tmp");
		pdfInFile = new File(path + "/tmp");

		DataInputStream dataIn;
		DataOutputStream dataOut;

		dataIn = new DataInputStream(new BufferedInputStream(new FileInputStream(pdfInFile)));
		// numMSDFlag
		int numMSDFlag = 0;
		// numStream
		int numStream = 1; // 1 for mgc, str, mag or dur
		// vectorSize
		int vectorSize;
		// numDurPdf
		int numPdf[];

		logger.debug("Reading: from file " + pdfInFileString);
		float pdf[]; // pdf[vectorSize];
		float fval;
		int numState = 5;

		// ---------------------------------------------------------------------------------
		// ------------ Read header --------------------------------------------------------
		// ---------------------------------------------------------------------------------
		vectorSize = dataIn.readInt();
		if (pdfInFileString.contains("lf0.pdf")) {
			numStream = vectorSize;
			vectorSize = 4; // vectorSize = 4 --> [1]:mean f0, [2]:var f0, [3]:voiced weight, [4]:unvoiced weight
			lf0 = true;
			numMSDFlag = 1;
		} else if (pdfInFileString.contains("dur.pdf")) {
			/* 2*nstate because the vector size for duration is the number of states */
			// pdf = new double[1][numDurPdf][1][2*numState]; // just one state and one stream
			numState = 1;
			numStream = 5;
		}
		logger.debug("vectorSize(r) = " + vectorSize + " numMSDFlag=" + numMSDFlag + " numStream=" + numStream + " numState="
				+ numState);

		/* Now we need the number of pdf's for each state */
		numPdf = new int[numState];
		for (i = 0; i < numState; i++) {
			numPdf[i] = dataIn.readInt();
			logger.debug("loadPdfs(r): numPdf[state:" + i + "]=" + numPdf[i]);
			if (numPdf[i] < 0)
				throw new Exception("loadPdfs: #pdf at state " + i + " must be positive value.");
		}
		pdf = new float[2 * vectorSize];

		dataOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(pdfOutFile)));
		// This is the format in version 2.0
		// numMSDFlag
		dataOut.writeInt(numMSDFlag);
		// numStream
		dataOut.writeInt(numStream);
		// vectorSize (for lf0 vectorsize is the same numStream)
		if (lf0)
			dataOut.writeInt(numStream);
		else
			dataOut.writeInt(vectorSize);
		// numPdf per state
		for (i = 0; i < numState; i++) {
			dataOut.writeInt(numPdf[i]);
		}

		// ---------------------------------------------------------------------------------
		// -------------- Now read the data ------------------------------------------------
		// in the old version the mean vector goes first and then the cov
		// in the new version the mean and cov elements of the vector are one after another
		// ---------------------------------------------------------------------------------
		if (lf0) {
			/* read pdfs (mean, variance). (2*vectorSize because mean and diag variance */
			/* are allocated in only one vector. */
			for (i = 0; i < numState; i++) {
				for (j = 0; j < numPdf[i]; j++) {
					for (k = 0; k < numStream; k++) {
						for (l = 0; l < vectorSize; l++) {
							fval = dataIn.readFloat();
							// NOTE: Here (hts_engine v1.04) the order seem to be the same as before
							dataOut.writeFloat(fval);
						}

					}
				}
				// System.out.println("New pdf  j=" + j);
			}
		} else {
			/* read pdfs (mean, variance). (2*vectorSize because mean and diag variance */
			/* are allocated in only one vector. */
			for (i = 0; i < numState; i++) {
				for (j = 0; j < numPdf[i]; j++) {
					for (k = 0; k < (2 * vectorSize); k++) {
						pdf[k] = dataIn.readFloat();
					}
					for (k = 0; k < vectorSize; k++) {
						dataOut.writeFloat(pdf[k]);
						dataOut.writeFloat(pdf[k + vectorSize]);
					}
				}
				// System.out.println("New pdf  j=" + j);
			}

		}
		dataIn.close();
		dataOut.close();
		pdfInFile.delete();
		logger.debug("Updated format in file " + pdfOutFile);

	}

	/**
	 * Converts file format from gv Mary format 4 to Mary 5, the converted file will have the same input name
	 * 
	 * @param gvInFile
	 *            gvInFile
	 * @throws IOException
	 *             IOException
	 */
	public void convertGvBinaryFile(File gvInFile) throws IOException {
		int i;
		String gvInFileString = gvInFile.getName();
		// the destination file name will be the same as the input file so
		String gvOutFile = gvInFile.getAbsolutePath();
		String path = gvInFile.getParent();
		// I make a copy or the original file
		FileUtils.copy(gvInFile.getAbsolutePath(), path + "/tmp");
		gvInFile = new File(path + "/tmp");

		DataInputStream dataIn;
		DataOutputStream dataOut;
		dataIn = new DataInputStream(new BufferedInputStream(new FileInputStream(gvInFile)));

		// int numMix = data_in.readShort(); /* --NOT USED -- first short is the number of mixtures in Gaussian model */
		int order = dataIn.readShort(); /* second short is the order of static vector */
		float gvmean[] = new float[order]; /* allocate memory of this size */
		float gvcov[] = new float[order];
		logger.debug("Reading from file " + gvInFileString + " order=" + order);

		for (i = 0; i < order; i++) {
			gvmean[i] = dataIn.readFloat();
			// System.out.format("gvmean[%d]=%f\n",i,gvmean[i]);
		}
		for (i = 0; i < order; i++) {
			gvcov[i] = dataIn.readFloat();
			// System.out.format("gvcov[%d]=%f\n",i,gvcov[i]);
		}
		dataIn.close();

		dataOut = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(gvOutFile)));
		/* This is the format in version 2.0 */
		// numMSDFlag
		dataOut.writeInt(0);
		// numStream
		dataOut.writeInt(1);
		// vectorSize
		dataOut.writeInt(order);
		// numDurPdf
		dataOut.writeInt(1);

		for (i = 0; i < order; i++) {
			dataOut.writeFloat(gvmean[i]);
			dataOut.writeFloat(gvcov[i]);
		}

		dataOut.close();
		gvInFile.delete();
		logger.debug("Updated format in file " + gvOutFile);

	}

	public static void main(String[] args) {
		if (args.length < 2) {
			usage();
			System.exit(1);
		}

		File componentsFile = new File(args[0]);
		if (!componentsFile.exists()) {
			System.err.println("No component file: " + args[0]);
			usage();
			System.exit(1);
		}

		List voiceDescriptions = null;
		try {
			InstallFileParser parser = new InstallFileParser(componentsFile.toURI().toURL());
			voiceDescriptions = parser.getVoiceDescriptions();
		} catch (Exception e) {
			e.printStackTrace();
			usage();
			System.exit(1);
		}

		for (int i = 1; i < args.length; i++) {
			File voiceZip = new File(args[i]);
			if (!voiceZip.exists()) {
				System.err.println("No such voice file: " + args[i]);
				usage();
				System.exit(1);
			}
			try {
				new Mary4To5VoiceConverter(voiceDescriptions, voiceZip).convert();
			} catch (Exception e) {
				e.printStackTrace();
				usage();
				System.exit(1);
			}
		}

	}

	private static void usage() {
		System.err.println("Usage:");
		System.err.println("java " + Mary4To5VoiceConverter.class.getName()
				+ " mary-components.xml mary-voice-file-4.3.0.zip [more voice files...]");
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy